test(packaging): harden Linux package probes

This commit is contained in:
4gray committed 2026-07-17 22:42:37 +02:00
1 parent f8794dd0a0
commit cc18cae320
2 files changed
+380 -2

No files matched your search

@@ -28,6 +28,7 @@ function defaultRunCommand(command, args, options = {}) {
encoding: 'utf8',
stdio: 'pipe',
timeout: options.timeout,
killSignal: options.killSignal,
windowsHide: true,
});
}
@@ -323,6 +324,188 @@ export function findExtractedResourceDir(extractionRoot) {
return candidates[0];
}
function yamlMappingEntries(lines, key, indent, start = 0, end = lines.length) {
const prefix = `${' '.repeat(indent)}${key}:`;
return lines
.map((line, index) => ({ index, line }))
.filter(
({ index, line }) =>
index >= start &&
index < end &&
line.startsWith(prefix) &&
line.slice(prefix.length).match(/^(?:\s|$)/) &&
line.length - line.trimStart().length === indent
)
.map(({ index, line }) => {
let blockEnd = end;
for (
let candidateIndex = index + 1;
candidateIndex < end;
candidateIndex += 1
) {
const candidate = lines[candidateIndex];
if (!candidate.trim() || candidate.trimStart().startsWith('#')) {
continue;
}
const candidateIndent =
candidate.length - candidate.trimStart().length;
if (candidateIndent <= indent) {
blockEnd = candidateIndex;
break;
}
}
return {
index,
end: blockEnd,
value: line.slice(prefix.length).trim(),
};
});
}
function singleYamlMappingEntry(lines, key, indent, start, end) {
const entries = yamlMappingEntries(lines, key, indent, start, end);
return entries.length === 1 ? entries[0] : null;
}
function yamlScalarEquals(value, expected) {
return (
value === expected ||
value === JSON.stringify(expected) ||
value === `'${expected.replaceAll("'", "''")}'`
);
}
function yamlSequenceIncludes(lines, entry, expected) {
if (entry.value) {
if (!entry.value.startsWith('[') || !entry.value.endsWith(']')) {
return false;
}
return entry.value
.slice(1, -1)
.split(',')
.map((value) => value.trim())
.some((value) => yamlScalarEquals(value, expected));
}
return lines
.slice(entry.index + 1, entry.end)
.some((line) =>
yamlScalarEquals(line.trim().replace(/^-\s*/, ''), expected)
);
}
export function validateExtractedSnapMetadata(extractionRoot) {
const snapYamlPath = path.join(extractionRoot, 'meta', 'snap.yaml');
let stat;
try {
stat = fs.lstatSync(snapYamlPath);
} catch {
return [`Missing extracted Snap metadata: ${snapYamlPath}`];
}
if (!stat.isFile() || stat.isSymbolicLink()) {
return [`Extracted Snap metadata must be a regular file: ${snapYamlPath}`];
}
let contents;
try {
contents = fs.readFileSync(snapYamlPath, 'utf8');
} catch (error) {
return [
`Unable to read extracted Snap metadata at ${snapYamlPath}: ${
error instanceof Error ? error.message : String(error)
}`,
];
}
if (contents.includes('\t')) {
return [
`Extracted Snap metadata must use space indentation: ${snapYamlPath}`,
];
}
const lines = contents.split(/\r?\n/);
const errors = [];
const plugs = singleYamlMappingEntry(
lines,
'plugs',
0,
0,
lines.length
);
const sharedMemory =
plugs &&
singleYamlMappingEntry(
lines,
'shared-memory',
2,
plugs.index + 1,
plugs.end
);
if (!plugs || plugs.value || !sharedMemory || sharedMemory.value) {
errors.push(
'Extracted Snap metadata must declare exactly one top-level shared-memory plug.'
);
} else {
const interfaceEntry = singleYamlMappingEntry(
lines,
'interface',
4,
sharedMemory.index + 1,
sharedMemory.end
);
const privateEntry = singleYamlMappingEntry(
lines,
'private',
4,
sharedMemory.index + 1,
sharedMemory.end
);
if (
!interfaceEntry ||
!yamlScalarEquals(interfaceEntry.value, 'shared-memory')
) {
errors.push(
'Extracted Snap top-level shared-memory plug must declare interface: shared-memory.'
);
}
if (!privateEntry || privateEntry.value !== 'true') {
errors.push(
'Extracted Snap top-level shared-memory plug must declare private: true.'
);
}
}
const apps = singleYamlMappingEntry(lines, 'apps', 0, 0, lines.length);
const app =
apps &&
singleYamlMappingEntry(
lines,
'iptvnator',
2,
apps.index + 1,
apps.end
);
const appPlugs =
app &&
singleYamlMappingEntry(
lines,
'plugs',
4,
app.index + 1,
app.end
);
if (
!apps ||
apps.value ||
!app ||
app.value ||
!appPlugs ||
!yamlSequenceIncludes(lines, appPlugs, 'shared-memory')
) {
errors.push(
'Extracted Snap iptvnator app must use the shared-memory plug.'
);
}
return errors;
}
export function readElfArchitecture(binaryPath) {
const descriptor = fs.openSync(binaryPath, 'r');
try {
@@ -792,6 +975,7 @@ export function verifyExtractedLinuxFrameCopyRuntime({
{
encoding: 'utf8',
env: probeEnvironment,
killSignal: 'SIGKILL',
timeout: RUNTIME_PROBE_TIMEOUT_MS,
windowsHide: true,
}
@@ -837,6 +1021,18 @@ export function verifyLinuxFrameCopyArtifact({
destination: extractionDestination,
runCommand,
});
if (format === 'snap') {
const snapMetadataErrors =
validateExtractedSnapMetadata(extractionRoot);
if (snapMetadataErrors.length > 0) {
throw new Error(
[
`Linux frame-copy package verification failed for ${resolvedArtifactPath}:`,
...snapMetadataErrors.map((error) => `- ${error}`),
].join('\n')
);
}
}
const resourceDir = findExtractedResourceDir(extractionRoot);
const metadata = metadataReader({
artifactPath: resolvedArtifactPath,
@@ -14,6 +14,7 @@ import {
readLinuxArtifactMetadata,
readElfArchitecture,
validateSystemPackageDependencies,
validateExtractedSnapMetadata,
verifyExtractedLinuxFrameCopyRuntime,
verifyLinuxFrameCopyArtifact,
} from './verify-linux-frame-copy-runtime.mjs';
@@ -513,8 +514,189 @@ test('validates an x64 system payload and executes one bounded helper probe', ()
path.join(fixture.nativeDir, 'iptvnator_mpv_helper')
);
assert.deepEqual(probeCalls[0].args, ['--runtime-probe']);
assert.equal(probeCalls[0].options.timeout, 3000);
assert.deepEqual(probeCalls[0].options.env, { PATH: '/usr/bin' });
assert.deepEqual(probeCalls[0].options, {
encoding: 'utf8',
env: { PATH: '/usr/bin' },
killSignal: 'SIGKILL',
timeout: 3000,
windowsHide: true,
});
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
test('rejects helper probes terminated by a signal or hard timeout', () => {
for (const probeResult of [
{
status: null,
signal: 'SIGKILL',
stdout: '',
stderr: '',
},
{
error: Object.assign(new Error('spawnSync helper ETIMEDOUT'), {
code: 'ETIMEDOUT',
}),
status: null,
signal: 'SIGKILL',
stdout: '',
stderr: '',
},
]) {
const fixture = createSystemPayload();
try {
const errors = verifyExtractedLinuxFrameCopyRuntime({
resourceDir: fixture.resourceDir,
artifactFormat: 'deb',
profileName: 'system',
packageDependencies: ['libmpv2'],
elfInspector: validElfInspector,
probeRunner() {
return probeResult;
},
});
assert.match(
errors.join('\n'),
/(?:runtime probe terminated by signal SIGKILL|Unable to execute .*ETIMEDOUT)/
);
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
}
});
test('requires a private top-level shared-memory plug used by the Snap app', () => {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-verifier-snap-metadata-')
);
const snapYamlPath = path.join(root, 'meta', 'snap.yaml');
fs.mkdirSync(path.dirname(snapYamlPath), { recursive: true });
const validSnapYaml = [
'name: iptvnator',
'apps:',
' iptvnator:',
' command: iptvnator',
' plugs:',
' - desktop',
' - shared-memory',
'plugs:',
' shared-memory:',
' interface: shared-memory',
' private: true',
'',
].join('\n');
try {
fs.writeFileSync(snapYamlPath, validSnapYaml);
assert.deepEqual(validateExtractedSnapMetadata(root), []);
for (const [mutate, expected] of [
[
(contents) =>
contents.replace(' private: true', ' private: false'),
/private: true/,
],
[
(contents) =>
contents.replace(' - shared-memory\n', ''),
/app.*shared-memory plug/i,
],
[
(contents) =>
contents.replace(
' shared-memory:\n interface: shared-memory\n private: true\n',
''
),
/top-level shared-memory plug/i,
],
]) {
fs.writeFileSync(snapYamlPath, mutate(validSnapYaml));
assert.match(
validateExtractedSnapMetadata(root).join('\n'),
expected
);
}
fs.rmSync(snapYamlPath);
assert.match(
validateExtractedSnapMetadata(root).join('\n'),
/Missing extracted Snap metadata/
);
fs.writeFileSync(path.join(root, 'outside.yaml'), validSnapYaml);
fs.symlinkSync(
path.join(root, 'outside.yaml'),
snapYamlPath,
process.platform === 'win32' ? 'file' : undefined
);
assert.match(
validateExtractedSnapMetadata(root).join('\n'),
/regular file/
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('artifact verification rejects Snap metadata before accepting its payload', () => {
const fixture = createSystemPayload();
const artifactPath = path.join(fixture.root, 'package.snap');
const snapYamlPath = path.join(fixture.root, 'meta', 'snap.yaml');
fs.writeFileSync(artifactPath, 'fixture');
fs.mkdirSync(path.dirname(snapYamlPath), { recursive: true });
fs.writeFileSync(
snapYamlPath,
[
'name: iptvnator',
'apps:',
' iptvnator:',
' command: iptvnator',
' plugs:',
' - desktop',
'plugs:',
' shared-memory:',
' interface: shared-memory',
' private: true',
'',
].join('\n')
);
let payloadVerifierCalls = 0;
const verify = () =>
verifyLinuxFrameCopyArtifact({
artifactPath,
profileName: 'portable',
extractArtifact() {
return fixture.root;
},
metadataReader() {
return { declaredArch: 'x64', dependencies: [] };
},
payloadVerifier() {
payloadVerifierCalls += 1;
return [];
},
});
try {
assert.throws(verify, /app.*shared-memory plug/i);
assert.equal(payloadVerifierCalls, 0);
fs.writeFileSync(
snapYamlPath,
fs
.readFileSync(snapYamlPath, 'utf8')
.replace(' - desktop\n', ' - shared-memory\n')
);
assert.deepEqual(verify(), {
artifactPath,
format: 'snap',
profileName: 'portable',
architecture: 'x64',
});
assert.equal(payloadVerifierCalls, 1);
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}