Skip to content

Commit 9b5f08b

Browse files
ntkmenex3
andauthored
Improve the performance of detecting linux-musl platform by parsing ELF (#315)
Co-authored-by: Natalie Weizenbaum <[email protected]>
1 parent a9f1440 commit 9b5f08b

File tree

2 files changed

+117
-6
lines changed

2 files changed

+117
-6
lines changed

lib/src/compiler-path.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,29 @@
44

55
import * as fs from 'fs';
66
import * as p from 'path';
7+
import {getElfInterpreter} from './elf';
78
import {isErrnoException} from './utils';
89

910
/**
10-
* Detect if the current running node binary is linked with musl libc by
11-
* checking if the binary contains a string like "/.../ld-musl-$ARCH.so"
11+
* Detect if the given binary is linked with musl libc by checking if
12+
* the interpreter basename starts with "ld-musl-"
1213
*/
13-
const isLinuxMusl = function () {
14-
return fs.readFileSync(process.execPath).includes('/ld-musl-');
15-
};
14+
function isLinuxMusl(path: string): boolean {
15+
try {
16+
const interpreter = getElfInterpreter(path);
17+
return p.basename(interpreter).startsWith('ld-musl-');
18+
} catch (error) {
19+
console.warn(
20+
`Warning: Failed to detect linux-musl, fallback to linux-gnu: ${error.message}`
21+
);
22+
return false;
23+
}
24+
}
1625

1726
/** The full command for the embedded compiler executable. */
1827
export const compilerCommand = (() => {
1928
const platform =
20-
process.platform === 'linux' && isLinuxMusl()
29+
process.platform === 'linux' && isLinuxMusl(process.execPath)
2130
? 'linux-musl'
2231
: (process.platform as string);
2332

lib/src/elf.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2024 Google LLC. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import * as fs from 'fs';
6+
7+
/** Read a chunk of data from a file descriptor into a new Buffer. */
8+
function readFileDescriptor(
9+
fd: number,
10+
position: number,
11+
length: number
12+
): Buffer {
13+
const buffer = Buffer.alloc(length);
14+
let offset = 0;
15+
while (offset < length) {
16+
const bytesRead = fs.readSync(fd, buffer, {
17+
offset: offset,
18+
position: position + offset,
19+
});
20+
if (bytesRead === 0) {
21+
throw new Error(`failed to read fd ${fd}`);
22+
}
23+
24+
offset += bytesRead;
25+
}
26+
return buffer;
27+
}
28+
29+
/** Parse an ELF file and return its interpreter. */
30+
export function getElfInterpreter(path: string): string {
31+
const fd = fs.openSync(path, 'r');
32+
try {
33+
const elfIdentification = new DataView(
34+
readFileDescriptor(fd, 0, 64).buffer
35+
);
36+
37+
if (
38+
elfIdentification.getUint8(0) !== 0x7f ||
39+
elfIdentification.getUint8(1) !== 0x45 ||
40+
elfIdentification.getUint8(2) !== 0x4c ||
41+
elfIdentification.getUint8(3) !== 0x46
42+
) {
43+
throw new Error(`${path} is not an ELF file.`);
44+
}
45+
46+
const elfIdentificationClass = elfIdentification.getUint8(4);
47+
if (elfIdentificationClass !== 1 && elfIdentificationClass !== 2) {
48+
throw new Error(`${path} has an invalid ELF class.`);
49+
}
50+
const elfClass32 = elfIdentificationClass === 1;
51+
52+
const elfIdentificationData = elfIdentification.getUint8(5);
53+
if (elfIdentificationData !== 1 && elfIdentificationData !== 2) {
54+
throw new Error(`${path} has an invalid endianness.`);
55+
}
56+
const littleEndian = elfIdentificationData === 1;
57+
58+
// Converting BigUint64 to Number because node Buffer length has to be
59+
// number type, and we don't expect any elf we check with this method to
60+
// be larger than 9007199254740991 bytes.
61+
const programHeadersOffset = elfClass32
62+
? elfIdentification.getUint32(28, littleEndian)
63+
: Number(elfIdentification.getBigUint64(32, littleEndian));
64+
const programHeadersEntrySize = elfClass32
65+
? elfIdentification.getUint16(42, littleEndian)
66+
: elfIdentification.getUint16(54, littleEndian);
67+
const programHeadersEntryCount = elfClass32
68+
? elfIdentification.getUint16(44, littleEndian)
69+
: elfIdentification.getUint16(56, littleEndian);
70+
71+
const programHeaders = new DataView(
72+
readFileDescriptor(
73+
fd,
74+
programHeadersOffset,
75+
programHeadersEntrySize * programHeadersEntryCount
76+
).buffer
77+
);
78+
for (let i = 0; i < programHeadersEntryCount; i++) {
79+
const byteOffset = i * programHeadersEntrySize;
80+
const segmentType = programHeaders.getUint32(byteOffset, littleEndian);
81+
if (segmentType !== 3) continue; // 3 is PT_INTERP, the interpreter
82+
83+
const segmentOffset = elfClass32
84+
? programHeaders.getUint32(byteOffset + 4, littleEndian)
85+
: Number(programHeaders.getBigUint64(byteOffset + 8, littleEndian));
86+
const segmentFileSize = elfClass32
87+
? programHeaders.getUint32(byteOffset + 16, littleEndian)
88+
: Number(programHeaders.getBigUint64(byteOffset + 32, littleEndian));
89+
90+
const buffer = readFileDescriptor(fd, segmentOffset, segmentFileSize);
91+
if (buffer[segmentFileSize - 1] !== 0) {
92+
throw new Error(`${path} is corrupted.`);
93+
}
94+
95+
return buffer.toString('utf8', 0, segmentFileSize - 1);
96+
}
97+
98+
throw new Error(`${path} does not contain an interpreter entry.`);
99+
} finally {
100+
fs.closeSync(fd);
101+
}
102+
}

0 commit comments

Comments
 (0)