Skip to content

Commit 4e07e4a

Browse files
committed
fix(java): positively detect musl native hosts
Parse the running Node executable's ELF interpreter and require the architecture-specific musl loader before packaging a musl classifier. Reject glibc, static, malformed, and otherwise unknown ELF hosts even when Node omits its glibc runtime report. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b30bdaec-2ebd-4d51-ab1e-4de5e43772cb
1 parent 171cda9 commit 4e07e4a

2 files changed

Lines changed: 314 additions & 4 deletions

File tree

‎java/copilot-native/scripts/validate-native-host.mjs‎

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,160 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------------------------------------------*/
44

5+
import { readFileSync } from "node:fs";
6+
import { posix as path } from "node:path";
57
import { pathToFileURL } from "node:url";
68

9+
const ELF64_HEADER_SIZE = 64;
10+
const ELF64_PROGRAM_HEADER_SIZE = 56;
11+
const ELFCLASS64 = 2;
12+
const ELFDATA2LSB = 1;
13+
const EV_CURRENT = 1;
14+
const EM_X86_64 = 62;
15+
const EM_AARCH64 = 183;
16+
const PT_INTERP = 3;
17+
18+
const MUSL_ARCHITECTURES = {
19+
x64: {
20+
arch: "x64",
21+
displayArch: "x64",
22+
elfMachine: EM_X86_64,
23+
interpreter: "ld-musl-x86_64.so.1",
24+
},
25+
arm64: {
26+
arch: "arm64",
27+
displayArch: "ARM64",
28+
elfMachine: EM_AARCH64,
29+
interpreter: "ld-musl-aarch64.so.1",
30+
},
31+
};
32+
33+
const MUSL_CLASSIFIERS = {
34+
"linuxmusl-x64": MUSL_ARCHITECTURES.x64,
35+
};
36+
37+
function readSafeInteger(buffer, offset, label) {
38+
const value = buffer.readBigUInt64LE(offset);
39+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
40+
throw new Error(`${label} exceeds the supported size`);
41+
}
42+
return Number(value);
43+
}
44+
45+
export function readElfInterpreter(buffer) {
46+
if (buffer.length < ELF64_HEADER_SIZE) {
47+
throw new Error("Node executable is too small to contain an ELF64 header");
48+
}
49+
if (
50+
buffer[0] !== 0x7f ||
51+
buffer[1] !== 0x45 ||
52+
buffer[2] !== 0x4c ||
53+
buffer[3] !== 0x46
54+
) {
55+
throw new Error("Node executable is not an ELF file");
56+
}
57+
if (buffer[4] !== ELFCLASS64 || buffer[5] !== ELFDATA2LSB) {
58+
throw new Error("Node executable is not a little-endian ELF64 file");
59+
}
60+
if (buffer[6] !== EV_CURRENT || buffer.readUInt32LE(20) !== EV_CURRENT) {
61+
throw new Error("Node executable uses an unsupported ELF version");
62+
}
63+
64+
const machine = buffer.readUInt16LE(18);
65+
const programHeaderOffset = readSafeInteger(
66+
buffer,
67+
32,
68+
"ELF program header offset",
69+
);
70+
const programHeaderEntrySize = buffer.readUInt16LE(54);
71+
const programHeaderCount = buffer.readUInt16LE(56);
72+
73+
if (programHeaderCount === 0xffff) {
74+
throw new Error("Extended ELF program header counts are unsupported");
75+
}
76+
if (
77+
programHeaderCount > 0 &&
78+
programHeaderEntrySize < ELF64_PROGRAM_HEADER_SIZE
79+
) {
80+
throw new Error("ELF program header entries are too small");
81+
}
82+
83+
const programHeaderEnd =
84+
BigInt(programHeaderOffset) +
85+
BigInt(programHeaderEntrySize) * BigInt(programHeaderCount);
86+
if (programHeaderEnd > BigInt(buffer.length)) {
87+
throw new Error("ELF program header table extends beyond the executable");
88+
}
89+
90+
let interpreter;
91+
for (let index = 0; index < programHeaderCount; index += 1) {
92+
const entryOffset = programHeaderOffset + index * programHeaderEntrySize;
93+
if (buffer.readUInt32LE(entryOffset) !== PT_INTERP) {
94+
continue;
95+
}
96+
if (interpreter !== undefined) {
97+
throw new Error("Node executable contains multiple ELF interpreters");
98+
}
99+
100+
const interpreterOffset = readSafeInteger(
101+
buffer,
102+
entryOffset + 8,
103+
"ELF interpreter offset",
104+
);
105+
const interpreterSize = readSafeInteger(
106+
buffer,
107+
entryOffset + 32,
108+
"ELF interpreter size",
109+
);
110+
const interpreterEnd = BigInt(interpreterOffset) + BigInt(interpreterSize);
111+
if (interpreterSize < 2 || interpreterEnd > BigInt(buffer.length)) {
112+
throw new Error("ELF interpreter extends beyond the executable");
113+
}
114+
115+
const bytes = buffer.subarray(
116+
interpreterOffset,
117+
interpreterOffset + interpreterSize,
118+
);
119+
if (
120+
bytes[bytes.length - 1] !== 0 ||
121+
bytes.subarray(0, bytes.length - 1).includes(0)
122+
) {
123+
throw new Error("ELF interpreter is not a valid null-terminated path");
124+
}
125+
126+
interpreter = bytes.subarray(0, bytes.length - 1).toString("utf8");
127+
if (!interpreter.startsWith("/")) {
128+
throw new Error("ELF interpreter path is not absolute");
129+
}
130+
}
131+
132+
return { machine, interpreter };
133+
}
134+
7135
export function validateNativeHost(classifier, host) {
8-
if (classifier === "linuxmusl-x64") {
9-
if (host.platform !== "linux" || host.arch !== "x64") {
136+
const muslHost = MUSL_CLASSIFIERS[classifier];
137+
if (muslHost) {
138+
if (host.platform !== "linux" || host.arch !== muslHost.arch) {
10139
throw new Error(
11-
`Native ${classifier} packaging requires Linux x64; detected ${host.platform}-${host.arch}`,
140+
`Native ${classifier} packaging requires Linux ${muslHost.displayArch}; detected ${host.platform}-${host.arch}`,
12141
);
13142
}
14143
if (host.glibcVersionRuntime) {
15144
throw new Error(
16145
`Native ${classifier} packaging requires musl; detected glibc ${host.glibcVersionRuntime}`,
17146
);
18147
}
148+
if (
149+
host.elfMachine !== muslHost.elfMachine ||
150+
path.basename(host.elfInterpreter ?? "") !== muslHost.interpreter
151+
) {
152+
const detected = host.elfDetectionError
153+
? `unable to inspect the Node executable: ${host.elfDetectionError}`
154+
: `${host.elfInterpreter ?? "no dynamic ELF interpreter"} (ELF machine ${host.elfMachine ?? "unknown"})`;
155+
throw new Error(
156+
`Native ${classifier} packaging requires musl; detected ${detected}`,
157+
);
158+
}
19159
return `Validated native build host: ${classifier} (musl)`;
20160
}
21161

@@ -62,10 +202,23 @@ export function validateNativeHost(classifier, host) {
62202

63203
export function detectNativeHost() {
64204
const report = process.report?.getReport();
205+
let elf;
206+
let elfDetectionError;
207+
if (process.platform === "linux") {
208+
try {
209+
elf = readElfInterpreter(readFileSync("/proc/self/exe"));
210+
} catch (error) {
211+
elfDetectionError =
212+
error instanceof Error ? error.message : String(error);
213+
}
214+
}
65215
return {
66216
platform: process.platform,
67217
arch: process.arch,
68218
glibcVersionRuntime: report?.header?.glibcVersionRuntime,
219+
elfMachine: elf?.machine,
220+
elfInterpreter: elf?.interpreter,
221+
elfDetectionError,
69222
};
70223
}
71224

‎java/copilot-native/scripts/validate-native-host.test.mjs‎

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,103 @@
55
import assert from "node:assert/strict";
66
import test from "node:test";
77

8-
import { validateNativeHost } from "./validate-native-host.mjs";
8+
import {
9+
readElfInterpreter,
10+
validateNativeHost,
11+
} from "./validate-native-host.mjs";
12+
13+
function createElf64({ machine = 62, interpreter } = {}) {
14+
const interpreterBytes =
15+
interpreter === undefined
16+
? undefined
17+
: Buffer.from(`${interpreter}\0`, "utf8");
18+
const programHeaderCount = interpreterBytes === undefined ? 0 : 1;
19+
const interpreterOffset = ELF64_HEADER_SIZE + ELF64_PROGRAM_HEADER_SIZE;
20+
const buffer = Buffer.alloc(
21+
interpreterOffset + (interpreterBytes?.length ?? 0),
22+
);
23+
24+
buffer.set([0x7f, 0x45, 0x4c, 0x46], 0);
25+
buffer[4] = 2;
26+
buffer[5] = 1;
27+
buffer[6] = 1;
28+
buffer.writeUInt16LE(machine, 18);
29+
buffer.writeUInt32LE(1, 20);
30+
buffer.writeBigUInt64LE(
31+
BigInt(programHeaderCount === 0 ? 0 : ELF64_HEADER_SIZE),
32+
32,
33+
);
34+
buffer.writeUInt16LE(ELF64_HEADER_SIZE, 52);
35+
buffer.writeUInt16LE(ELF64_PROGRAM_HEADER_SIZE, 54);
36+
buffer.writeUInt16LE(programHeaderCount, 56);
37+
38+
if (interpreterBytes !== undefined) {
39+
buffer.writeUInt32LE(3, ELF64_HEADER_SIZE);
40+
buffer.writeBigUInt64LE(BigInt(interpreterOffset), ELF64_HEADER_SIZE + 8);
41+
buffer.writeBigUInt64LE(
42+
BigInt(interpreterBytes.length),
43+
ELF64_HEADER_SIZE + 32,
44+
);
45+
interpreterBytes.copy(buffer, interpreterOffset);
46+
}
47+
48+
return buffer;
49+
}
50+
51+
const ELF64_HEADER_SIZE = 64;
52+
const ELF64_PROGRAM_HEADER_SIZE = 56;
53+
54+
test("reads the x64 musl interpreter from an ELF executable", () => {
55+
assert.deepEqual(
56+
readElfInterpreter(
57+
createElf64({
58+
interpreter: "/lib/ld-musl-x86_64.so.1",
59+
}),
60+
),
61+
{
62+
machine: 62,
63+
interpreter: "/lib/ld-musl-x86_64.so.1",
64+
},
65+
);
66+
});
67+
68+
test("reads the ARM64 musl interpreter from an ELF executable", () => {
69+
assert.deepEqual(
70+
readElfInterpreter(
71+
createElf64({
72+
machine: 183,
73+
interpreter: "/lib/ld-musl-aarch64.so.1",
74+
}),
75+
),
76+
{
77+
machine: 183,
78+
interpreter: "/lib/ld-musl-aarch64.so.1",
79+
},
80+
);
81+
});
82+
83+
test("reports a static ELF executable without an interpreter", () => {
84+
assert.deepEqual(readElfInterpreter(createElf64()), {
85+
machine: 62,
86+
interpreter: undefined,
87+
});
88+
});
89+
90+
test("rejects a truncated ELF executable", () => {
91+
assert.throws(
92+
() => readElfInterpreter(Buffer.from([0x7f, 0x45, 0x4c, 0x46])),
93+
/too small/,
94+
);
95+
});
96+
97+
test("rejects an ELF interpreter outside the executable", () => {
98+
const buffer = createElf64({
99+
interpreter: "/lib/ld-musl-x86_64.so.1",
100+
});
101+
buffer.writeBigUInt64LE(BigInt(buffer.length + 1), ELF64_HEADER_SIZE + 8);
102+
103+
assert.throws(() => readElfInterpreter(buffer), /interpreter extends beyond/);
104+
});
9105

10106
test("accepts Linux x64 with glibc", () => {
11107
assert.equal(
@@ -35,6 +131,8 @@ test("accepts Linux musl x64", () => {
35131
platform: "linux",
36132
arch: "x64",
37133
glibcVersionRuntime: undefined,
134+
elfMachine: 62,
135+
elfInterpreter: "/lib/ld-musl-x86_64.so.1",
38136
}),
39137
"Validated native build host: linuxmusl-x64 (musl)",
40138
);
@@ -115,11 +213,68 @@ test("rejects Linux musl x64 with glibc", () => {
115213
platform: "linux",
116214
arch: "x64",
117215
glibcVersionRuntime: "2.39",
216+
elfMachine: 62,
217+
elfInterpreter: "/lib64/ld-linux-x86-64.so.2",
118218
}),
119219
/requires musl/,
120220
);
121221
});
122222

223+
test("rejects Linux musl x64 when the glibc report is unavailable", () => {
224+
assert.throws(
225+
() =>
226+
validateNativeHost("linuxmusl-x64", {
227+
platform: "linux",
228+
arch: "x64",
229+
glibcVersionRuntime: undefined,
230+
elfMachine: 62,
231+
elfInterpreter: "/lib64/ld-linux-x86-64.so.2",
232+
}),
233+
/ld-linux-x86-64/,
234+
);
235+
});
236+
237+
test("rejects Linux musl x64 with unknown libc", () => {
238+
assert.throws(
239+
() =>
240+
validateNativeHost("linuxmusl-x64", {
241+
platform: "linux",
242+
arch: "x64",
243+
glibcVersionRuntime: undefined,
244+
elfMachine: 62,
245+
elfInterpreter: undefined,
246+
}),
247+
/no dynamic ELF interpreter/,
248+
);
249+
});
250+
251+
test("rejects Linux musl x64 with the ARM64 musl interpreter", () => {
252+
assert.throws(
253+
() =>
254+
validateNativeHost("linuxmusl-x64", {
255+
platform: "linux",
256+
arch: "x64",
257+
glibcVersionRuntime: undefined,
258+
elfMachine: 183,
259+
elfInterpreter: "/lib/ld-musl-aarch64.so.1",
260+
}),
261+
/ld-musl-aarch64/,
262+
);
263+
});
264+
265+
test("rejects Linux musl x64 when ELF detection fails", () => {
266+
assert.throws(
267+
() =>
268+
validateNativeHost("linuxmusl-x64", {
269+
platform: "linux",
270+
arch: "x64",
271+
glibcVersionRuntime: undefined,
272+
elfDetectionError: "permission denied",
273+
}),
274+
/unable to inspect the Node executable: permission denied/,
275+
);
276+
});
277+
123278
test("rejects a non-x64 host for Linux musl x64", () => {
124279
assert.throws(
125280
() =>
@@ -247,6 +402,8 @@ test("rejects an unimplemented classifier", () => {
247402
platform: "linux",
248403
arch: "arm64",
249404
glibcVersionRuntime: undefined,
405+
elfMachine: 183,
406+
elfInterpreter: "/lib/ld-musl-aarch64.so.1",
250407
}),
251408
/Unsupported native build classifier/,
252409
);

0 commit comments

Comments
 (0)