mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
fix(packaging): enforce Linux frame-copy isolation
This commit is contained in:
1 parent
d960997061
commit
e8903c86fb
5 files changed
+235
-30
No files matched your search
@@ -1,6 +1,5 @@
|
||||
const linuxAfterPack = require('./linux-after-pack.cjs');
|
||||
const {
|
||||
getEmbeddedMpvAddonArch,
|
||||
isForeignLinuxEmbeddedMpvArch,
|
||||
resolveElectronBuilderArchName,
|
||||
validatePackagedEmbeddedMpv,
|
||||
@@ -118,8 +117,11 @@ function resolveLinuxFrameCopyPackagingContext(
|
||||
);
|
||||
}
|
||||
|
||||
const addonArch = getEmbeddedMpvAddonArch(environment);
|
||||
const foreignArch = targetArch !== addonArch;
|
||||
// Official Linux frame-copy artifacts are intentionally x64-only. Do not
|
||||
// let a caller-provided build-arch environment value promote an ARM
|
||||
// package to a supported layout: every non-x64 target must remain the
|
||||
// marker-only native-view fallback.
|
||||
const foreignArch = targetArch !== 'x64';
|
||||
const profileValue =
|
||||
environment.IPTVNATOR_LINUX_FRAME_COPY_PROFILE?.trim() ?? '';
|
||||
if (!profileValue) {
|
||||
|
||||
@@ -305,6 +305,9 @@ test('package layout verifier uses canonical helpers and direct dependencies', (
|
||||
packageLayoutVerifier,
|
||||
/validateLinuxProfileTargets\(\s*linuxFrameCopyProfile,\s*linuxTargetNames\s*\)/s
|
||||
);
|
||||
assert.match(packageLayoutVerifier, /dirArch !== 'x64'/);
|
||||
assert.doesNotMatch(packageLayoutVerifier, /getEmbeddedMpvAddonArch/);
|
||||
assert.match(electronAfterPackSource, /targetArch !== 'x64'/);
|
||||
});
|
||||
|
||||
test('nx-electron packaging does not copy duplicate root package metadata', () => {
|
||||
|
||||
@@ -301,6 +301,26 @@ test('resolves the required Linux profile from afterPack targets before mutation
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps every non-x64 Linux target marker-only even when the configured addon arch matches it', () => {
|
||||
const context = resolveLinuxFrameCopyPackagingContext(
|
||||
{
|
||||
electronPlatformName: 'linux',
|
||||
arch: 3,
|
||||
targets: [{ name: 'deb' }],
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
environment: {
|
||||
IPTVNATOR_LINUX_FRAME_COPY_PROFILE: 'system',
|
||||
IPTVNATOR_EMBEDDED_MPV_ARCH: 'arm64',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(context.targetArch, 'arm64');
|
||||
assert.equal(context.foreignArch, true);
|
||||
});
|
||||
|
||||
test('rejects missing, mixed, and unknown required Linux packaging context', () => {
|
||||
const baseParams = {
|
||||
electronPlatformName: 'linux',
|
||||
@@ -811,6 +831,24 @@ test('validates Linux ELF process isolation, helper linkage, and bundled closure
|
||||
});
|
||||
assert.match(addonLinkErrors.join('\n'), /must not link libmpv/);
|
||||
|
||||
const pathBearingAddonLinkErrors = validatePackagedEmbeddedMpv(
|
||||
fixture.resourceDir,
|
||||
{
|
||||
...options,
|
||||
elfInspector: validElfInspector(fixture.nativeDir, manifest, {
|
||||
[FRAME_COPY_ARTIFACTS.addon]: {
|
||||
needed: ['/tmp/build/libmpv.so.2'],
|
||||
rpath: [],
|
||||
runpath: [],
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
assert.match(
|
||||
pathBearingAddonLinkErrors.join('\n'),
|
||||
/must not link libmpv.*\/tmp\/build\/libmpv\.so\.2/
|
||||
);
|
||||
|
||||
const helperLinkErrors = validatePackagedEmbeddedMpv(fixture.resourceDir, {
|
||||
...options,
|
||||
elfInspector: validElfInspector(fixture.nativeDir, manifest, {
|
||||
@@ -826,6 +864,29 @@ test('validates Linux ELF process isolation, helper linkage, and bundled closure
|
||||
/must directly need libmpv\.so\.2/
|
||||
);
|
||||
|
||||
const unexpectedHelperLinkErrors = validatePackagedEmbeddedMpv(
|
||||
fixture.resourceDir,
|
||||
{
|
||||
...options,
|
||||
elfInspector: validElfInspector(fixture.nativeDir, manifest, {
|
||||
[FRAME_COPY_ARTIFACTS.helper]: {
|
||||
needed: [
|
||||
manifest.libmpvSoname,
|
||||
'libEGL.so.1',
|
||||
'libc.so.6',
|
||||
'libsurprise.so.1',
|
||||
],
|
||||
rpath: [],
|
||||
runpath: ['$ORIGIN/lib'],
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
assert.match(
|
||||
unexpectedHelperLinkErrors.join('\n'),
|
||||
/helper dependency is not bundled or allowlisted.*libsurprise\.so\.1/
|
||||
);
|
||||
|
||||
const closureErrors = validatePackagedEmbeddedMpv(fixture.resourceDir, {
|
||||
...options,
|
||||
elfInspector: validElfInspector(fixture.nativeDir, manifest, {
|
||||
@@ -841,6 +902,69 @@ test('validates Linux ELF process isolation, helper linkage, and bundled closure
|
||||
closureErrors.join('\n'),
|
||||
/not bundled or allowlisted.*libsurprise\.so\.1/
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
join(fixture.appOutDir, 'libelectron-extra.so'),
|
||||
'electron library'
|
||||
);
|
||||
const electronLibraryErrors = validatePackagedEmbeddedMpv(
|
||||
fixture.resourceDir,
|
||||
{
|
||||
...options,
|
||||
elfInspector: validElfInspector(fixture.nativeDir, manifest, {
|
||||
'libelectron-extra.so': {
|
||||
needed: ['libmpv.so.2'],
|
||||
rpath: [],
|
||||
runpath: [],
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
assert.match(
|
||||
electronLibraryErrors.join('\n'),
|
||||
/Linux Electron library must not link libmpv.*libelectron-extra\.so/
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects undeclared helper dependencies in system-runtime packages', (t) => {
|
||||
const fixture = createNativeFixture();
|
||||
t.after(() =>
|
||||
fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true })
|
||||
);
|
||||
const manifest = preparePackagedFrameCopyArtifacts(
|
||||
fixture.nativeDir,
|
||||
'linux',
|
||||
{
|
||||
profile: 'system',
|
||||
targetNames: ['deb'],
|
||||
}
|
||||
);
|
||||
|
||||
const errors = validatePackagedEmbeddedMpv(fixture.resourceDir, {
|
||||
platform: 'linux',
|
||||
required: true,
|
||||
foreignArch: false,
|
||||
profile: 'system',
|
||||
targetNames: ['deb'],
|
||||
hostPlatform: 'linux',
|
||||
elfInspector: validElfInspector(fixture.nativeDir, manifest, {
|
||||
[FRAME_COPY_ARTIFACTS.helper]: {
|
||||
needed: [
|
||||
manifest.libmpvSoname,
|
||||
'libEGL.so.1',
|
||||
'libc.so.6',
|
||||
'libsurprise.so.1',
|
||||
],
|
||||
rpath: [],
|
||||
runpath: ['$ORIGIN/lib'],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
assert.match(
|
||||
errors.join('\n'),
|
||||
/helper dependency is not bundled or allowlisted.*libsurprise\.so\.1/
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveElectronBuilderArchName maps builder-util Arch enum values', () => {
|
||||
@@ -852,7 +976,7 @@ test('resolveElectronBuilderArchName maps builder-util Arch enum values', () =>
|
||||
assert.equal(resolveElectronBuilderArchName(99), null);
|
||||
});
|
||||
|
||||
test('flags Linux packages whose arch differs from the built addon', () => {
|
||||
test('flags every non-x64 Linux package regardless of configured build arch', () => {
|
||||
assert.equal(
|
||||
isForeignLinuxEmbeddedMpvArch('linux', 3, X64_ADDON_ENV),
|
||||
true
|
||||
@@ -865,6 +989,12 @@ test('flags Linux packages whose arch differs from the built addon', () => {
|
||||
isForeignLinuxEmbeddedMpvArch('linux', 'x64', X64_ADDON_ENV),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
isForeignLinuxEmbeddedMpvArch('linux', 'arm64', {
|
||||
IPTVNATOR_EMBEDDED_MPV_ARCH: 'arm64',
|
||||
}),
|
||||
true
|
||||
);
|
||||
// Only Linux fans out foreign arches from one dist tree.
|
||||
assert.equal(
|
||||
isForeignLinuxEmbeddedMpvArch('darwin', 'arm64', X64_ADDON_ENV),
|
||||
|
||||
@@ -745,26 +745,17 @@ function resolveConfiguredLinuxTargetNames(configuredTargets, targetArch) {
|
||||
return [...targetNames].sort();
|
||||
}
|
||||
|
||||
function getEmbeddedMpvAddonArch(env = process.env) {
|
||||
return env.IPTVNATOR_EMBEDDED_MPV_ARCH || process.arch;
|
||||
}
|
||||
|
||||
/**
|
||||
* The embedded MPV addon is compiled once per CI host (x64 on Linux), but
|
||||
* electron-builder also produces arm64/armv7l Linux packages from the same
|
||||
* dist output. Those packages must not ship a foreign-architecture
|
||||
* `embedded_mpv.node` — it can never load and produces a cryptic error.
|
||||
* Official Linux frame-copy support is x64-only. electron-builder also
|
||||
* produces arm64/armv7l packages, which must always carry only the
|
||||
* unavailable marker and native-view fallback.
|
||||
*/
|
||||
function isForeignLinuxEmbeddedMpvArch(
|
||||
platform,
|
||||
targetArch,
|
||||
env = process.env
|
||||
) {
|
||||
function isForeignLinuxEmbeddedMpvArch(platform, targetArch) {
|
||||
if (normalizeEmbeddedMpvPlatform(platform) !== 'linux') {
|
||||
return false;
|
||||
}
|
||||
const archName = resolveElectronBuilderArchName(targetArch);
|
||||
return Boolean(archName) && archName !== getEmbeddedMpvAddonArch(env);
|
||||
return Boolean(archName) && archName !== 'x64';
|
||||
}
|
||||
|
||||
// Maps electron-builder Linux output directory names (`linux-unpacked`,
|
||||
@@ -1294,6 +1285,40 @@ function normalizeElfInspection(value, binaryPath) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function dependencyFileName(dependencyName) {
|
||||
return dependencyName.replaceAll('\\', '/').split('/').at(-1) ?? '';
|
||||
}
|
||||
|
||||
function listElectronShippedLinuxLibraries(resourceDir) {
|
||||
const appDir = path.dirname(resourceDir);
|
||||
const normalizedResourceDir = 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) === normalizedResourceDir) {
|
||||
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 inspectLinuxElfIsolation(
|
||||
resourceDir,
|
||||
nativeDir,
|
||||
@@ -1328,6 +1353,11 @@ function inspectLinuxElfIsolation(
|
||||
reader: path.join(nativeDir, linuxFrameCopyArtifacts.frameReader.name),
|
||||
helper: path.join(nativeDir, linuxFrameCopyArtifacts.helper.name),
|
||||
};
|
||||
for (const [index, libraryPath] of listElectronShippedLinuxLibraries(
|
||||
resourceDir
|
||||
).entries()) {
|
||||
inspectedPaths[`electronLibrary:${index}`] = libraryPath;
|
||||
}
|
||||
const inspections = {};
|
||||
for (const [label, binaryPath] of Object.entries(inspectedPaths)) {
|
||||
if (
|
||||
@@ -1353,19 +1383,35 @@ function inspectLinuxElfIsolation(
|
||||
}
|
||||
}
|
||||
|
||||
for (const label of ['electron', 'addon', 'reader']) {
|
||||
for (const label of Object.keys(inspections).filter(
|
||||
(name) =>
|
||||
name === 'electron' ||
|
||||
name === 'addon' ||
|
||||
name === 'reader' ||
|
||||
name.startsWith('electronLibrary:')
|
||||
)) {
|
||||
const dynamic = inspections[label];
|
||||
if (!dynamic) {
|
||||
continue;
|
||||
}
|
||||
const displayLabel = label.startsWith('electronLibrary:')
|
||||
? 'Electron library'
|
||||
: label;
|
||||
for (const dependencyName of dynamic.needed) {
|
||||
if (dependencyFileName(dependencyName) !== dependencyName) {
|
||||
errors.push(
|
||||
`Linux ${displayLabel} DT_NEEDED entry must not contain a path: ${dependencyName} in ${inspectedPaths[label]}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
const libmpvDependencies = dynamic.needed.filter((dependencyName) =>
|
||||
anyLinuxLibmpvPattern.test(dependencyName)
|
||||
anyLinuxLibmpvPattern.test(dependencyFileName(dependencyName))
|
||||
);
|
||||
if (libmpvDependencies.length > 0) {
|
||||
errors.push(
|
||||
`Linux ${label} binary must not link libmpv; found ${libmpvDependencies.join(
|
||||
`Linux ${displayLabel} must not link libmpv; found ${libmpvDependencies.join(
|
||||
', '
|
||||
)}.`
|
||||
)} in ${inspectedPaths[label]}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1379,8 +1425,9 @@ function inspectLinuxElfIsolation(
|
||||
}
|
||||
const unexpectedLibmpvDependencies = helper.needed.filter(
|
||||
(dependencyName) =>
|
||||
anyLinuxLibmpvPattern.test(dependencyName) &&
|
||||
dependencyName !== manifest.libmpvSoname
|
||||
anyLinuxLibmpvPattern.test(
|
||||
dependencyFileName(dependencyName)
|
||||
) && dependencyName !== manifest.libmpvSoname
|
||||
);
|
||||
if (unexpectedLibmpvDependencies.length > 0) {
|
||||
errors.push(
|
||||
@@ -1408,6 +1455,32 @@ function inspectLinuxElfIsolation(
|
||||
}.`
|
||||
);
|
||||
}
|
||||
|
||||
const allowedHelperDependencies = new Set([
|
||||
manifest.libmpvSoname,
|
||||
...GLIBC_TOOLCHAIN_ALLOWLIST,
|
||||
...EXTERNAL_SYSTEM_LIBRARIES.map(({ name }) => name),
|
||||
...(manifest.runtimeMode === 'bundled' &&
|
||||
Array.isArray(manifest.runtimeFiles)
|
||||
? manifest.runtimeFiles.map(({ name }) => name)
|
||||
: []),
|
||||
...(manifest.runtimeMode === 'bundled' &&
|
||||
Array.isArray(
|
||||
manifest.runtimeDependencyClosure?.externalDependencies
|
||||
)
|
||||
? manifest.runtimeDependencyClosure.externalDependencies
|
||||
: []),
|
||||
]);
|
||||
for (const dependencyName of helper.needed) {
|
||||
if (
|
||||
dependencyFileName(dependencyName) !== dependencyName ||
|
||||
!allowedHelperDependencies.has(dependencyName)
|
||||
) {
|
||||
errors.push(
|
||||
`Linux frame-copy helper dependency is not bundled or allowlisted: ${dependencyName}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -1822,7 +1895,6 @@ module.exports = {
|
||||
validateNoForbiddenRuntimeLinks,
|
||||
getPackagedRuntimeCandidates,
|
||||
validatePackagedEmbeddedMpv,
|
||||
getEmbeddedMpvAddonArch,
|
||||
isForeignLinuxEmbeddedMpvArch,
|
||||
linuxUnpackedDirArch,
|
||||
resolveConfiguredLinuxTargetNames,
|
||||
|
||||
@@ -8,7 +8,6 @@ import { inspectPackagedDependencyClosure } from './asar-dependency-closure.mjs'
|
||||
const require = createRequire(import.meta.url);
|
||||
const { extractFile, listPackage } = require('@electron/asar');
|
||||
const {
|
||||
getEmbeddedMpvAddonArch,
|
||||
linuxUnpackedDirArch,
|
||||
resolveConfiguredLinuxTargetNames,
|
||||
validatePackagedEmbeddedMpv,
|
||||
@@ -622,9 +621,8 @@ function verifyPackagedDependencyClosure(resourceDir, errors) {
|
||||
);
|
||||
}
|
||||
|
||||
// The embedded MPV addon is built once per CI host (x64), but electron-builder
|
||||
// emits arm64/armv7l Linux output directories from the same dist tree. Those
|
||||
// must carry the unavailable marker instead of a foreign-architecture addon.
|
||||
// Official Linux frame-copy support is x64-only. Every arm64/armv7l output
|
||||
// must carry the unavailable marker instead of native frame-copy artifacts.
|
||||
function isForeignArchLinuxResourceDir(resourceDir) {
|
||||
if (platform !== 'linux') {
|
||||
return false;
|
||||
@@ -633,7 +631,7 @@ function isForeignArchLinuxResourceDir(resourceDir) {
|
||||
const dirArch = linuxUnpackedDirArch(
|
||||
path.basename(path.dirname(resourceDir))
|
||||
);
|
||||
return Boolean(dirArch) && dirArch !== getEmbeddedMpvAddonArch();
|
||||
return Boolean(dirArch) && dirArch !== 'x64';
|
||||
}
|
||||
|
||||
function verifyResourceDir(resourceDir) {
|
||||
|
||||
Reference in new issue
Block a user