mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
fix(packaging): scope Snap Electron library checks
This commit is contained in:
1 parent
541d47ac96
commit
351f9609d4
9 files changed
+315
-51
No files matched your search
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user