[GH-ISSUE #6818] [Bug] Mach-O section and segment names lose surrounding whitespace #4166

Closed
opened 2026-10-31 00:47:21 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 23, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6818

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

The production parseOtoolLoadCommands function changes the section name " sec " to "sec" and its segment name " seg " to "seg". A generated Mach-O object contains the original names, and real llvm-otool -h -l output preserves their spaces. REA silently trims them in both the latest published release and the inspected main revision.

This is a production-parser reproduction on Linux, not an end-to-end macOS CLI run.

Reproduction

From a source checkout with locked dependencies installed and llvm-otool available, save the following as repro.mjs, then run node --import tsx repro.mjs "$PWD/src/native/parsers/otool.ts". It generates a minimal x86_64 Mach-O object containing one section; no target code executes.

import {mkdtempSync,writeFileSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {execFileSync} from 'node:child_process';
import {pathToFileURL} from 'node:url';
const [modulePath, rawOutputPath]=process.argv.slice(2);
const {parseOtoolLoadCommands}=await import(pathToFileURL(modulePath).href);
const dir=mkdtempSync(join(tmpdir(),'rea-names-'));
const path=join(dir,'fixture.o');
const b=Buffer.alloc(192);
for(const [at,value] of [[0,0xfeedfacf],[4,0x01000007],[8,3],[12,1],[16,1],[20,152],[32,0x19],[36,152],[88,7],[92,7],[96,1],[152,184]])b.writeUInt32LE(value,at);
b.writeBigUInt64LE(8n,64);b.writeBigUInt64LE(184n,72);b.writeBigUInt64LE(8n,80);
b.write(' sec ',104);b.write(' seg ',120);b.writeBigUInt64LE(8n,144);b.write('fixture',184);
writeFileSync(path,b);
const output=execFileSync('llvm-otool',['-h','-l',path],{encoding:'utf8'});
if(rawOutputPath)writeFileSync(rawOutputPath,output);
console.log('Encoded names:',JSON.stringify({name:b.subarray(104,109).toString(),segment:b.subarray(120,125).toString()}));
console.log('Producer fields:',JSON.stringify(output.split('\n').filter(line=>/segname|sectname/.test(line))));
const section=parseOtoolLoadCommands(output).segments[0].sections[0];
console.log('REA names:',JSON.stringify({name:section.name,segment:section.segment}));
console.log('Producer exit: 0');

Executed once against the release parser and once against the main parser, with the same result below.

Expected behavior

Preserve " sec " and " seg " as distinct identities from "sec" and "seg". machoName explicitly describes a segment or section name as exactly observed. Apple's Mach-O section definitions store these as fixed-width character names.

Impact and scope

A successful static parse silently changes names used to identify sections and their containing segment. Whitespace-distinct names can therefore become indistinguishable to consumers. The demonstrated scope is one small valid Mach-O object and its genuine LLVM producer output; full macOS CLI behavior and real-world frequency were not measured. Reading the raw producer output preserves the names.

REA version

Latest published rea-agents 6.7.0, source 27dc42d10249f0410904a75c0997842eb03b931c; also reproduced against the parser from main 567b55947486516b9d35d30b7202ac69c6f40f6d.

Runtime and platform

Node.js v24.18.0; Linux x86_64; LLVM llvm-otool 23.1.3. No Hopper/Ghidra provider was used.

Target and artifact context

Generated 192-byte, little-endian x86_64 MH_OBJECT with one LC_SEGMENT_64 and one eight-byte section. Its section name is " sec " and segment identity is " seg ".

Interface

Other

Earliest failure stage

Transport or result projection

Relevant output or evidence

Actual output from both parser runs:

Encoded names: {"name":" sec ","segment":" seg "}
Producer fields: ["  segname ","  sectname  sec ","   segname  seg "]
REA names: {"name":"sec","segment":"seg"}
Producer exit: 0

Typed outcome or recovery expected

Return the original section/segment names while retaining normal numeric-field parsing and support for the genuinely unnamed object-file segment.

Analysis or suspected cause

Source-derived explanation: parseField trims the entire line, consumes all whitespace after the label, and trims the captured value. TEXT_FIELDS prevents numeric coercion of sectname and segname but does not preserve their whitespace. The dedicated whitespace-preserving handling of offset strings and linker options does not cover these fields.

Additional context

Related closed #5680 concerned rejecting the literal name version through greedy field-label parsing; this report concerns successful parsing that changes whitespace-bearing names. Closed #3875 concerned the fallback used for a genuinely unnamed object-file segment. Neither is the demonstrated whitespace-loss behavior.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The production parser silently changes exact section identities for the demonstrated valid input and can conflate distinct names.
  • Workaround status: available
  • Workaround basis: The real llvm-otool output in the reproduction preserves the original names for direct inspection.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6818 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior The production `parseOtoolLoadCommands` function changes the section name `" sec "` to `"sec"` and its segment name `" seg "` to `"seg"`. A generated Mach-O object contains the original names, and real `llvm-otool -h -l` output preserves their spaces. REA silently trims them in both the latest published release and the inspected main revision. This is a production-parser reproduction on Linux, not an end-to-end macOS CLI run. ### Reproduction From a source checkout with locked dependencies installed and `llvm-otool` available, save the following as `repro.mjs`, then run `node --import tsx repro.mjs "$PWD/src/native/parsers/otool.ts"`. It generates a minimal x86_64 Mach-O object containing one section; no target code executes. ```js import {mkdtempSync,writeFileSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join} from 'node:path'; import {execFileSync} from 'node:child_process'; import {pathToFileURL} from 'node:url'; const [modulePath, rawOutputPath]=process.argv.slice(2); const {parseOtoolLoadCommands}=await import(pathToFileURL(modulePath).href); const dir=mkdtempSync(join(tmpdir(),'rea-names-')); const path=join(dir,'fixture.o'); const b=Buffer.alloc(192); for(const [at,value] of [[0,0xfeedfacf],[4,0x01000007],[8,3],[12,1],[16,1],[20,152],[32,0x19],[36,152],[88,7],[92,7],[96,1],[152,184]])b.writeUInt32LE(value,at); b.writeBigUInt64LE(8n,64);b.writeBigUInt64LE(184n,72);b.writeBigUInt64LE(8n,80); b.write(' sec ',104);b.write(' seg ',120);b.writeBigUInt64LE(8n,144);b.write('fixture',184); writeFileSync(path,b); const output=execFileSync('llvm-otool',['-h','-l',path],{encoding:'utf8'}); if(rawOutputPath)writeFileSync(rawOutputPath,output); console.log('Encoded names:',JSON.stringify({name:b.subarray(104,109).toString(),segment:b.subarray(120,125).toString()})); console.log('Producer fields:',JSON.stringify(output.split('\n').filter(line=>/segname|sectname/.test(line)))); const section=parseOtoolLoadCommands(output).segments[0].sections[0]; console.log('REA names:',JSON.stringify({name:section.name,segment:section.segment})); console.log('Producer exit: 0'); ``` Executed once against the release parser and once against the main parser, with the same result below. ### Expected behavior Preserve `" sec "` and `" seg "` as distinct identities from `"sec"` and `"seg"`. [`machoName`](https://github.com/morluto/rea/blob/567b55947486516b9d35d30b7202ac69c6f40f6d/src/native/parsers/otool.ts) explicitly describes a segment or section name as exactly observed. Apple's [Mach-O section definitions](https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/loader.h#L422-L434) store these as fixed-width character names. ### Impact and scope A successful static parse silently changes names used to identify sections and their containing segment. Whitespace-distinct names can therefore become indistinguishable to consumers. The demonstrated scope is one small valid Mach-O object and its genuine LLVM producer output; full macOS CLI behavior and real-world frequency were not measured. Reading the raw producer output preserves the names. ### REA version Latest published `rea-agents` 6.7.0, source `27dc42d10249f0410904a75c0997842eb03b931c`; also reproduced against the parser from main `567b55947486516b9d35d30b7202ac69c6f40f6d`. ### Runtime and platform Node.js v24.18.0; Linux x86_64; LLVM `llvm-otool` 23.1.3. No Hopper/Ghidra provider was used. ### Target and artifact context Generated 192-byte, little-endian x86_64 `MH_OBJECT` with one `LC_SEGMENT_64` and one eight-byte section. Its section name is `" sec "` and segment identity is `" seg "`. ### Interface Other ### Earliest failure stage Transport or result projection ### Relevant output or evidence Actual output from both parser runs: ```text Encoded names: {"name":" sec ","segment":" seg "} Producer fields: [" segname "," sectname sec "," segname seg "] REA names: {"name":"sec","segment":"seg"} Producer exit: 0 ``` ### Typed outcome or recovery expected Return the original section/segment names while retaining normal numeric-field parsing and support for the genuinely unnamed object-file segment. ### Analysis or suspected cause Source-derived explanation: [`parseField`](https://github.com/morluto/rea/blob/567b55947486516b9d35d30b7202ac69c6f40f6d/src/native/parsers/otool.ts#L510-L528) trims the entire line, consumes all whitespace after the label, and trims the captured value. `TEXT_FIELDS` prevents numeric coercion of `sectname` and `segname` but does not preserve their whitespace. The dedicated whitespace-preserving handling of offset strings and linker options does not cover these fields. ### Additional context Related closed #5680 concerned rejecting the literal name `version` through greedy field-label parsing; this report concerns successful parsing that changes whitespace-bearing names. Closed #3875 concerned the fallback used for a genuinely unnamed object-file segment. Neither is the demonstrated whitespace-loss behavior. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The production parser silently changes exact section identities for the demonstrated valid input and can conflate distinct names. - Workaround status: available - Workaround basis: The real llvm-otool output in the reproduction preserves the original names for direct inspection.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4166
No description provided.