Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 107 additions & 3 deletions test/bundler/compile-trim-capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ async function build(without: string[]) {
}

const namesOf = (places: { name: string }[]) => [...new Set(places.map(place => place.name))].sort();

// What every executable calls, whatever --trim removed. A scan that finds neither has not read the code of the file.
const alwaysCalled = ["mmap", "write"];

function placesLeftThatCall(trimmed: Uint8Array, names: readonly string[]) {
const places = placesThatCall(trimmed, [...names, ...alwaysCalled]);
expect(namesOf(places.filter(place => alwaysCalled.includes(place.name)))).toEqual(alwaysCalled);
return places.filter(place => !alwaysCalled.includes(place.name));
}

const numbersOf = (places: { number: number }[]) =>
[...new Set(places.map(place => place.number))].sort((a, b) => a - b);

Expand All @@ -93,7 +103,7 @@ if (isASAN)
if (isLinux) expect(numbersOf(placesThatMakeSystemCalls(whole.file, startsAProgram.systemCalls))).toContain(435);

const cut = await build(startsAProgram.features);
expect(placesThatCall(cut.file, startsAProgram.functions)).toEqual([]);
expect(placesLeftThatCall(cut.file, startsAProgram.functions)).toEqual([]);
expect(placesThatMakeSystemCalls(cut.file, startsAProgram.systemCalls)).toEqual([]);
expect(await run([cut.executable], folder)).toEqual(saysHello);
});
Expand All @@ -107,7 +117,7 @@ if (isASAN)
});
const file = readFileSync(join(folder, "named"));
expect(
namesOf(placesThatCall(file, startsAProgram.functions)).filter(name => !sendsACrashReport.includes(name)),
namesOf(placesLeftThatCall(file, startsAProgram.functions)).filter(name => !sendsACrashReport.includes(name)),
).toEqual([]);
expect(placesThatMakeSystemCalls(file, startsAProgram.systemCalls)).toEqual([]);
expect(await run([join(folder, "named")], folder)).toEqual(saysHello);
Expand All @@ -125,7 +135,7 @@ if (isASAN)

const cut = await build(loadsNativeCode.features);
expect(
placesThatCall(cut.file, loadsNativeCode.functions).map(place => ({
placesLeftThatCall(cut.file, loadsNativeCode.functions).map(place => ({
...place,
keptBy: featuresThatKeep(place.address, loadsNativeCode.features),
})),
Expand All @@ -134,3 +144,97 @@ if (isASAN)
});
},
);

// A Mach-O file that imports `execve`. __TEXT is its one executable segment: __text is at 0x100000400 (up to 16 bytes),
// __stubs at 0x100000410 and __const, with the flags given, at 0x100000480 (up to 128 bytes). The sections of __DATA are
// the three kinds that have no bytes in the file.
function machO(cpu: number, text: string, stub: string, constants: string, flagsOfConstants: number): Uint8Array {
type Section = [name: string, address: number, size: number, offset: number, flags: number, indirect?: number];
const file = new Uint8Array(0x530);
const view = new DataView(file.buffer);
let at = 32;
const u32 = (...values: number[]) => values.forEach(value => (view.setUint32(at, value, true), (at += 4)));
const u64 = (...values: number[]) => values.forEach(value => (view.setBigUint64(at, BigInt(value), true), (at += 8)));
const name = (value: string) => (file.set(Buffer.from(value), at), (at += 16));
const segment = (segmentName: string, [address, size, offset, bytes, protection]: number[], sections: Section[]) => {
u32(0x19, 72 + 80 * sections.length);
name(segmentName);
u64(0x100000000 + address, size, offset, bytes);
u32(protection, protection, sections.length, 0);
for (const [sectionName, sectionAddress, sectionSize, sectionOffset, flags, indirect = 0] of sections) {
name(sectionName);
name(segmentName);
u64(0x100000000 + sectionAddress, sectionSize);
u32(sectionOffset, 0, 0, 0, flags, indirect, sectionName === "__stubs" ? sectionSize : 0, 0);
}
};
segment(
"__TEXT",
[0, 0x4000, 0, 0x500, 5],
[
["__text", 0x400, text.length / 2, 0x400, 0x80000400],
["__stubs", 0x410, stub.length / 2, 0x410, 0x80000408],
["__const", 0x480, constants.length / 2, 0x480, flagsOfConstants],
],
);
segment("__DATA_CONST", [0x4000, 0x4000, 0x500, 8, 3], [["__got", 0x4000, 8, 0x500, 6, 1]]);
segment(
"__DATA",
[0x8000, 0xc000, 0, 0, 3],
[
["__bss", 0x8000, 0x4000, 0, 1],
["__huge", 0xc000, 0x4000, 0, 12],
["__thread_bss", 0x10000, 0x4000, 0, 18],
],
);
segment("__LINKEDIT", [0x14000, 0x4000, 0x508, 0x28, 1], []);
// LC_SYMTAB: one symbol at 0x508, its name at 0x520. LC_DYSYMTAB: it is undefined, and at 0x518 the stub and the slot
// of __got name it.
u32(2, 24, 0x508, 1, 0x520, 9);
u32(11, 80, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0x518, 2, 0, 0, 0, 0);
const sizeOfCommands = at - 32;
at = 0;
u32(0xfeedfacf, cpu, cpu === 0x01000007 ? 3 : 0, 2, 6, sizeOfCommands, 0, 0);
file.set(Buffer.from(text, "hex"), 0x400);
file.set(Buffer.from(stub, "hex"), 0x410);
file.set(Buffer.from(constants, "hex"), 0x480);
at = 0x508;
u32(1, 1, 0, 0);
file.set(Buffer.from("\0_execve\0"), 0x520);
return file;
}

describe("a Mach-O file", () => {
test.each([
{
cpu: "arm64",
cpuType: 0x0100000c,
text: "04000094" + "c0035fd6", // bl 0x100000410; ret
stub: "30000090" + "100240f9" + "00021fd6", // adrp x16, 0x100004000; ldr x16, [x16]; br x16
constants: "e4ffff97" + "f1db0110", // as instructions: bl 0x100000410; adr x17, 0x100004000
call: 0x100000400,
callsInConstants: [0x100000480, 0x100000484],
},
{
cpu: "x64",
cpuType: 0x01000007,
text: "e80b000000" + "c3" + "9090", // call 0x100000410; ret; nop; nop
stub: "ff25ea3b0000", // jmp *0x100004000(%rip)
constants: "e98bffffff" + "000000", // as an instruction: jmp 0x100000410
call: 0x100000401,
callsInConstants: [0x100000481],
},
])(
"for $cpu is read as instructions only where a section says that it holds them",
({ cpuType, text, stub, constants, call, callsInConstants }) => {
const callsWith = (flagsOfConstants: number) =>
placesThatCall(machO(cpuType, text, stub, constants, flagsOfConstants), ["execve"]).map(place => place.address);
// S_ATTR_PURE_INSTRUCTIONS is 0x80000000, S_ATTR_SOME_INSTRUCTIONS 0x400.
expect({ data: callsWith(0), pure: callsWith(0x80000000), some: callsWith(0x400) }).toEqual({
data: [call],
pure: [call, ...callsInConstants],
some: [call, ...callsInConstants],
});
},
);
});
19 changes: 15 additions & 4 deletions test/bundler/what-an-executable-can-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface Place {
code: string;
}

// What is scanned as a whole: a segment of an ELF file, a section of a Mach-O file.
interface Segment {
offset: number;
end: number;
Expand All @@ -26,6 +27,9 @@ const jumpSlot = { x64: 7, arm64: 1026 };
const globalData = { x64: 6, arm64: 1025 };
const absolute = { x64: 1, arm64: 257 };
const operandOpcodes = [0x8d, 0x8b, 0xff, 0x3b, 0x39, 0x89];
// Of a Mach-O section: the attributes S_ATTR_PURE_INSTRUCTIONS and S_ATTR_SOME_INSTRUCTIONS, and the types S_ZEROFILL, S_GB_ZEROFILL and S_THREAD_LOCAL_ZEROFILL, which have no bytes in the file.
const instructions = 0x80000400;
const zeroFill = [1, 12, 18];

function signed(value: number, bits: number): number {
return value >= 2 ** (bits - 1) ? value - 2 ** bits : value;
Expand All @@ -49,11 +53,16 @@ function readMachO(file: Uint8Array): Image {
if (command === 2) [symbols, strings] = [u32(at + 8), u32(at + 16)];
if (command === 11) indirect = u32(at + 56);
if (command !== 0x19) continue;
if (u64(at + 48) > 0) image.segments.push({ offset: u64(at + 40), end: u64(at + 40) + u64(at + 48), address: u64(at + 24), isExecutable: (u32(at + 60) & 4) !== 0 });
for (let section = at + 72; section < at + 72 + u32(at + 64) * 80; section += 80) {
const type = u32(section + 64) & 0xff;
if (type === 6 || type === 7) pointers.push({ address: u64(section + 32), size: u64(section + 40), first: u32(section + 68), each: 8, isStub: false });
if (type === 8) pointers.push({ address: u64(section + 32), size: u64(section + 40), first: u32(section + 68), each: u32(section + 72), isStub: true });
const address = u64(section + 32);
const size = u64(section + 40);
const offset = u32(section + 48);
const flags = u32(section + 64);
const type = flags & 0xff;
// __TEXT is executable as a whole, and __const and __cstring are in it. Only a section says whether its bytes are instructions.
if (!zeroFill.includes(type)) image.segments.push({ offset, end: offset + size, address, isExecutable: (flags & instructions) !== 0 });
if (type === 6 || type === 7) pointers.push({ address, size, first: u32(section + 68), each: 8, isStub: false });
if (type === 8) pointers.push({ address, size, first: u32(section + 68), each: u32(section + 72), isStub: true });
}
}
for (const { address, size, first, each, isStub } of pointers) {
Expand Down Expand Up @@ -202,6 +211,8 @@ function placesIn(elf: Image, wanted: (name: string) => boolean): Place[] {
}
continue;
}
// A call can be the first instruction of a section. The loop sees a displacement only with two bytes before it.
if (offset + 5 <= end && (file[offset] === 0xe8 || file[offset] === 0xe9)) found(nameAt(base + offset + 5 + view.getInt32(offset + 1, true)), base + offset + 1);
for (let at = offset + 2; at + 4 <= end; at++) {
const word = view.getInt32(at, true);
const before = file[at - 1]!;
Expand Down
Loading