fix(packaging): harden Flatpak launcher validation

This commit is contained in:
4gray committed 2026-07-18 19:58:33 +02:00
1 parent 1a5ab50872
commit 7df496505d
5 files changed
+245 -71

No files matched your search

@@ -312,6 +312,10 @@ test('package layout verifier uses canonical helpers and direct dependencies', (
packageLayoutVerifier,
/const\s*{\s*resolveLinuxLauncherLayout\s*}\s*=\s*require\(['"]\.\/linux-launcher-layout\.cjs['"]\)/
);
assert.match(
packageLayoutVerifier,
/const\s*{\s*validateFlatpakLauncher\s*,?\s*}\s*=\s*require\(['"]\.\/flatpak-launcher-validation\.cjs['"]\)/
);
assert.match(
packageLayoutVerifier,
/function verifyLinuxLauncher\(\s*resourceDir,\s*targetNames,\s*errors\s*\)/
@@ -324,16 +328,6 @@ test('package layout verifier uses canonical helpers and direct dependencies', (
packageLayoutVerifier,
/verifyLinuxLauncher\(\s*resourceDir,\s*linuxTargetNames,\s*errors\s*\)/
);
assert.match(packageLayoutVerifier, /const elfMagic = Buffer\.alloc\(4\)/);
assert.match(
packageLayoutVerifier,
/fs\.openSync\(launcherPath,\s*['"]r['"]\)/
);
assert.match(
packageLayoutVerifier,
/fs\.readSync\(\s*descriptor,\s*elfMagic,\s*0,\s*elfMagic\.length,\s*0\s*\)/
);
assert.match(packageLayoutVerifier, /fs\.closeSync\(descriptor\)/);
const launcherVerifier = packageLayoutVerifier.match(
/function verifyLinuxLauncher\([\s\S]*?\n}\n\nfunction verifyFlatpakPermissions/
@@ -345,11 +339,7 @@ test('package layout verifier uses canonical helpers and direct dependencies', (
);
assert.match(
launcherVerifier,
/if \(!launcherLayout\.wrapperRequired\) \{[\s\S]*?return;[\s\S]*?\}\s*const launcherBinaryPath[\s\S]*?fs\.readFileSync\(launcherPath,\s*['"]utf8['"]\)/
);
assert.match(
launcherVerifier,
/fs\.existsSync\(flatpakBinarySiblingPath\)/
/if \(!launcherLayout\.wrapperRequired\) \{\s*errors\.push\(\s*\.\.\.validateFlatpakLauncher\(\s*appDir,\s*linuxExecutableName\s*\)\s*\);\s*return;\s*\}\s*const launcherBinaryPath[\s\S]*?fs\.readFileSync\(launcherPath,\s*['"]utf8['"]\)/
);
});
@@ -0,0 +1,125 @@
'use strict';
const fs = require('fs');
const path = require('path');
const expectedElfMagic = Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
const executableModeBits = 0o111;
function fsErrorCode(error) {
return error && typeof error === 'object' && typeof error.code === 'string'
? error.code
: 'UNKNOWN';
}
function validateBinarySibling(siblingPath, errors) {
try {
fs.lstatSync(siblingPath);
} catch (error) {
if (fsErrorCode(error) === 'ENOENT') {
return;
}
errors.push(
`Unable to inspect Flatpak Electron launcher binary sibling at ${siblingPath} (${fsErrorCode(error)}).`
);
return;
}
errors.push(
`Flatpak Electron layout must not include a launcher binary sibling: ${siblingPath}`
);
}
function validateFlatpakLauncher(appDir, executableName) {
const errors = [];
const launcherPath = path.join(appDir, executableName);
const flatpakBinarySiblingPath = `${launcherPath}.bin`;
validateBinarySibling(flatpakBinarySiblingPath, errors);
let descriptor;
try {
descriptor = fs.openSync(
launcherPath,
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW
);
} catch (error) {
const errorCode = fsErrorCode(error);
if (errorCode === 'ENOENT') {
errors.push(
`Missing Flatpak Electron ELF in ${appDir}: ${path.basename(launcherPath)}`
);
} else if (errorCode === 'ELOOP') {
errors.push(
`Flatpak Electron launcher must be a regular file: ${launcherPath}`
);
} else {
errors.push(
`Unable to open Flatpak Electron launcher at ${launcherPath} (${errorCode}).`
);
}
return errors;
}
try {
let launcherStat;
try {
launcherStat = fs.fstatSync(descriptor);
} catch (error) {
errors.push(
`Unable to stat Flatpak Electron launcher at ${launcherPath} (${fsErrorCode(error)}).`
);
return errors;
}
if (!launcherStat.isFile()) {
errors.push(
`Flatpak Electron launcher must be a regular file: ${launcherPath}`
);
return errors;
}
if ((launcherStat.mode & executableModeBits) === 0) {
errors.push(
`Flatpak Electron launcher must be executable: ${launcherPath}`
);
}
const elfMagic = Buffer.alloc(expectedElfMagic.length);
let bytesRead;
try {
bytesRead = fs.readSync(
descriptor,
elfMagic,
0,
elfMagic.length,
0
);
} catch (error) {
errors.push(
`Unable to read Flatpak Electron launcher at ${launcherPath} (${fsErrorCode(error)}).`
);
return errors;
}
if (
bytesRead !== expectedElfMagic.length ||
!elfMagic.equals(expectedElfMagic)
) {
errors.push(
`Flatpak Electron launcher must be an ELF binary: ${launcherPath}`
);
}
} finally {
try {
fs.closeSync(descriptor);
} catch (error) {
errors.push(
`Unable to close Flatpak Electron launcher at ${launcherPath} (${fsErrorCode(error)}).`
);
}
}
return errors;
}
module.exports = {
validateFlatpakLauncher,
};
@@ -0,0 +1,108 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const {
validateFlatpakLauncher,
} = require('./flatpak-launcher-validation.cjs');
const executableName = 'iptvnator';
const electronElf = Buffer.from([
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00,
]);
async function createFixture(t) {
const appDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'iptvnator-flatpak-launcher-')
);
t.after(() => fs.rm(appDir, { recursive: true, force: true }));
return {
appDir,
launcherPath: path.join(appDir, executableName),
siblingPath: path.join(appDir, `${executableName}.bin`),
};
}
async function writeLauncher(launcherPath, contents, mode) {
await fs.writeFile(launcherPath, contents);
await fs.chmod(launcherPath, mode);
}
test('accepts a regular executable Flatpak Electron ELF', async (t) => {
const fixture = await createFixture(t);
await writeLauncher(fixture.launcherPath, electronElf, 0o755);
assert.deepEqual(
validateFlatpakLauncher(fixture.appDir, executableName),
[]
);
});
test('rejects a symlinked Flatpak Electron launcher', async (t) => {
const fixture = await createFixture(t);
const electronTargetPath = path.join(fixture.appDir, 'electron-target');
await writeLauncher(electronTargetPath, electronElf, 0o755);
await fs.symlink(electronTargetPath, fixture.launcherPath);
assert.deepEqual(validateFlatpakLauncher(fixture.appDir, executableName), [
`Flatpak Electron launcher must be a regular file: ${fixture.launcherPath}`,
]);
});
test('rejects a dangling Flatpak launcher binary sibling', async (t) => {
const fixture = await createFixture(t);
await writeLauncher(fixture.launcherPath, electronElf, 0o755);
await fs.symlink('missing-electron', fixture.siblingPath);
assert.deepEqual(validateFlatpakLauncher(fixture.appDir, executableName), [
`Flatpak Electron layout must not include a launcher binary sibling: ${fixture.siblingPath}`,
]);
});
test('rejects a non-executable regular Flatpak Electron ELF', async (t) => {
const fixture = await createFixture(t);
await writeLauncher(fixture.launcherPath, electronElf, 0o644);
assert.deepEqual(validateFlatpakLauncher(fixture.appDir, executableName), [
`Flatpak Electron launcher must be executable: ${fixture.launcherPath}`,
]);
});
for (const [description, contents] of [
['short ELF magic', Buffer.from([0x7f, 0x45, 0x4c])],
['incorrect ELF magic', Buffer.from([0x00, 0x45, 0x4c, 0x46])],
]) {
test(`rejects ${description} in the Flatpak Electron launcher`, async (t) => {
const fixture = await createFixture(t);
await writeLauncher(fixture.launcherPath, contents, 0o755);
assert.deepEqual(
validateFlatpakLauncher(fixture.appDir, executableName),
[
`Flatpak Electron launcher must be an ELF binary: ${fixture.launcherPath}`,
]
);
});
}
test('packaging Nx tests register the Flatpak launcher validation suite', async () => {
const project = JSON.parse(
await fs.readFile(new URL('./project.json', import.meta.url), 'utf8')
);
const moduleFile =
'{workspaceRoot}/tools/packaging/flatpak-launcher-validation.cjs';
const testFile =
'{workspaceRoot}/tools/packaging/flatpak-launcher-validation.test.mjs';
assert.ok(project.targets.test.inputs.includes(moduleFile));
assert.ok(project.targets.test.inputs.includes(testFile));
assert.match(
project.targets.test.options.command,
/node --test .*tools\/packaging\/flatpak-launcher-validation\.test\.mjs/
);
});
+3 -1
View File
@@ -22,6 +22,8 @@
"{workspaceRoot}/tools/packaging/asar-dependency-closure.test.mjs",
"{workspaceRoot}/tools/packaging/embedded-mpv-packaging.cjs",
"{workspaceRoot}/tools/packaging/embedded-mpv-arch.test.mjs",
"{workspaceRoot}/tools/packaging/flatpak-launcher-validation.cjs",
"{workspaceRoot}/tools/packaging/flatpak-launcher-validation.test.mjs",
"{workspaceRoot}/tools/packaging/configure-linux-frame-copy-build.mjs",
"{workspaceRoot}/tools/packaging/configure-linux-frame-copy-build.test.mjs",
"{workspaceRoot}/tools/packaging/linux-after-pack.test.mjs",
@@ -50,7 +52,7 @@
"{workspaceRoot}/tools/embedded-mpv/stage-windows-runtime-archive.mjs"
],
"options": {
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-after-pack.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs tools/packaging/publish-snap-workflow.test.mjs tools/packaging/release-snap-assets.test.mjs tools/packaging/verify-linux-frame-copy-runtime.test.mjs tools/embedded-mpv/build-linux-runtime.test.mjs tools/embedded-mpv/generate-linux-runtime-notices.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs",
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/flatpak-launcher-validation.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-after-pack.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs tools/packaging/publish-snap-workflow.test.mjs tools/packaging/release-snap-assets.test.mjs tools/packaging/verify-linux-frame-copy-runtime.test.mjs tools/embedded-mpv/build-linux-runtime.test.mjs tools/embedded-mpv/generate-linux-runtime-notices.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs",
"cwd": "{workspaceRoot}"
}
},
@@ -19,6 +19,9 @@ const {
validateLinuxProfileTargets,
} = require('./linux-frame-copy-profile.cjs');
const { resolveLinuxLauncherLayout } = require('./linux-launcher-layout.cjs');
const {
validateFlatpakLauncher,
} = require('./flatpak-launcher-validation.cjs');
const args = process.argv.slice(2);
const normalizedArgs = args[0] === '--' ? args.slice(1) : args;
const [platform, arch = ''] = normalizedArgs;
@@ -489,61 +492,7 @@ function verifyLinuxLauncher(resourceDir, targetNames, errors) {
const launcherPath = path.join(appDir, linuxExecutableName);
if (!launcherLayout.wrapperRequired) {
if (!fileExists(launcherPath)) {
errors.push(
`Missing Flatpak Electron ELF in ${appDir}: ${path.basename(launcherPath)}`
);
return;
}
const flatpakBinarySiblingPath = `${launcherPath}.bin`;
if (fs.existsSync(flatpakBinarySiblingPath)) {
errors.push(
`Flatpak Electron layout must not include a launcher binary sibling: ${flatpakBinarySiblingPath}`
);
}
const expectedElfMagic = Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
const elfMagic = Buffer.alloc(4);
let descriptor;
try {
descriptor = fs.openSync(launcherPath, 'r');
const bytesRead = fs.readSync(
descriptor,
elfMagic,
0,
elfMagic.length,
0
);
if (
bytesRead !== expectedElfMagic.length ||
!elfMagic.equals(expectedElfMagic)
) {
errors.push(
`Flatpak Electron launcher must be an ELF binary: ${launcherPath}`
);
}
} catch (error) {
errors.push(
`Unable to inspect Flatpak Electron ELF at ${launcherPath}: ${
error instanceof Error ? error.message : String(error)
}`
);
} finally {
if (descriptor !== undefined) {
try {
fs.closeSync(descriptor);
} catch (error) {
errors.push(
`Unable to close Flatpak Electron ELF at ${launcherPath}: ${
error instanceof Error
? error.message
: String(error)
}`
);
}
}
}
errors.push(...validateFlatpakLauncher(appDir, linuxExecutableName));
return;
}