fix(packaging): scope Snap Electron library checks

This commit is contained in:
4gray committed 2026-07-18 10:31:11 +02:00
1 parent 541d47ac96
commit 351f9609d4
9 files changed
+315 -51

No files matched your search

+5
View File
@@ -236,6 +236,11 @@ Key files:
Electron libraries, `embedded_mpv.node`, and
`embedded_mpv_frame_reader.node` must not load or link it. Preserve this
process-isolation contract in build, package, and smoke checks.
- Pristine afterPack/unpacked layouts scan Electron libraries recursively.
Extracted Snap payloads exclude only the package-manager `lib/**` and
`usr/lib/**` trees that Snap overlays into the same root; every other
directory remains recursive, and Electron-library symlinks still fail
closed.
- Linux frame-copy availability is fail-closed. The packaged manifest,
artifact modes, declared bundled hashes/closure, and bounded
`--runtime-probe` must all succeed before frame-copy can relax the renderer
+6 -2
View File
@@ -627,8 +627,12 @@ engine` (restart required) or
`<canvas data-embedded-mpv-frame>`. Shared `app-player-controls` owns the DOM
UI; native-view retains the legacy dock. On Linux, only
`iptvnator_mpv_helper` may link libmpv; Electron, its shipped libraries, the
addon, and frame reader must not. Official x64 packages use three separate
profiles: DEB/RPM/Pacman depend on system libmpv plus the helper's direct
addon, and frame reader must not. Pristine afterPack/unpacked layouts scan
Electron libraries recursively; extracted Snap payloads exclude only the
package-manager `lib/**` and `usr/lib/**` trees overlaid into the same root.
Every other directory remains recursive, and Electron-library symlinks still
fail closed. Official x64 packages use three separate profiles:
DEB/RPM/Pacman depend on system libmpv plus the helper's direct
EGL/OpenGL/GBM interfaces, AppImage/Snap bundle the pinned LGPL closure, and
Flatpak bundles the same closure. Exact system dependencies are
DEB=`libmpv2,libegl1,libopengl0,libgbm1`,
+8
View File
@@ -106,6 +106,14 @@ native directory also contains hash-validated `embedded-mpv-notices.json`,
marker-only packages intentionally contain neither a private `native/lib`
directory nor the bundled-runtime legal payload.
The pristine `afterPack` and unpacked-layout checks recursively inspect
Electron-owned shared libraries. An extracted Snap has already overlaid its
package-manager `lib/**` and `usr/lib/**` runtime trees onto that same payload
root, so the post-target verifier excludes exactly those two target-provided
trees while continuing to scan every other directory recursively. Electron
libraries are still required to be regular files and free of libmpv linkage;
Snap runtime symlinks are outside that ownership boundary.
ARM Linux packages remain marker-only. They never borrow x64 native artifacts,
even when build environment variables claim a matching staged architecture.
Consequently frame-copy is not advertised there, and the normal inline/external
@@ -117,7 +117,11 @@ they may not retain build-prefix paths.
`embedded_mpv.node`, the Electron executable (`iptvnator.bin`), and Electron's
shipped libraries must not have a direct `DT_NEEDED` entry for libmpv.
`iptvnator_mpv_helper` must have one. Process isolation is an invariant, not a
profile-specific choice.
profile-specific choice. The pristine Electron tree is scanned recursively
before target packaging. Because Snap later overlays package-manager
`lib/**`/`usr/lib/**` trees into the payload root, its extracted-target scan
excludes exactly those two target-provided trees while remaining recursive
everywhere else. Electron-library symlinks outside those roots fail closed.
The manifest records:
+5 -1
View File
@@ -172,7 +172,11 @@ the bundled legal payload. AppImage, Snap, and Flatpak receive
`embedded-mpv-notices.json`, `THIRD_PARTY_NOTICES.txt`, and
`licenses/<package>/**`; DEB, RPM, Pacman, and marker-only packages must not
retain them. Package validation also scans the Electron executable and all
shipped Electron libraries for a direct libmpv dependency.
shipped Electron libraries for a direct libmpv dependency. Before target
packaging, that scan is recursive over the pristine Electron tree. After Snap
has merged its template runtime into the payload root, the post-target scan
excludes exactly its package-manager `lib/**` and `usr/lib/**` trees while
remaining recursive everywhere else.
At startup, Linux x64 frame-copy is advertised only after the main process
validates that manifest/files and successfully executes:
+116 -11
View File
@@ -958,17 +958,29 @@ test('validates Linux ELF process isolation, helper linkage, and bundled closure
[]
);
const addonLinkErrors = validatePackagedEmbeddedMpv(fixture.resourceDir, {
...options,
elfInspector: validElfInspector(fixture.nativeDir, manifest, {
[FRAME_COPY_ARTIFACTS.addon]: {
needed: ['libmpv.so.2'],
rpath: [],
runpath: [],
},
}),
});
assert.match(addonLinkErrors.join('\n'), /must not link libmpv/);
for (const artifactName of [
FRAME_COPY_ARTIFACTS.addon,
FRAME_COPY_ARTIFACTS.frameReader,
]) {
const isolationErrors = validatePackagedEmbeddedMpv(
fixture.resourceDir,
{
...options,
elfInspector: validElfInspector(fixture.nativeDir, manifest, {
[artifactName]: {
needed: ['libmpv.so.2'],
rpath: [],
runpath: [],
},
}),
}
);
assert.match(
isolationErrors.join('\n'),
/must not link libmpv/,
`${artifactName} must remain process-isolated from libmpv`
);
}
const pathBearingAddonLinkErrors = validatePackagedEmbeddedMpv(
fixture.resourceDir,
@@ -1065,6 +1077,99 @@ test('validates Linux ELF process isolation, helper linkage, and bundled closure
);
});
test('recursively validates pristine Electron libraries before target packaging', (t) => {
const fixture = createNativeFixture();
t.after(() =>
fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true })
);
const manifest = preparePackagedFrameCopyArtifacts(
fixture.nativeDir,
'linux',
{
profile: 'portable',
targetNames: ['snap'],
}
);
const nestedElectronLibrary = join(
fixture.appOutDir,
'future-electron-runtime',
'libfuture-electron.so'
);
fs.mkdirSync(dirname(nestedElectronLibrary), { recursive: true });
fs.writeFileSync(nestedElectronLibrary, 'future electron library');
const validationOptions = {
platform: 'linux',
required: true,
foreignArch: false,
profile: 'portable',
targetNames: ['snap'],
hostPlatform: 'linux',
};
const errors = validatePackagedEmbeddedMpv(fixture.resourceDir, {
...validationOptions,
elfInspector: validElfInspector(fixture.nativeDir, manifest, {
'libfuture-electron.so': {
needed: ['libmpv.so.2'],
rpath: [],
runpath: [],
},
}),
});
assert.match(
errors.join('\n'),
/Linux Electron library must not link libmpv.*libfuture-electron\.so/
);
fs.rmSync(dirname(nestedElectronLibrary), {
recursive: true,
force: true,
});
for (const packageLibraryDir of [
join(fixture.appOutDir, 'lib', 'x86_64-linux-gnu'),
join(fixture.appOutDir, 'usr', 'lib', 'x86_64-linux-gnu'),
]) {
const targetPath = join(packageLibraryDir, 'libpackage.so.1.0');
fs.mkdirSync(packageLibraryDir, { recursive: true });
fs.writeFileSync(targetPath, 'Snap template library');
fs.symlinkSync(
'libpackage.so.1.0',
join(packageLibraryDir, 'libpackage.so.1')
);
}
assert.deepEqual(
validatePackagedEmbeddedMpv(fixture.resourceDir, {
...validationOptions,
artifactFormat: 'snap',
elfInspector: validElfInspector(fixture.nativeDir, manifest),
}),
[]
);
fs.mkdirSync(dirname(nestedElectronLibrary), { recursive: true });
fs.writeFileSync(nestedElectronLibrary, 'future electron library');
const snapIsolationErrors = validatePackagedEmbeddedMpv(
fixture.resourceDir,
{
...validationOptions,
artifactFormat: 'snap',
elfInspector: validElfInspector(fixture.nativeDir, manifest, {
'libfuture-electron.so': {
needed: ['libmpv.so.2'],
rpath: [],
runpath: [],
},
}),
}
);
assert.match(
snapIsolationErrors.join('\n'),
/Linux Electron library must not link libmpv.*libfuture-electron\.so/
);
});
test('rejects undeclared helper dependencies in system-runtime packages', (t) => {
const fixture = createNativeFixture();
t.after(() =>
+21 -2
View File
@@ -1320,9 +1320,18 @@ function dependencyFileName(dependencyName) {
return dependencyName.replaceAll('\\', '/').split('/').at(-1) ?? '';
}
function listElectronShippedLinuxLibraries(resourceDir) {
function listElectronShippedLinuxLibraries(resourceDir, options = {}) {
const appDir = path.dirname(resourceDir);
const normalizedResourceDir = path.resolve(resourceDir);
const excludedSnapLibraryRoots =
options.artifactFormat === 'snap'
? new Set(
[
path.join(appDir, 'lib'),
path.join(appDir, 'usr', 'lib'),
].map((directoryPath) => path.resolve(directoryPath))
)
: new Set();
const libraries = [];
function visit(directoryPath) {
@@ -1334,6 +1343,9 @@ function listElectronShippedLinuxLibraries(resourceDir) {
continue;
}
if (entry.isDirectory()) {
if (excludedSnapLibraryRoots.has(path.resolve(entryPath))) {
continue;
}
visit(entryPath);
continue;
}
@@ -1346,6 +1358,11 @@ function listElectronShippedLinuxLibraries(resourceDir) {
}
}
// afterPack and unpacked-layout checks see the pristine Electron tree, so
// recurse to catch future nested Electron libraries. An extracted Snap
// overlays package-manager lib/ and usr/lib/ trees onto that same root;
// exclude exactly those target-provided roots while scanning everything
// else recursively.
visit(appDir);
return libraries.sort();
}
@@ -1385,7 +1402,8 @@ function inspectLinuxElfIsolation(
helper: path.join(nativeDir, linuxFrameCopyArtifacts.helper.name),
};
for (const [index, libraryPath] of listElectronShippedLinuxLibraries(
resourceDir
resourceDir,
{ artifactFormat: options.artifactFormat }
).entries()) {
inspectedPaths[`electronLibrary:${index}`] = libraryPath;
}
@@ -1918,6 +1936,7 @@ module.exports = {
commandExists,
copyRuntimeToNativeBuild,
findLibMpv,
listElectronShippedLinuxLibraries,
listRuntimeFiles,
listDylibs,
parseOtoolDependencies,
@@ -11,7 +11,10 @@ const require = createRequire(import.meta.url);
const {
parseReadelfDynamic,
} = require('../embedded-mpv/build-linux-runtime.cjs');
const { validatePackagedEmbeddedMpv } = require('./embedded-mpv-packaging.cjs');
const {
listElectronShippedLinuxLibraries,
validatePackagedEmbeddedMpv,
} = require('./embedded-mpv-packaging.cjs');
const {
LINUX_SYSTEM_PACKAGE_DEPENDENCIES,
resolveLinuxFrameCopyProfile,
@@ -970,42 +973,14 @@ function dependencyFileName(dependencyName) {
return String(dependencyName).replaceAll('\\', '/').split('/').at(-1) ?? '';
}
function listElectronLibraries(resourceDir) {
const appDir = path.dirname(resourceDir);
const resolvedResourceDir = path.resolve(resourceDir);
const libraries = [];
function visit(directoryPath) {
for (const entry of fs.readdirSync(directoryPath, {
withFileTypes: true,
})) {
const entryPath = path.join(directoryPath, entry.name);
if (path.resolve(entryPath) === resolvedResourceDir) {
continue;
}
if (entry.isDirectory()) {
visit(entryPath);
continue;
}
if (
(entry.isFile() || entry.isSymbolicLink()) &&
/\.so(?:\.\d+)*$/.test(entry.name)
) {
libraries.push(entryPath);
}
}
}
visit(appDir);
return libraries.sort();
}
function validateElectronIsolation(resourceDir, elfInspector) {
function validateElectronIsolation(resourceDir, artifactFormat, elfInspector) {
const errors = [];
const electronPath = path.join(path.dirname(resourceDir), 'iptvnator.bin');
const binaries = [
{ label: 'Electron binary', binaryPath: electronPath },
...listElectronLibraries(resourceDir).map((binaryPath) => ({
...listElectronShippedLinuxLibraries(resourceDir, {
artifactFormat,
}).map((binaryPath) => ({
label: 'Electron library',
binaryPath,
})),
@@ -1196,12 +1171,15 @@ export function verifyExtractedLinuxFrameCopyRuntime({
targetArch: packageArch,
profile: profile.name,
targetNames: profile.targets,
artifactFormat,
hostPlatform: 'linux',
executableName: 'iptvnator',
elfInspector,
})
);
errors.push(...validateElectronIsolation(resourceDir, elfInspector));
errors.push(
...validateElectronIsolation(resourceDir, artifactFormat, elfInspector)
);
if (foreignArch || errors.length > 0) {
return errors;
}
@@ -792,6 +792,143 @@ test('validates an x64 system payload and executes one bounded helper probe', ()
}
});
test('excludes only Snap template library roots from Electron isolation checks', () => {
const fixture = createSystemPayload({ architecture: 'arm64' });
const packageLibraryDirs = [
path.join(fixture.appDir, 'lib', 'x86_64-linux-gnu'),
path.join(fixture.appDir, 'usr', 'lib', 'x86_64-linux-gnu'),
];
const electronLibrary = path.join(fixture.appDir, 'libelectron-extra.so');
for (const [index, packageLibraryDir] of packageLibraryDirs.entries()) {
const packageLibraryTarget = path.join(
packageLibraryDir,
`libpackage-${index}.so.0.12.2`
);
fs.mkdirSync(packageLibraryDir, { recursive: true });
fs.writeFileSync(packageLibraryTarget, 'package-managed library');
fs.symlinkSync(
path.basename(packageLibraryTarget),
path.join(packageLibraryDir, `libpackage-${index}.so.0`)
);
}
fs.writeFileSync(electronLibrary, 'electron library');
const inspectedPaths = [];
const isPackageLibraryPath = (binaryPath) =>
packageLibraryDirs.some((packageLibraryDir) =>
path
.resolve(binaryPath)
.startsWith(`${path.resolve(packageLibraryDir)}${path.sep}`)
);
const ownershipScopedElfInspector = (binaryPath) => {
inspectedPaths.push(binaryPath);
if (isPackageLibraryPath(binaryPath)) {
throw new Error(
'package-manager library tree crossed the Electron ownership boundary'
);
}
return validElfInspector(binaryPath);
};
try {
assert.deepEqual(
verifyExtractedLinuxFrameCopyRuntime({
resourceDir: fixture.resourceDir,
artifactFormat: 'snap',
profileName: 'portable',
packageDependencies: [],
elfInspector: ownershipScopedElfInspector,
probeRunner() {
assert.fail('foreign-architecture Snap must not probe');
},
}),
[]
);
assert.ok(inspectedPaths.includes(electronLibrary));
assert.equal(inspectedPaths.some(isPackageLibraryPath), false);
const isolationErrors = verifyExtractedLinuxFrameCopyRuntime({
resourceDir: fixture.resourceDir,
artifactFormat: 'snap',
profileName: 'portable',
packageDependencies: [],
elfInspector(binaryPath) {
if (binaryPath === electronLibrary) {
return {
soname: null,
needed: ['libmpv.so.2'],
rpath: [],
runpath: [],
};
}
return ownershipScopedElfInspector(binaryPath);
},
probeRunner() {
assert.fail('foreign-architecture Snap must not probe');
},
});
assert.match(
isolationErrors.join('\n'),
/Electron library must not link libmpv.*libelectron-extra\.so/
);
const nestedElectronLibrary = path.join(
fixture.appDir,
'future-electron-runtime',
'libfuture-electron.so'
);
fs.mkdirSync(path.dirname(nestedElectronLibrary), {
recursive: true,
});
fs.writeFileSync(nestedElectronLibrary, 'nested electron library');
const nestedIsolationErrors = verifyExtractedLinuxFrameCopyRuntime({
resourceDir: fixture.resourceDir,
artifactFormat: 'snap',
profileName: 'portable',
packageDependencies: [],
elfInspector(binaryPath) {
if (binaryPath === nestedElectronLibrary) {
return {
soname: null,
needed: ['libmpv.so.2'],
rpath: [],
runpath: [],
};
}
return ownershipScopedElfInspector(binaryPath);
},
probeRunner() {
assert.fail('foreign-architecture Snap must not probe');
},
});
assert.match(
nestedIsolationErrors.join('\n'),
/Electron library must not link libmpv.*libfuture-electron\.so/
);
fs.symlinkSync(
path.basename(electronLibrary),
path.join(fixture.appDir, 'libEGL.so')
);
const symlinkErrors = verifyExtractedLinuxFrameCopyRuntime({
resourceDir: fixture.resourceDir,
artifactFormat: 'snap',
profileName: 'portable',
packageDependencies: [],
elfInspector: ownershipScopedElfInspector,
probeRunner() {
assert.fail('foreign-architecture Snap must not probe');
},
});
assert.match(
symlinkErrors.join('\n'),
/Electron library must be a regular file: .*libEGL\.so/
);
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
test('rejects helper probes terminated by a signal or hard timeout', () => {
for (const probeResult of [
{