fix(wasm): preserve debug module names in WABT output (#1967)
* fix(wasm): preserve debug module names in WABT output * fix(native): accept otool dylib-use options Preserve modern dylib options while requiring one encoding metadata row and retaining duplicate and multiline-output checks. Share field parsing between preservation and validation. Verify delayed dylibs through real macOS CLI/MCP workflows and refresh the high-address golden with the provider's -h -l capture. Validated 85 focused native tests, npm run check, and verify:macos-bundle on macOS arm64. --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
parent
1d4c4eb030
commit
b777e5d4d8
|
|
@ -1387,7 +1387,9 @@ reports a named skip; `--require-tools` makes missing configuration fail.
|
|||
|
||||
The verifier creates and validates real modules with custom sections, multiple
|
||||
import/export kinds, escaped names, an empty module, and a bulk-memory
|
||||
DataCount section. It compares exact WAT,
|
||||
DataCount section. Debug-name fixtures cover compiler-generated module names
|
||||
and preserved name-section payloads with empty, multiline, Unicode, long and
|
||||
NUL-containing names. It compares exact WAT,
|
||||
section output and selected/tool digests through the compiled CLI and real MCP
|
||||
SDK, validates advertised schemas, checks identical CLI/MCP Evidence IDs and
|
||||
session bundle readback, retains invalid/truncated module diagnostics, preserves
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ into a private workspace as read-only `module.wasm`; its digest is rechecked
|
|||
before and after each tool. WABT validates with `--enable-all`,
|
||||
reports sections through `wasm-objdump -h -x`, and decodes through
|
||||
`wasm2wat --enable-all`. The result retains selected byte SHA-256 and size,
|
||||
section payload ranges, full header/detail output, exact import/export WAT
|
||||
section payload ranges, full header/detail output (including module-name
|
||||
preambles), exact import/export WAT
|
||||
forms, decoded WAT and its UTF-8 SHA-256. Producer format drift and invalid or
|
||||
truncated modules are failures, with validation diagnostics and partial Evidence
|
||||
retaining selected byte identity and tool provenance.
|
||||
|
|
|
|||
|
|
@ -131,15 +131,35 @@ try {
|
|||
i32.const 1
|
||||
memory.init 0
|
||||
data.drop 0))\n`,
|
||||
];
|
||||
for (const [index, source] of sources.entries()) {
|
||||
].map((source) => ({ source, debugNames: false }));
|
||||
sources.push(
|
||||
{
|
||||
source:
|
||||
'(module $named (func (export "answer") (result i32) i32.const 42))\n',
|
||||
debugNames: true,
|
||||
},
|
||||
{
|
||||
source: await readFile(
|
||||
new URL("../tests/fixtures/wasm/module-names.wat", import.meta.url),
|
||||
"utf8",
|
||||
),
|
||||
debugNames: false,
|
||||
},
|
||||
);
|
||||
for (const [index, { source, debugNames }] of sources.entries()) {
|
||||
const fixtureRoot = join(root.path, `fixture-${index}`);
|
||||
await mkdir(fixtureRoot);
|
||||
const path = join(fixtureRoot, "module.wasm");
|
||||
await writeFile(join(fixtureRoot, "source.wat"), source);
|
||||
await tool(
|
||||
"wat2wasm",
|
||||
["--enable-all", "source.wat", "-o", "module.wasm"],
|
||||
[
|
||||
"--enable-all",
|
||||
...(debugNames ? ["--debug-names"] : []),
|
||||
"source.wat",
|
||||
"-o",
|
||||
"module.wasm",
|
||||
],
|
||||
fixtureRoot,
|
||||
);
|
||||
await tool("wasm-validate", ["--enable-all", "module.wasm"], fixtureRoot);
|
||||
|
|
@ -229,6 +249,10 @@ try {
|
|||
'"unicode-é\\\t"',
|
||||
],
|
||||
);
|
||||
if (debugNames) {
|
||||
assert.ok(report.headers.includes("\nmodule name: <named>\n"));
|
||||
assert.deepEqual(report.exports, ['(export "answer" (func 0))']);
|
||||
}
|
||||
if (index === 4) {
|
||||
assert.match(
|
||||
headers,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,19 @@ export async function verifyNativeProvenance(app, executable) {
|
|||
assert.equal(mcp.raw_result, null);
|
||||
assert.ok(cli.normalized_result.provenance.length > 0);
|
||||
assert.deepEqual(mcp.normalized_result, cli.normalized_result);
|
||||
if (operation === "inspect_macho") {
|
||||
const delayed = cli.normalized_result.load_commands.items.find(
|
||||
({ fields }) => fields.name === "@rpath/libdelay.dylib (offset 28)",
|
||||
);
|
||||
assert.equal(delayed?.kind, "LC_LOAD_DYLIB");
|
||||
assert.equal(delayed.fields.options, "delay-init");
|
||||
assert.equal(delayed.fields["time stamp"], undefined);
|
||||
assert.ok(
|
||||
cli.normalized_result.dependencies.items.some(
|
||||
({ path }) => path === "@rpath/libdelay.dylib",
|
||||
),
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
JSON.parse(response.content.find((item) => item.type === "text").text),
|
||||
response.structuredContent,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,16 @@ const DYLIB_COMMANDS = new Set([
|
|||
"LC_LOAD_UPWARD_DYLIB",
|
||||
]);
|
||||
|
||||
const DYLIB_FIELDS = [
|
||||
"cmd",
|
||||
"cmdsize",
|
||||
"name",
|
||||
"current version",
|
||||
"compatibility version",
|
||||
];
|
||||
// otool prints options for dylib_use_command and a timestamp for dylib_command.
|
||||
const DYLIB_ENCODING_FIELDS = ["time stamp", "options"];
|
||||
|
||||
/**
|
||||
* otool emits LF, but a capture replayed from another host or toolchain can
|
||||
* carry CRLF. The section/command boundaries below match a bare `\n`, so a
|
||||
|
|
@ -195,7 +205,7 @@ const parseLoadCommand = (block: string, state: ParsedLoadCommands): void => {
|
|||
const kind = stringField(fields, "cmd");
|
||||
if (kind === null) throw new TypeError("otool load command is missing cmd");
|
||||
for (const key of scalarFields(kind)) requireUniqueField(header, key);
|
||||
if (DYLIB_COMMANDS.has(kind)) validateDylibCommandLines(header);
|
||||
if (DYLIB_COMMANDS.has(kind)) validateDylibCommandLines(header, fields);
|
||||
state.commands.push({
|
||||
index,
|
||||
kind,
|
||||
|
|
@ -208,19 +218,23 @@ const parseLoadCommand = (block: string, state: ParsedLoadCommands): void => {
|
|||
collectSegment(kind, block, fields, state);
|
||||
};
|
||||
|
||||
const validateDylibCommandLines = (block: string): void => {
|
||||
const allowed = new Set([
|
||||
"cmd",
|
||||
"cmdsize",
|
||||
"name",
|
||||
"time stamp",
|
||||
"current version",
|
||||
"compatibility version",
|
||||
]);
|
||||
const validateDylibCommandLines = (
|
||||
block: string,
|
||||
fields: Readonly<Record<string, string | number | null>>,
|
||||
): void => {
|
||||
const encodingFields = DYLIB_ENCODING_FIELDS.filter((key) =>
|
||||
Object.hasOwn(fields, key),
|
||||
);
|
||||
if (encodingFields.length !== 1)
|
||||
throw new TypeError(
|
||||
"otool dylib command must contain timestamp or options",
|
||||
);
|
||||
for (const key of encodingFields) requireUniqueField(block, key);
|
||||
const allowed = new Set([...DYLIB_FIELDS, ...DYLIB_ENCODING_FIELDS]);
|
||||
for (const rawLine of block.split("\n").slice(1)) {
|
||||
const line = rawLine.trim();
|
||||
if (line.length === 0) continue;
|
||||
const key = /^(time stamp|\S+(?:\s+version)?)\s+/u.exec(line)?.[1];
|
||||
const key = parseField(line)?.[0];
|
||||
if (key === undefined || !allowed.has(key))
|
||||
throw new TypeError(
|
||||
"otool dylib command contains ambiguous multiline text",
|
||||
|
|
@ -230,14 +244,7 @@ const validateDylibCommandLines = (block: string): void => {
|
|||
|
||||
const scalarFields = (kind: string): readonly string[] => {
|
||||
const common = ["cmd", "cmdsize"];
|
||||
if (DYLIB_COMMANDS.has(kind))
|
||||
return [
|
||||
...common,
|
||||
"name",
|
||||
"time stamp",
|
||||
"current version",
|
||||
"compatibility version",
|
||||
];
|
||||
if (DYLIB_COMMANDS.has(kind)) return DYLIB_FIELDS;
|
||||
if (kind === "LC_LOAD_DYLINKER" || kind === "LC_ID_DYLINKER")
|
||||
return [...common, "name"];
|
||||
if (kind === "LC_UUID") return [...common, "uuid"];
|
||||
|
|
@ -339,25 +346,31 @@ const collectSegment = (
|
|||
const parseFields = (block: string): Record<string, string | number | null> => {
|
||||
const fields: Record<string, string | number | null> = {};
|
||||
for (const rawLine of block.split(/\r?\n/u).slice(1)) {
|
||||
const line = rawLine.trim();
|
||||
// Keys are `token` or a compound `token version`. `time stamp` is the one
|
||||
// other real key, and it must be matched as a whole: treating `stamp` as a
|
||||
// generic suffix would swallow an install name such as
|
||||
// `name stamp plugin.dylib`, hiding the dependency path behind `unknown`.
|
||||
const match = /^((?:time stamp)|\S+(?:\s+version)?)\s+(.+)$/u.exec(line);
|
||||
if (match?.[1] === undefined || match[2] === undefined) continue;
|
||||
const value = match[2].trim();
|
||||
fields[match[1]] =
|
||||
match[1] === "segname" || match[1] === "sectname"
|
||||
const field = parseField(rawLine);
|
||||
if (field === undefined) continue;
|
||||
const [key, value] = field;
|
||||
fields[key] =
|
||||
key === "segname" || key === "sectname"
|
||||
? value
|
||||
: (numeric(value) ?? value);
|
||||
}
|
||||
return fields;
|
||||
};
|
||||
|
||||
const parseField = (rawLine: string): readonly [string, string] | undefined => {
|
||||
const line = rawLine.trim();
|
||||
// A dylib-use command with no flags still has an empty options row.
|
||||
if (line === "options") return ["options", ""];
|
||||
// Match time stamp as a whole without swallowing `name stamp plugin.dylib`.
|
||||
const match = /^((?:time stamp)|\S+(?:\s+version)?)\s+(.+)$/u.exec(line);
|
||||
return match?.[1] === undefined || match[2] === undefined
|
||||
? undefined
|
||||
: [match[1], match[2].trim()];
|
||||
};
|
||||
|
||||
const requireUniqueField = (block: string, key: string): void => {
|
||||
const escaped = key.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
const pattern = new RegExp(`^\\s*${escaped}\\s+`, "mu");
|
||||
const pattern = new RegExp(`^\\s*${escaped}(?:\\s|$)`, "mu");
|
||||
let count = 0;
|
||||
for (const line of block.split("\n")) if (pattern.test(line)) count += 1;
|
||||
if (count !== 1)
|
||||
|
|
|
|||
|
|
@ -323,9 +323,8 @@ export class WabtArtifactProvider implements WasmArtifactPort {
|
|||
OPERATION,
|
||||
"Unexpected WABT decoding output.",
|
||||
);
|
||||
const { imports, exports, customSectionNames } = parseWabtWat(
|
||||
wat.stdout.text,
|
||||
);
|
||||
const { imports, exports, customSectionNames, debugModuleNames } =
|
||||
parseWabtWat(wat.stdout.text);
|
||||
capturedOutput = {
|
||||
stdout: dump.stdout.text,
|
||||
stderr: dump.stderr.text,
|
||||
|
|
@ -335,6 +334,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
|
|||
dump.stdout.text,
|
||||
artifact.bytes,
|
||||
customSectionNames,
|
||||
debugModuleNames,
|
||||
);
|
||||
const limitations = [
|
||||
"WAT is a decoded representation, not original source or proof of runtime behavior. Custom annotations may retain payloads, but WAT does not establish original binary layout or round-trip byte identity. Original bytes and objdump output remain authoritative.",
|
||||
|
|
|
|||
|
|
@ -138,6 +138,28 @@ it("accepts an unpadded DataCount row and still rejects malformed output and bad
|
|||
),
|
||||
).toThrow(/header/u);
|
||||
});
|
||||
it.each([
|
||||
{ displayed: [], decoded: ["named"] },
|
||||
{ displayed: ["named"], decoded: [] },
|
||||
{ displayed: ["other"], decoded: ["named"] },
|
||||
])(
|
||||
"rejects module-name headers inconsistent with decoded WAT: %j",
|
||||
async ({ displayed, decoded }) => {
|
||||
const text = (await golden()).replace(
|
||||
"\n\nSections:",
|
||||
`\n${displayed.map((name) => `module name: <${name}>\n`).join("")}\nSections:`,
|
||||
);
|
||||
expect(() => parseWabtOutput(text, 76, ["test"], decoded)).toThrow(
|
||||
/header format/u,
|
||||
);
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
'(module (@custom "name" "\\00\\06\\05name"))',
|
||||
'(module (@custom "name" "\\00\\05\\05name"))',
|
||||
])("rejects incomplete decoded module-name metadata: %s", (text) => {
|
||||
expect(() => parseWabtWat(text)).toThrow(/metadata/u);
|
||||
});
|
||||
it("extracts escaped WAT import/export forms while retaining multiline producer text", async () => {
|
||||
const text = await readFile(
|
||||
new URL("../../tests/fixtures/wasm/decoded.wat", import.meta.url),
|
||||
|
|
@ -147,6 +169,7 @@ it("extracts escaped WAT import/export forms while retaining multiline producer
|
|||
imports: ['(import "env" "foo" (func (;0;) (type 0)))'],
|
||||
exports: ['(export "memory" (memory 0))', '(export "run" (func 1))'],
|
||||
customSectionNames: ["test"],
|
||||
debugModuleNames: [],
|
||||
});
|
||||
expect(
|
||||
parseWabtWat(
|
||||
|
|
@ -156,6 +179,7 @@ it("extracts escaped WAT import/export forms while retaining multiline producer
|
|||
imports: ['(import "a.b" "c\\0aline" (func))'],
|
||||
exports: ['(export "fake\\22Export[1]:" (func 0))'],
|
||||
customSectionNames: [],
|
||||
debugModuleNames: [],
|
||||
});
|
||||
expect(
|
||||
parseWabtWat(
|
||||
|
|
|
|||
|
|
@ -3,12 +3,21 @@ export const parseWabtOutput = (
|
|||
text: string,
|
||||
artifactBytes: number,
|
||||
customSectionNames: readonly string[],
|
||||
debugModuleNames: readonly string[] = [],
|
||||
) => {
|
||||
const prefix =
|
||||
/^\nmodule\.wasm:\tfile format wasm 0x1\n\nSections:\n\n/u.exec(text);
|
||||
if (prefix === null)
|
||||
// WABT prints module names with printf's %.*s: embedded NUL ends the display.
|
||||
const preamble =
|
||||
"\nmodule.wasm:\tfile format wasm 0x1\n" +
|
||||
debugModuleNames
|
||||
.map((name) => {
|
||||
const displayedName = name.split("\0", 1)[0];
|
||||
return `module name: <${displayedName}>\n`;
|
||||
})
|
||||
.join("");
|
||||
const prefix = `${preamble}\nSections:\n\n`;
|
||||
if (!text.startsWith(prefix))
|
||||
throw new Error("Unsupported WABT section header format.");
|
||||
let offset = prefix[0].length;
|
||||
let offset = prefix.length;
|
||||
let customIndex = 0;
|
||||
const sections = [];
|
||||
while (!text.startsWith("\nSection Details:\n", offset)) {
|
||||
|
|
@ -61,6 +70,6 @@ export const parseWabtOutput = (
|
|||
return {
|
||||
sections,
|
||||
headers: text.slice(0, separator),
|
||||
details: `module.wasm:\tfile format wasm 0x1\n\n${text.slice(separator)}`,
|
||||
details: `${preamble.slice(1)}\n${text.slice(separator)}`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
/** Decode only a producer custom-section name to frame objdump's raw display text. */
|
||||
const customSectionName = (form: string): string => {
|
||||
const prefix = /^\(@custom\s+"/u.exec(form);
|
||||
if (prefix === null) throw new Error("Unsupported WABT custom-section name.");
|
||||
const start = prefix[0].length;
|
||||
/** Decode a producer WAT string to frame objdump's raw display text. */
|
||||
const watString = (form: string, start: number) => {
|
||||
let end = start;
|
||||
while (end < form.length && form[end] !== '"') {
|
||||
if (form[end] === "\\") end++;
|
||||
|
|
@ -45,18 +42,53 @@ const customSectionName = (form: string): string => {
|
|||
index += 2;
|
||||
}
|
||||
}
|
||||
return new TextDecoder("utf-8", { fatal: true }).decode(
|
||||
bytes.subarray(0, length),
|
||||
);
|
||||
return { bytes: bytes.subarray(0, length), end: end + 1 };
|
||||
};
|
||||
|
||||
/** Extract exact top-level WABT WAT forms; no binary decoding or import/export name unescaping. */
|
||||
/** Read only module-name metadata from WABT's preserved name-section payload. */
|
||||
const moduleNames = (bytes: Uint8Array): string[] => {
|
||||
const names: string[] = [];
|
||||
let offset = 0;
|
||||
const uint32 = (end: number): number => {
|
||||
let value = 0;
|
||||
for (let shift = 0; shift <= 28 && offset < end; shift += 7) {
|
||||
const byte = bytes[offset++];
|
||||
if (byte === undefined) break;
|
||||
if (shift === 28 && byte > 15) break;
|
||||
value += (byte & 127) * 2 ** shift;
|
||||
if (byte < 128) return value;
|
||||
}
|
||||
throw new Error("Incomplete WABT name-section metadata.");
|
||||
};
|
||||
while (offset < bytes.length) {
|
||||
const type = uint32(bytes.length);
|
||||
const size = uint32(bytes.length);
|
||||
const end = offset + size;
|
||||
if (end > bytes.length)
|
||||
throw new Error("Incomplete WABT name-section metadata.");
|
||||
if (type === 0 && size !== 0) {
|
||||
const length = uint32(end);
|
||||
if (offset + length !== end)
|
||||
throw new Error("Incomplete WABT module-name metadata.");
|
||||
names.push(
|
||||
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
||||
bytes.subarray(offset, end),
|
||||
),
|
||||
);
|
||||
}
|
||||
offset = end;
|
||||
}
|
||||
return names;
|
||||
};
|
||||
|
||||
/** Extract exact top-level WABT WAT forms; no raw WASM decoding or import/export name unescaping. */
|
||||
export const parseWabtWat = (text: string) => {
|
||||
if (!/^\(module(?:\s|\))/u.test(text))
|
||||
throw new Error("Unsupported WABT WAT module prefix.");
|
||||
const imports: string[] = [];
|
||||
const exports: string[] = [];
|
||||
const customSectionNames: string[] = [];
|
||||
const debugModuleNames: string[] = [];
|
||||
let depth = 0;
|
||||
let start = 0;
|
||||
let quoted = false;
|
||||
|
|
@ -110,8 +142,26 @@ export const parseWabtWat = (text: string) => {
|
|||
const form = text.slice(start, index + 1);
|
||||
if (/^\(import\s/u.test(form)) imports.push(form);
|
||||
else if (/^\(export\s/u.test(form)) exports.push(form);
|
||||
else if (/^\(@custom\s/u.test(form))
|
||||
customSectionNames.push(customSectionName(form));
|
||||
else if (/^\(@custom\s/u.test(form)) {
|
||||
const prefix = /^\(@custom\s+"/u.exec(form);
|
||||
if (prefix === null)
|
||||
throw new Error("Unsupported WABT custom-section name.");
|
||||
const nameString = watString(form, prefix[0].length);
|
||||
const name = new TextDecoder("utf-8", { fatal: true }).decode(
|
||||
nameString.bytes,
|
||||
);
|
||||
customSectionNames.push(name);
|
||||
if (name === "name") {
|
||||
const payloadPrefix = /^\s+"/u.exec(form.slice(nameString.end));
|
||||
if (payloadPrefix === null)
|
||||
throw new Error("Unsupported WABT name-section payload.");
|
||||
const payload = watString(
|
||||
form,
|
||||
nameString.end + payloadPrefix[0].length,
|
||||
);
|
||||
debugModuleNames.push(...moduleNames(payload.bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (--depth < 0) throw new Error("Unbalanced WABT WAT output.");
|
||||
if (depth === 0) closed = true;
|
||||
|
|
@ -120,5 +170,5 @@ export const parseWabtWat = (text: string) => {
|
|||
}
|
||||
if (depth !== 0 || quoted || commentDepth !== 0)
|
||||
throw new Error("Incomplete WABT WAT output.");
|
||||
return { imports, exports, customSectionNames };
|
||||
return { imports, exports, customSectionNames, debugModuleNames };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,6 +32,69 @@ it("retains lazily loaded library dependencies", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Captured from the real Apple bundle CI fixture's `otool -h -l` output.
|
||||
const delayedDylib = withOtoolMachHeader(`Load command 0
|
||||
cmd LC_LOAD_DYLIB
|
||||
cmdsize 56
|
||||
name @rpath/libdelay.dylib (offset 28)
|
||||
options delay-init
|
||||
current version 0.0.0
|
||||
compatibility version 1.0.0
|
||||
`);
|
||||
|
||||
it("preserves dylib-use options through native Mach-O inspection", async () => {
|
||||
const runner = new NativeFixtureRunner({ "otool:-h": delayedDylib });
|
||||
const client = new NativeMacOSProvider({}, runner, "darwin").createClient(
|
||||
nativeMachoTarget("/owned/fixture"),
|
||||
);
|
||||
const result = await client.execute("inspect_macho", {});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.result).toMatchObject({
|
||||
load_commands: {
|
||||
items: [{ kind: "LC_LOAD_DYLIB", fields: { options: "delay-init" } }],
|
||||
},
|
||||
dependencies: {
|
||||
items: [
|
||||
{
|
||||
path: "@rpath/libdelay.dylib",
|
||||
kind: "LC_LOAD_DYLIB",
|
||||
current_version: "0.0.0",
|
||||
compatibility_version: "1.0.0",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("retains empty dylib-use options and rejects missing or ambiguous encoding fields", () => {
|
||||
expect(
|
||||
parseOtoolLoadCommands(delayedDylib.replace("delay-init", "")).commands[0]
|
||||
?.fields.options,
|
||||
).toBe("");
|
||||
const malformed: Array<readonly [string, RegExp]> = [
|
||||
["", /timestamp or options/u],
|
||||
[
|
||||
" options delay-init\n options weak\n",
|
||||
/options is missing or repeated/u,
|
||||
],
|
||||
[
|
||||
" options delay-init\n time stamp 2 Thu Jan 1 00:00:02 1970\n",
|
||||
/timestamp or options/u,
|
||||
],
|
||||
[
|
||||
" options delay-init\n injected text\n",
|
||||
/ambiguous multiline text/u,
|
||||
],
|
||||
];
|
||||
for (const [replacement, reason] of malformed) {
|
||||
expect(() =>
|
||||
parseOtoolLoadCommands(
|
||||
delayedDylib.replace(" options delay-init\n", replacement),
|
||||
),
|
||||
).toThrow(reason);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves dependency install names that begin with the word stamp", () => {
|
||||
const load = parseOtoolLoadCommands(
|
||||
withOtoolMachHeader(
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ section = struct.pack(
|
|||
)
|
||||
Path('/tmp/rea-high-address.macho').write_bytes(header + segment + section + b'\xc0')
|
||||
PY
|
||||
xcrun otool -l /tmp/rea-high-address.macho
|
||||
xcrun otool -h -l /tmp/rea-high-address.macho
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
fixture:
|
||||
Mach header
|
||||
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
|
||||
0xfeedfacf 16777228 0 0x00 5 1 152 0x00000000
|
||||
Load command 0
|
||||
cmd LC_SEGMENT_64
|
||||
cmdsize 152
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
(module
|
||||
(@custom "name" "\00\01\00")
|
||||
(@custom "name" "\00\ad\01\ab\01unicode-\c3\a9 >\0a\0aSections:\0a\0aa-long-package-path/a-long-package-path/a-long-package-path/a-long-package-path/a-long-package-path/a-long-package-path/a-long-package-path/inside")
|
||||
(@custom "name" "\00\0f\0evisible\00hidden"))
|
||||
Loading…
Reference in New Issue