https://github.com/qiyao created 
https://github.com/llvm/llvm-project/pull/204471

This fixes two denial-of-service issues in the universal (fat) Mach-O
container parser, both found by `lldb-target-fuzzer` and both reachable from
`SBDebugger::CreateTarget` → `ObjectFile::GetModuleSpecifications` →
`ObjectContainerUniversalMachO::{GetModuleSpecifications,ParseHeader}`.

### 1. Bound the fat header arch loop by available data

`ParseHeader` read the untrusted 32-bit `nfat_arch` field and used it directly
as the loop bound when indexing the `fat_arch` records, without validating it
against the data actually present. A crafted header claiming
`nfat_arch = 0xFFFFFFFF` with only a few bytes of payload made the loop spin
~4.29 billion times — `ValidOffsetForDataOfSize()` fails on every iteration
past the real data, but the loop keeps going up to `nfat_arch`, so the call
never returns in practice (~20 minutes under ASan). The reduced reproducer is
40 bytes.

Fix: break out of the loop as soon as the next `fat_arch` record no longer fits
in the remaining data. This is behavior-preserving — a failed
`ValidOffsetForDataOfSize` never advanced the offset, so the set of parsed
`fat_archs` is unchanged; it just stops the useless spinning.

### 2. Reject self-referential slices in the fat container

`GetModuleSpecifications` re-parses every fat slice by recursing into
`ObjectFile::GetModuleSpecifications` at the slice's offset, with no
recursion-depth limit and no check that a slice advances past the current
offset. A slice whose offset is 0 produces a recursive call with identical
arguments (same file, same offset, same size) and recurses until the stack
overflows. Fixing the `nfat_arch` loop bound above unmasked this for the same
input: the initial 512-byte read zero-pads a short file, `ParseHeader` then
parses a trailing all-zero arch record whose offset is 0, and the slice
recurses on itself.

Fix: require `slice_file_offset > file_offset` before recursing, so every
recursive call strictly advances into the file. Offsets at or before the
current offset (including 0) and offsets inside the fat header are skipped.

### Tests

Two API tests in `TestTargetCommand.py`, each with a `yaml2obj` fixture:
- `fat_nfat_arch_huge.yaml` — `nfat_arch = 0xFFFFFFFF` with a single real
  slice; `target create` must complete promptly.
- `fat_slice_offset_zero.yaml` — a self-referential slice at offset 0;
  `target create` must skip it and fail gracefully ("is not a valid
  executable") rather than crashing.


>From a91eba0305efd74e97d99daf37599e2e14abe329 Mon Sep 17 00:00:00 2001
From: Yao Qi <[email protected]>
Date: Tue, 16 Jun 2026 22:30:34 +0100
Subject: [PATCH 1/3] [lldb][Mach-O] Bound the fat header arch loop by
 available data

`ObjectContainerUniversalMachO::ParseHeader` reads the 32-bit `nfat_arch`
field straight from the file and uses it as the loop bound when indexing
the fat_arch records, without validating it against the amount of data
actually present.  A crafted fat header that claims nfat_arch = 0xFFFFFFFF
while providing only a few bytes makes the loop iterate ~4.29 billion
times: `ValidOffsetForDataOfSize()` fails on every iteration past the real
data, but the loop keeps spinning up to `nfat_arch`, so the call does not
return in any practical amount of time (~20 minutes under ASan).

Found by `lldb-target-fuzzer`, reachable from `SBDebugger::CreateTarget`
-> `ObjectFile::GetModuleSpecifications` ->
`ObjectContainerUniversalMachO::{GetModuleSpecifications,ParseHeader}`. The
reduced reproducer is 40 bytes: a `FAT_MAGIC_64` header with
`nfat_arch = 0xFFFFFFFF` followed by a single, partial arch record.

Break out of the loop as soon as the next fat_arch record no longer fits
in the remaining data.  This is behavior-preserving, a failed
`ValidOffsetForDataOfSize` never advances the offset, so the set of parsed
fat_archs is unchanged.  It just stops the useless spinning.
---
 .../Universal-Mach-O/ObjectContainerUniversalMachO.cpp       | 5 +++++
 1 file changed, 5 insertions(+)

diff --git 
a/lldb/source/Plugins/ObjectContainer/Universal-Mach-O/ObjectContainerUniversalMachO.cpp
 
b/lldb/source/Plugins/ObjectContainer/Universal-Mach-O/ObjectContainerUniversalMachO.cpp
index d7b1710dce05d..6193fc5bdd39d 100644
--- 
a/lldb/source/Plugins/ObjectContainer/Universal-Mach-O/ObjectContainerUniversalMachO.cpp
+++ 
b/lldb/source/Plugins/ObjectContainer/Universal-Mach-O/ObjectContainerUniversalMachO.cpp
@@ -122,6 +122,11 @@ bool ObjectContainerUniversalMachO::ParseHeader(
           arch.align = extractor.GetU32(&offset);
           fat_archs.emplace_back(arch);
         }
+      } else {
+        // nfat_arch is read from the file and is untrusted (up to 0xFFFFFFFF).
+        // Once the data is exhausted the offset can no longer advance, so the
+        // remaining iterations would spin uselessly; stop here.
+        break;
       }
     }
     return true;

>From 8f6e4598b5062a6664b7c55c7a4ac40553779d26 Mon Sep 17 00:00:00 2001
From: Yao Qi <[email protected]>
Date: Tue, 16 Jun 2026 22:31:46 +0100
Subject: [PATCH 2/3] [lldb][Mach-O] Reject self-referential slices in the fat
 container

`ObjectContainerUniversalMachO::GetModuleSpecifications` re-parses every fat
slice by recursing into `ObjectFile::GetModuleSpecifications` at the slice's
offset. There is no recursion-depth limit and no check that a slice
advances past the current offset, so a slice whose offset is 0 produces a
recursive call with identical arguments (same file, same file_offset, same
size) and recurses until the stack overflows.

Found by `lldb-target-fuzzer`, reachable from `SBDebugger::CreateTarget`
 -> `ObjectFile::GetModuleSpecifications` ->
`ObjectContainerUniversalMachO::GetModuleSpecifications`, which calls back
into `ObjectFile::GetModuleSpecifications`. Fixing the `nfat_arch` loop bound
(previous commit) unmasked this for the same input: the initial 512-byte
read zero-pads a short file, `ParseHeader` then parses a trailing all-zero
arch record whose offset is 0, and the slice recurses on itself.

Require `slice_file_offset > file_offset` before recursing, so every
recursive call strictly advances into the file.  Offsets that land at or
before the current offset, including 0, and offsets inside the fat
header, are skipped.
---
 .../Universal-Mach-O/ObjectContainerUniversalMachO.cpp     | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git 
a/lldb/source/Plugins/ObjectContainer/Universal-Mach-O/ObjectContainerUniversalMachO.cpp
 
b/lldb/source/Plugins/ObjectContainer/Universal-Mach-O/ObjectContainerUniversalMachO.cpp
index 6193fc5bdd39d..f3127ef920982 100644
--- 
a/lldb/source/Plugins/ObjectContainer/Universal-Mach-O/ObjectContainerUniversalMachO.cpp
+++ 
b/lldb/source/Plugins/ObjectContainer/Universal-Mach-O/ObjectContainerUniversalMachO.cpp
@@ -207,7 +207,12 @@ ModuleSpecList 
ObjectContainerUniversalMachO::GetModuleSpecifications(
       for (const FatArch &fat_arch : fat_archs) {
         const lldb::offset_t slice_file_offset =
             fat_arch.GetOffset() + file_offset;
-        if (fat_arch.GetOffset() < file_size && file_size > slice_file_offset) 
{
+        // The slice must start strictly past the current offset.  A slice 
whose
+        // offset is 0 (or otherwise does not advance) is self-referential: the
+        // recursive call below would re-parse the same range and recurse
+        // without bound.  Skip such slices instead of overflowing the stack.
+        if (slice_file_offset > file_offset &&
+            fat_arch.GetOffset() < file_size && file_size > slice_file_offset) 
{
           ModuleSpecList arch_specs = ObjectFile::GetModuleSpecifications(
               file, slice_file_offset, file_size - slice_file_offset);
           specs.Append(arch_specs);

>From 7b65b6f52bca6310c26343ffe1dd2633dee25bd9 Mon Sep 17 00:00:00 2001
From: Yao Qi <[email protected]>
Date: Wed, 17 Jun 2026 20:37:56 +0100
Subject: [PATCH 3/3] Two test cases

---
 .../target/basic/TestTargetCommand.py         | 26 ++++++++++++++++
 .../target/basic/fat_nfat_arch_huge.yaml      | 30 +++++++++++++++++++
 .../target/basic/fat_slice_offset_zero.yaml   | 30 +++++++++++++++++++
 3 files changed, 86 insertions(+)
 create mode 100644 lldb/test/API/commands/target/basic/fat_nfat_arch_huge.yaml
 create mode 100644 
lldb/test/API/commands/target/basic/fat_slice_offset_zero.yaml

diff --git a/lldb/test/API/commands/target/basic/TestTargetCommand.py 
b/lldb/test/API/commands/target/basic/TestTargetCommand.py
index dd6bdb234108a..f6870dc2e7efe 100644
--- a/lldb/test/API/commands/target/basic/TestTargetCommand.py
+++ b/lldb/test/API/commands/target/basic/TestTargetCommand.py
@@ -147,6 +147,32 @@ def test_target_create_unsupported_platform(self):
             patterns=["error: no matching platforms found for this file"],
         )
 
+    @no_debug_info_test
+    def test_target_create_fat_macho_huge_nfat_arch(self):
+        """A fat Mach-O header whose nfat_arch is larger than the data must not
+        send the arch-indexing loop spinning."""
+        yaml = os.path.join(self.getSourceDir(), "fat_nfat_arch_huge.yaml")
+        exe = self.getBuildArtifact("fat_nfat_arch_huge")
+        self.yaml2obj(yaml, exe)
+        # Before the fix this loops ~0xFFFFFFFF times and test timeout.  The
+        # test "passing" here just means target create completes promptly.
+        self.expect("target create {}".format(exe))
+
+    @no_debug_info_test
+    def test_target_create_fat_macho_self_referential_slice(self):
+        """A fat Mach-O slice at offset 0 is self-referential.  Parsing it must
+        not recurse until the stack overflows."""
+        yaml = os.path.join(self.getSourceDir(), "fat_slice_offset_zero.yaml")
+        exe = self.getBuildArtifact("fat_slice_offset_zero")
+        self.yaml2obj(yaml, exe)
+        # Before the fix this recurses on identical arguments and crashes.  The
+        # non-advancing slice is now skipped, leaving nothing loadable.
+        self.expect(
+            "target create {}".format(exe),
+            error=True,
+            patterns=["is not a valid executable"],
+        )
+
     @no_debug_info_test
     def test_target_create_invalid_platform(self):
         self.buildB()
diff --git a/lldb/test/API/commands/target/basic/fat_nfat_arch_huge.yaml 
b/lldb/test/API/commands/target/basic/fat_nfat_arch_huge.yaml
new file mode 100644
index 0000000000000..4052d044d2449
--- /dev/null
+++ b/lldb/test/API/commands/target/basic/fat_nfat_arch_huge.yaml
@@ -0,0 +1,30 @@
+--- !fat-mach-o
+# Regression fixture: a universal (fat) Mach-O whose header claims
+# nfat_arch = 0xFFFFFFFF while the file holds a single arch slice.  The arch
+# loop in ObjectContainerUniversalMachO::ParseHeader used nfat_arch as its
+# bound without checking it against the available data, so this header sent it
+# spinning ~4.29 billion times.  "target create" must instead parse the one
+# real slice and return promptly.  Found by lldb-target-fuzzer.
+FatHeader:
+  magic:           0xCAFEBABF
+  nfat_arch:       0xFFFFFFFF
+FatArchs:
+  - cputype:         0x01000007
+    cpusubtype:      0x00000003
+    offset:          0x0000000000004000
+    size:            4
+    align:           14
+    reserved:        0x00000000
+Slices:
+  - !mach-o
+    FileHeader:
+      magic:           0xFEEDFACF
+      cputype:         0x01000007
+      cpusubtype:      0x00000003
+      filetype:        0x00000002
+      ncmds:           0
+      sizeofcmds:      0
+      flags:           0x00000000
+      reserved:        0x00000000
+    LoadCommands:    []
+...
diff --git a/lldb/test/API/commands/target/basic/fat_slice_offset_zero.yaml 
b/lldb/test/API/commands/target/basic/fat_slice_offset_zero.yaml
new file mode 100644
index 0000000000000..ea5d120026791
--- /dev/null
+++ b/lldb/test/API/commands/target/basic/fat_slice_offset_zero.yaml
@@ -0,0 +1,30 @@
+--- !fat-mach-o
+# Regression fixture: a universal (fat) Mach-O with a self-referential slice
+# whose offset is 0.  ObjectContainerUniversalMachO::GetModuleSpecifications
+# re-parsed each slice by recursing into ObjectFile::GetModuleSpecifications at
+# the slice offset; a slice at offset 0 produced a recursive call with
+# identical arguments and recursed until the stack overflowed.  "target create"
+# must skip the non-advancing slice and fail gracefully instead of crashing.
+# Found by lldb-target-fuzzer.
+FatHeader:
+  magic:           0xCAFEBABE
+  nfat_arch:       1
+FatArchs:
+  - cputype:         0x00000007
+    cpusubtype:      0x00000003
+    offset:          0x00000000
+    size:            0x00001000
+    align:           12
+Slices:
+  - !mach-o
+    FileHeader:
+      magic:           0xFEEDFACF
+      cputype:         0x00000007
+      cpusubtype:      0x00000003
+      filetype:        0x00000002
+      ncmds:           0
+      sizeofcmds:      0
+      flags:           0x00000000
+      reserved:        0x00000000
+    LoadCommands:    []
+...

_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to