Skip to content

Commit f209d80

Browse files
sunnylqmclaude
andcommitted
ci: fail publish when 64-bit .so LOAD segments are not 16KB-aligned
npm 10.45.0-10.48.0 shipped 4KB-aligned arm64-v8a/x86_64 libraries (runner images preset ANDROID_NDK_HOME to a bundled old NDK, bypassing the r28 pin), and Google Play rejects them. verify-android-so.js now asserts p_align >= 16384 on 64-bit ABIs so the publish pipeline fails loudly on regression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a4575c5 commit f209d80

1 file changed

Lines changed: 55 additions & 1 deletion

File tree

scripts/verify-android-so.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,39 @@ const REQUIRED_SYMBOLS = [
4141

4242
const SHT_DYNSYM = 11;
4343
const SHN_UNDEF = 0;
44+
const PT_LOAD = 1;
45+
46+
// Google Play requires 16 KB page-size support for 64-bit ABIs: every ELF
47+
// LOAD segment must be aligned to at least 16384 bytes. v10.45.0–v10.48.0
48+
// shipped 4 KB-aligned libraries because the publish CI resolved an old NDK,
49+
// and nothing caught it — this check makes that failure loud.
50+
const PAGE_ALIGN_ABIS = { 'arm64-v8a': 16384, x86_64: 16384 };
51+
52+
/** Returns the smallest p_align among the ELF's LOAD segments. */
53+
function readMinLoadAlignment(buffer) {
54+
const is64 = buffer[4] === 2;
55+
const phoff = is64
56+
? Number(buffer.readBigUInt64LE(0x20))
57+
: buffer.readUInt32LE(0x1c);
58+
const phentsize = buffer.readUInt16LE(is64 ? 0x36 : 0x2a);
59+
const phnum = buffer.readUInt16LE(is64 ? 0x38 : 0x2c);
60+
61+
let min = Infinity;
62+
for (let i = 0; i < phnum; i++) {
63+
const base = phoff + i * phentsize;
64+
if (buffer.readUInt32LE(base) !== PT_LOAD) {
65+
continue;
66+
}
67+
const align = is64
68+
? Number(buffer.readBigUInt64LE(base + 0x30))
69+
: buffer.readUInt32LE(base + 0x1c);
70+
min = Math.min(min, align);
71+
}
72+
if (min === Infinity) {
73+
throw new Error('no LOAD segments');
74+
}
75+
return min;
76+
}
4477

4578
/** Returns the set of defined dynamic symbol names exported by an ELF file. */
4679
function readDynamicSymbols(buffer, expectedMachine) {
@@ -136,15 +169,36 @@ for (const abi of ABIS) {
136169
continue;
137170
}
138171

172+
const buffer = fs.readFileSync(soPath);
139173
let symbols;
140174
try {
141-
symbols = readDynamicSymbols(fs.readFileSync(soPath), ABI_MACHINE[abi]);
175+
symbols = readDynamicSymbols(buffer, ABI_MACHINE[abi]);
142176
} catch (error) {
143177
console.error(`error: cannot read symbols from ${soPath}: ${error.message}`);
144178
failed = true;
145179
continue;
146180
}
147181

182+
const requiredAlign = PAGE_ALIGN_ABIS[abi];
183+
if (requiredAlign) {
184+
try {
185+
const align = readMinLoadAlignment(buffer);
186+
if (align < requiredAlign) {
187+
console.error(
188+
`error: ${soPath} LOAD segments aligned to ${align} bytes; Google Play ` +
189+
`requires ${requiredAlign} (16 KB page size) for ${abi}. Rebuild with ` +
190+
`NDK r28+ ('npm run build:so').`,
191+
);
192+
failed = true;
193+
}
194+
} catch (error) {
195+
console.error(
196+
`error: cannot read LOAD alignment from ${soPath}: ${error.message}`,
197+
);
198+
failed = true;
199+
}
200+
}
201+
148202
const missing = REQUIRED_SYMBOLS.filter(symbol => !symbols.has(symbol));
149203
if (missing.length) {
150204
for (const symbol of missing) {

0 commit comments

Comments
 (0)