llvmorg-github-actions[bot] wrote:

<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lldb

Author: Yao Qi (qiyao)

<details>
<summary>Changes</summary>

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 &gt; 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.


---
Full diff: https://github.com/llvm/llvm-project/pull/204471.diff


4 Files Affected:

- (modified) 
lldb/source/Plugins/ObjectContainer/Universal-Mach-O/ObjectContainerUniversalMachO.cpp
 (+11-1) 
- (modified) lldb/test/API/commands/target/basic/TestTargetCommand.py (+26) 
- (added) lldb/test/API/commands/target/basic/fat_nfat_arch_huge.yaml (+30) 
- (added) lldb/test/API/commands/target/basic/fat_slice_offset_zero.yaml (+30) 


``````````diff
diff --git 
a/lldb/source/Plugins/ObjectContainer/Universal-Mach-O/ObjectContainerUniversalMachO.cpp
 
b/lldb/source/Plugins/ObjectContainer/Universal-Mach-O/ObjectContainerUniversalMachO.cpp
index d7b1710dce05d..f3127ef920982 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;
@@ -202,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);
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:    []
+...

``````````

</details>


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

Reply via email to