fix(embedded-mpv): verify Linux helper linkage

This commit is contained in:
4gray committed 2026-07-17 22:42:36 +02:00
1 parent 45c3ab86d6
commit d960997061
6 files changed
+904 -118

No files matched your search

+130 -108
View File
@@ -16,11 +16,15 @@ const {
validateLinuxRuntimeManifest,
validateLinuxSystemBuildInputManifest,
} = require('../../tools/embedded-mpv/linux-runtime-manifest.cjs');
const {
resolveVerifiedLinuxLibMpvSoname,
runWithCleanup,
validateLinuxFrameCopyLinkage,
} = require('./embedded-mpv-linux-linkage.cjs');
const LINUX_PACKAGE_RUNTIME_MODES = Object.freeze(['system', 'bundled']);
const LINUX_STAGED_RUNTIME_ORIGIN = 'vendored-lgpl';
const LINUX_SOURCE_RUNTIME_ORIGIN = 'vendored-lgpl-source-build';
const VERSIONED_LINUX_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/;
const workspaceRoot = process.cwd();
const addonRoot = path.join(
@@ -452,35 +456,6 @@ function assertRequiredLinuxFrameCopyRuntime(runtime) {
}
}
function deriveLinuxLibMpvSoname(runtime) {
if (
runtime.buildInputMode !== 'bundled-runtime' ||
runtime.sourceRuntimeValidated !== true ||
!Array.isArray(runtime.sourceRuntimeManifest?.runtimeFiles)
) {
return null;
}
const candidates = runtime.sourceRuntimeManifest.runtimeFiles
.map((runtimeFile) => runtimeFile.name)
.filter((fileName) => VERSIONED_LINUX_LIBMPV_PATTERN.test(fileName))
.sort((left, right) => {
const leftVersionParts = left
.slice('libmpv.so.'.length)
.split('.').length;
const rightVersionParts = right
.slice('libmpv.so.'.length)
.split('.').length;
return (
leftVersionParts - rightVersionParts ||
left.length - right.length ||
left.localeCompare(right)
);
});
return candidates[0] ?? null;
}
function copyLinuxRuntimeClosureToNativeBuild(runtime) {
fs.rmSync(outputLibDir, { recursive: true, force: true });
@@ -540,7 +515,18 @@ function copyLinuxRuntimeClosureToNativeBuild(runtime) {
function writeLinuxFrameCopyBuildManifest(runtime) {
const copiedRuntimeFiles = copyLinuxRuntimeClosureToNativeBuild(runtime);
const libmpvSoname = deriveLinuxLibMpvSoname(runtime);
const libmpvSoname =
runtime.sourceRuntimeValidated === true &&
runtime.buildInputMode === 'bundled-runtime' &&
copiedRuntimeFiles.length > 0
? resolveVerifiedLinuxLibMpvSoname({
outputLibDir,
runtimeFiles: copiedRuntimeFiles,
runtimeDependencyClosure:
runtime.sourceRuntimeManifest.runtimeDependencyClosure,
readDynamicSection: readLinuxDynamicSection,
})
: null;
const packageRuntimeAvailable =
runtime.sourceRuntimeValidated === true &&
runtime.buildInputMode === 'bundled-runtime' &&
@@ -704,12 +690,33 @@ function runNodeGyp(command, env) {
}
}
function readLinuxDynamicSection(filePath) {
const result = spawnSync('readelf', ['-d', filePath], {
cwd: workspaceRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
if (result.error) {
throw new Error(
`Unable to run readelf for ${filePath}: ${result.error.message}`
);
}
if (result.status !== 0) {
throw new Error(
`readelf -d failed for ${filePath} with status ${
result.status ?? 1
}: ${(result.stderr ?? '').trim()}`
);
}
return result.stdout;
}
function main() {
fs.mkdirSync(outputDir, { recursive: true });
cleanDistNativeOutput();
cleanOutput();
if (targetPlatform !== process.platform) {
cleanOutput();
if (embeddedMpvRequired) {
throw new Error(
`Embedded MPV is required for ${targetPlatform}-${targetArch}, but this host is ${process.platform}-${process.arch}.`
@@ -743,57 +750,101 @@ function main() {
assertRequiredLinuxFrameCopyRuntime(runtime);
const runtimeManifest =
targetPlatform === 'darwin'
? copyRuntimeToNativeBuild({
runtimeLibDir: runtime.libDir,
outputLibDir,
runtimeOrigin: runtime.origin,
runtimeManifest: {
targetArch,
...runtime.manifest,
},
})
: targetPlatform === 'linux'
? writeLinuxFrameCopyBuildManifest(runtime)
: copyGenericRuntimeToNativeBuild(runtime);
fs.rmSync(unavailableMarkerFile, { force: true });
const electronPackageJson = require(
path.join(workspaceRoot, 'node_modules', 'electron', 'package.json')
);
const electronVersion = electronPackageJson.version;
const env = {
...process.env,
npm_config_runtime: 'electron',
npm_config_target: electronVersion,
npm_config_arch: targetArch,
npm_config_disturl: 'https://electronjs.org/headers',
npm_config_build_from_source: 'true',
npm_config_update_binary: 'false',
LIBMPV_INCLUDE_DIR: runtime.includeDir,
...(targetPlatform === 'linux'
? {
LINUX_NATIVE_LIBRARY_DIR: runtime.libDir,
}
: { LIBMPV_LIBRARY_DIR: outputLibDir }),
...(runtime.windowsImportLib
? {
LIBMPV_IMPORT_LIB: path.join(
const buildNativeArtifacts = () => {
const runtimeManifest =
targetPlatform === 'darwin'
? copyRuntimeToNativeBuild({
runtimeLibDir: runtime.libDir,
outputLibDir,
path.basename(runtime.windowsImportLib)
),
}
: {}),
};
runtimeOrigin: runtime.origin,
runtimeManifest: {
targetArch,
...runtime.manifest,
},
})
: targetPlatform === 'linux'
? writeLinuxFrameCopyBuildManifest(runtime)
: copyGenericRuntimeToNativeBuild(runtime);
log(
`Building native addon against Electron ${electronVersion} using ${runtime.origin} runtime for ${targetPlatform}-${targetArch}...`
);
cleanNativeBuildIntermediates();
try {
const electronPackageJson = require(
path.join(workspaceRoot, 'node_modules', 'electron', 'package.json')
);
const electronVersion = electronPackageJson.version;
const env = {
...process.env,
npm_config_runtime: 'electron',
npm_config_target: electronVersion,
npm_config_arch: targetArch,
npm_config_disturl: 'https://electronjs.org/headers',
npm_config_build_from_source: 'true',
npm_config_update_binary: 'false',
LIBMPV_INCLUDE_DIR: runtime.includeDir,
...(targetPlatform === 'linux'
? {
LINUX_VERIFIED_RUNTIME_LIBRARY_DIR: outputLibDir,
}
: { LIBMPV_LIBRARY_DIR: outputLibDir }),
...(runtime.windowsImportLib
? {
LIBMPV_IMPORT_LIB: path.join(
outputLibDir,
path.basename(runtime.windowsImportLib)
),
}
: {}),
};
log(
`Building native addon against Electron ${electronVersion} using ${runtime.origin} runtime for ${targetPlatform}-${targetArch}...`
);
cleanNativeBuildIntermediates();
runNodeGyp('configure', env);
runNodeGyp('build', env);
if (!fs.existsSync(outputFile)) {
throw new Error(`Build finished without producing ${outputFile}.`);
}
if (targetPlatform === 'linux') {
validateLinuxFrameCopyLinkage({
expectedLibmpvSoname: runtimeManifest.libmpvSoname,
outputDir,
readDynamicSection: readLinuxDynamicSection,
});
}
if (targetPlatform === 'darwin') {
patchAddonForBundledRuntime(outputFile, outputLibDir);
// The frame-copy helper executable links libmpv too and sits next
// to the same lib/ directory, so it gets the identical
// dependency-path rewrite + ad-hoc re-sign.
const frameHelperFile = path.join(
outputDir,
'iptvnator_mpv_helper'
);
if (fs.existsSync(frameHelperFile)) {
patchAddonForBundledRuntime(frameHelperFile, outputLibDir);
}
const forbiddenLinkErrors = validateNoForbiddenRuntimeLinks([
outputFile,
...(fs.existsSync(frameHelperFile) ? [frameHelperFile] : []),
...runtimeManifest.dylibs.map((dylib) =>
path.join(outputLibDir, dylib)
),
]);
if (
runtime.origin === 'vendored-lgpl' &&
forbiddenLinkErrors.length > 0
) {
throw new Error(forbiddenLinkErrors.join('\n'));
}
}
fs.rmSync(unavailableMarkerFile, { force: true });
};
try {
runWithCleanup(buildNativeArtifacts, cleanOutput);
} catch (error) {
if (!embeddedMpvRequired && runtime.origin === 'system-dev') {
// The system-dev fallback triggers on any machine with
@@ -805,40 +856,11 @@ function main() {
error instanceof Error ? error.message : String(error)
}`
);
cleanOutput();
return;
}
throw error;
}
if (!fs.existsSync(outputFile)) {
throw new Error(`Build finished without producing ${outputFile}.`);
}
if (targetPlatform === 'darwin') {
patchAddonForBundledRuntime(outputFile, outputLibDir);
// The frame-copy helper executable links libmpv too and sits next to
// the same lib/ directory, so it gets the identical dependency-path
// rewrite + ad-hoc re-sign.
const frameHelperFile = path.join(outputDir, 'iptvnator_mpv_helper');
if (fs.existsSync(frameHelperFile)) {
patchAddonForBundledRuntime(frameHelperFile, outputLibDir);
}
const forbiddenLinkErrors = validateNoForbiddenRuntimeLinks([
outputFile,
...(fs.existsSync(frameHelperFile) ? [frameHelperFile] : []),
...runtimeManifest.dylibs.map((dylib) =>
path.join(outputLibDir, dylib)
),
]);
if (
runtime.origin === 'vendored-lgpl' &&
forbiddenLinkErrors.length > 0
) {
throw new Error(forbiddenLinkErrors.join('\n'));
}
}
log(`Built ${path.relative(workspaceRoot, outputFile)}.`);
}
@@ -0,0 +1,294 @@
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const VERSIONED_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/;
const LIBMPV_NEEDED_PATTERN = /^libmpv\.so(?:\..*)?$/;
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
const LINUX_FRAME_COPY_ARTIFACTS = Object.freeze([
Object.freeze({
fileName: 'embedded_mpv.node',
label: 'embedded MPV addon',
mayLinkLibmpv: false,
}),
Object.freeze({
fileName: 'embedded_mpv_frame_reader.node',
label: 'embedded MPV frame reader',
mayLinkLibmpv: false,
}),
Object.freeze({
fileName: 'iptvnator_mpv_helper',
label: 'embedded MPV frame-copy helper',
mayLinkLibmpv: true,
}),
]);
function parseReadelfDynamic(output) {
if (typeof output !== 'string') {
throw new TypeError('readelf dynamic output must be a string.');
}
const dynamic = {
needed: [],
rpath: [],
runpath: [],
soname: [],
};
const dynamicEntryPattern =
/\((NEEDED|RPATH|RUNPATH|SONAME)\)[^[]*\[([^\]]*)\]/g;
for (const [, tag, value] of output.matchAll(dynamicEntryPattern)) {
if (tag === 'NEEDED') {
dynamic.needed.push(value);
continue;
}
if (tag === 'SONAME') {
dynamic.soname.push(value);
continue;
}
dynamic[tag.toLowerCase()].push(
...value.split(':').filter((entry) => entry.length > 0)
);
}
return dynamic;
}
function exactlyOneRuntimeFile(runtimeFiles, name) {
if (!Array.isArray(runtimeFiles)) {
throw new Error('Linux runtimeFiles metadata must be an array.');
}
const matchingRecords = runtimeFiles.filter(
(runtimeFile) => runtimeFile?.name === name
);
if (matchingRecords.length !== 1) {
throw new Error(
`Linux runtime must contain exactly one exact runtimeFiles record for ${name}.`
);
}
const [runtimeFile] = matchingRecords;
if (
!Number.isInteger(runtimeFile.size) ||
runtimeFile.size <= 0 ||
typeof runtimeFile.sha256 !== 'string' ||
!SHA256_PATTERN.test(runtimeFile.sha256)
) {
throw new Error(
`Linux runtimeFiles record for ${name} has invalid size or SHA-256 metadata.`
);
}
return runtimeFile;
}
function readVerifiedRuntimeFile(filePath, runtimeFile) {
let stat;
try {
stat = fs.lstatSync(filePath);
} catch {
throw new Error(`Missing copied Linux runtime file: ${filePath}`);
}
if (!stat.isFile() || stat.isSymbolicLink()) {
throw new Error(
`Copied Linux runtime file must be a regular non-symbolic-link file: ${filePath}`
);
}
let descriptor;
try {
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0)
);
const descriptorStat = fs.fstatSync(descriptor);
if (!descriptorStat.isFile()) {
throw new Error(
`Copied Linux runtime path is not a regular file: ${filePath}`
);
}
const contents = fs.readFileSync(descriptor);
if (contents.byteLength !== runtimeFile.size) {
throw new Error(
`Size mismatch for copied Linux runtime file ${runtimeFile.name}: expected ${runtimeFile.size}, received ${contents.byteLength}.`
);
}
const actualSha256 = crypto
.createHash('sha256')
.update(contents)
.digest('hex');
if (actualSha256 !== runtimeFile.sha256) {
throw new Error(
`SHA-256 mismatch for copied Linux runtime file ${runtimeFile.name}: expected ${runtimeFile.sha256}, received ${actualSha256}.`
);
}
} finally {
if (descriptor !== undefined) {
fs.closeSync(descriptor);
}
}
}
function closureLibMpvSoname(runtimeDependencyClosure) {
if (!Array.isArray(runtimeDependencyClosure?.entries)) {
throw new Error(
'Validated Linux runtime dependency closure entries are required.'
);
}
const libMpvEntries = runtimeDependencyClosure.entries.filter(
(entry) =>
entry?.name === 'libmpv.so' ||
VERSIONED_LIBMPV_PATTERN.test(entry?.name)
);
const declaredSonames = libMpvEntries.map((entry) => entry.soname);
const uniqueSonames = new Set(declaredSonames);
if (
declaredSonames.length === 0 ||
declaredSonames.some(
(soname) =>
typeof soname !== 'string' ||
!VERSIONED_LIBMPV_PATTERN.test(soname)
) ||
uniqueSonames.size !== 1
) {
throw new Error(
'Validated Linux runtime closure must declare exactly one versioned libmpv SONAME.'
);
}
return declaredSonames[0];
}
function resolveVerifiedLinuxLibMpvSoname({
outputLibDir,
runtimeFiles,
runtimeDependencyClosure,
readDynamicSection,
}) {
if (typeof readDynamicSection !== 'function') {
throw new TypeError('Linux readelf dynamic reader is required.');
}
const expectedSoname = closureLibMpvSoname(runtimeDependencyClosure);
const linkerInputRecord = exactlyOneRuntimeFile(runtimeFiles, 'libmpv.so');
const exactSonameRecord = exactlyOneRuntimeFile(
runtimeFiles,
expectedSoname
);
const linkerInputPath = path.join(outputLibDir, 'libmpv.so');
const exactSonamePath = path.join(outputLibDir, expectedSoname);
readVerifiedRuntimeFile(linkerInputPath, linkerInputRecord);
readVerifiedRuntimeFile(exactSonamePath, exactSonameRecord);
const dynamic = parseReadelfDynamic(readDynamicSection(linkerInputPath));
if (
dynamic.soname.length !== 1 ||
!VERSIONED_LIBMPV_PATTERN.test(dynamic.soname[0])
) {
throw new Error(
'Copied Linux libmpv.so must contain exactly one DT_SONAME with a versioned libmpv basename.'
);
}
if (dynamic.soname[0] !== expectedSoname) {
throw new Error(
`Copied Linux libmpv.so DT_SONAME ${dynamic.soname[0]} does not match validated closure SONAME ${expectedSoname}.`
);
}
return expectedSoname;
}
function assertRegularArtifact(filePath, label) {
let stat;
try {
stat = fs.lstatSync(filePath);
} catch {
throw new Error(`Missing ${label}: ${filePath}`);
}
if (!stat.isFile() || stat.isSymbolicLink()) {
throw new Error(
`${label} must be a regular non-symbolic-link file: ${filePath}`
);
}
}
function validateLinuxFrameCopyLinkage({
expectedLibmpvSoname,
outputDir,
readDynamicSection,
}) {
if (
typeof expectedLibmpvSoname !== 'string' ||
!VERSIONED_LIBMPV_PATTERN.test(expectedLibmpvSoname)
) {
throw new Error(
'Linux frame-copy linkage validation requires an exact versioned libmpv SONAME.'
);
}
if (typeof readDynamicSection !== 'function') {
throw new TypeError('Linux readelf dynamic reader is required.');
}
for (const artifact of LINUX_FRAME_COPY_ARTIFACTS) {
const artifactPath = path.join(outputDir, artifact.fileName);
assertRegularArtifact(artifactPath, artifact.label);
const dynamic = parseReadelfDynamic(readDynamicSection(artifactPath));
const libMpvDependencies = dynamic.needed.filter((dependency) =>
LIBMPV_NEEDED_PATTERN.test(dependency)
);
if (!artifact.mayLinkLibmpv) {
if (libMpvDependencies.length > 0) {
throw new Error(
`${artifact.label} must not have a direct libmpv DT_NEEDED entry; found ${libMpvDependencies.join(', ')}.`
);
}
continue;
}
if (
libMpvDependencies.length !== 1 ||
libMpvDependencies[0] !== expectedLibmpvSoname
) {
throw new Error(
`${artifact.label} DT_NEEDED must contain exactly ${expectedLibmpvSoname}; found ${
libMpvDependencies.join(', ') || '<none>'
}.`
);
}
if (dynamic.rpath.length !== 0) {
throw new Error(
`${artifact.label} must not contain RPATH; found ${dynamic.rpath.join(':')}.`
);
}
if (
dynamic.runpath.length !== 1 ||
dynamic.runpath[0] !== '$ORIGIN/lib'
) {
throw new Error(
`${artifact.label} RUNPATH must be exactly $ORIGIN/lib; found ${
dynamic.runpath.join(':') || '<none>'
}.`
);
}
}
}
function runWithCleanup(operation, cleanup) {
try {
return operation();
} catch (error) {
cleanup();
throw error;
}
}
module.exports = {
parseReadelfDynamic,
resolveVerifiedLinuxLibMpvSoname,
runWithCleanup,
validateLinuxFrameCopyLinkage,
};
+2 -2
View File
@@ -162,8 +162,8 @@
"-Wl,-rpath,'$$ORIGIN/lib'"
],
"libraries": [
"-L<!(node -p \"process.env.LINUX_NATIVE_LIBRARY_DIR || '/usr/lib'\")",
"<!(node -e \"const path = require('path'); const dir = process.env.LINUX_NATIVE_LIBRARY_DIR || '/usr/lib'; process.stdout.write(path.join(dir, 'libmpv.so'))\")",
"-L<!(node -e \"const dir = process.env.LINUX_VERIFIED_RUNTIME_LIBRARY_DIR; if (!dir) { throw new Error('Missing LINUX_VERIFIED_RUNTIME_LIBRARY_DIR'); } process.stdout.write(dir)\")",
"<!(node -e \"const path = require('path'); const dir = process.env.LINUX_VERIFIED_RUNTIME_LIBRARY_DIR; if (!dir) { throw new Error('Missing LINUX_VERIFIED_RUNTIME_LIBRARY_DIR'); } process.stdout.write(path.join(dir, 'libmpv.so'))\")",
"-lEGL",
"-lOpenGL",
"-lgbm",
@@ -0,0 +1,442 @@
import {
existsSync,
mkdtempSync,
mkdirSync,
rmSync,
symlinkSync,
unlinkSync,
writeFileSync,
} from 'fs';
import { createHash } from 'crypto';
import { createRequire } from 'module';
import { tmpdir } from 'os';
import path from 'path';
const linkageModulePath = path.resolve(
__dirname,
'../../../embedded-mpv-linux-linkage.cjs'
);
const requireBuildHelper = createRequire(__filename);
interface RuntimeFileRecord {
name: string;
size: number;
sha256: string;
}
interface SonameFixture {
exactPath: string;
outputLibDir: string;
runtimeDependencyClosure: {
entries: Array<{
name: string;
needed: string[];
rpath: string[];
runpath: string[];
soname: string | null;
}>;
};
runtimeFiles: RuntimeFileRecord[];
}
function loadLinkageModule(): {
parseReadelfDynamic: (output: string) => {
needed: string[];
rpath: string[];
runpath: string[];
soname: string[];
};
resolveVerifiedLinuxLibMpvSoname: (options: {
outputLibDir: string;
readDynamicSection: (filePath: string) => string;
runtimeDependencyClosure: SonameFixture['runtimeDependencyClosure'];
runtimeFiles: RuntimeFileRecord[];
}) => string;
runWithCleanup: <T>(operation: () => T, cleanup: () => void) => T;
validateLinuxFrameCopyLinkage: (options: {
expectedLibmpvSoname: string;
outputDir: string;
readDynamicSection: (filePath: string) => string;
}) => void;
} {
expect(existsSync(linkageModulePath)).toBe(true);
return requireBuildHelper(linkageModulePath);
}
function sha256(contents: Buffer): string {
return createHash('sha256').update(contents).digest('hex');
}
function runtimeFile(name: string, contents: Buffer): RuntimeFileRecord {
return {
name,
size: contents.byteLength,
sha256: sha256(contents),
};
}
function readelfDynamic(
entries: Array<['NEEDED' | 'RPATH' | 'RUNPATH' | 'SONAME', string]>
): string {
return entries
.map(
([tag, value], index) =>
` 0x${index
.toString(16)
.padStart(16, '0')} (${tag}) Library value: [${value}]`
)
.join('\n');
}
describe('Linux Embedded MPV linkage verification', () => {
const temporaryDirectories: string[] = [];
afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});
function temporaryDirectory(): string {
const directory = mkdtempSync(
path.join(tmpdir(), 'iptvnator-mpv-linkage-')
);
temporaryDirectories.push(directory);
return directory;
}
function createSonameFixture(soname = 'libmpv.so.2'): SonameFixture {
const outputLibDir = path.join(temporaryDirectory(), 'lib');
mkdirSync(outputLibDir, { recursive: true });
const contents = Buffer.from('verified libmpv ELF contents');
const aliasPath = path.join(outputLibDir, 'libmpv.so');
const exactPath = path.join(outputLibDir, soname);
writeFileSync(aliasPath, contents);
writeFileSync(exactPath, contents);
return {
exactPath,
outputLibDir,
runtimeFiles: [
runtimeFile('libmpv.so', contents),
runtimeFile(soname, contents),
],
runtimeDependencyClosure: {
entries: [
{
name: 'libmpv.so',
needed: [],
rpath: [],
runpath: ['$ORIGIN'],
soname,
},
{
name: soname,
needed: [],
rpath: [],
runpath: ['$ORIGIN'],
soname,
},
],
},
};
}
it('parses every dynamic tag without hiding duplicate SONAME entries', () => {
const { parseReadelfDynamic } = loadLinkageModule();
expect(
parseReadelfDynamic(
readelfDynamic([
['NEEDED', 'libmpv.so.2'],
['RPATH', '/forbidden'],
['RUNPATH', '$ORIGIN/lib'],
['SONAME', 'libmpv.so.2'],
['SONAME', 'libmpv.so.3'],
])
)
).toEqual({
needed: ['libmpv.so.2'],
rpath: ['/forbidden'],
runpath: ['$ORIGIN/lib'],
soname: ['libmpv.so.2', 'libmpv.so.3'],
});
});
it('resolves the exact libmpv SONAME from closure metadata and verified copied files', () => {
const { resolveVerifiedLinuxLibMpvSoname } = loadLinkageModule();
const fixture = createSonameFixture();
expect(
resolveVerifiedLinuxLibMpvSoname({
...fixture,
readDynamicSection: () =>
readelfDynamic([['SONAME', 'libmpv.so.2']]),
})
).toBe('libmpv.so.2');
});
it('rejects missing and ambiguous closure SONAME metadata', () => {
const { resolveVerifiedLinuxLibMpvSoname } = loadLinkageModule();
const missingFixture = createSonameFixture();
for (const entry of missingFixture.runtimeDependencyClosure.entries) {
entry.soname = null;
}
expect(() =>
resolveVerifiedLinuxLibMpvSoname({
...missingFixture,
readDynamicSection: () =>
readelfDynamic([['SONAME', 'libmpv.so.2']]),
})
).toThrow(/exactly one versioned libmpv SONAME/i);
const ambiguousFixture = createSonameFixture();
ambiguousFixture.runtimeDependencyClosure.entries.push({
name: 'libmpv.so.3',
needed: [],
rpath: [],
runpath: ['$ORIGIN'],
soname: 'libmpv.so.3',
});
expect(() =>
resolveVerifiedLinuxLibMpvSoname({
...ambiguousFixture,
readDynamicSection: () =>
readelfDynamic([['SONAME', 'libmpv.so.2']]),
})
).toThrow(/exactly one versioned libmpv SONAME/i);
});
it('rejects missing, ambiguous, and mismatched DT_SONAME values', () => {
const { resolveVerifiedLinuxLibMpvSoname } = loadLinkageModule();
const fixture = createSonameFixture();
expect(() =>
resolveVerifiedLinuxLibMpvSoname({
...fixture,
readDynamicSection: () => readelfDynamic([]),
})
).toThrow(/exactly one DT_SONAME/i);
expect(() =>
resolveVerifiedLinuxLibMpvSoname({
...fixture,
readDynamicSection: () =>
readelfDynamic([
['SONAME', 'libmpv.so.2'],
['SONAME', 'libmpv.so.3'],
]),
})
).toThrow(/exactly one DT_SONAME/i);
expect(() =>
resolveVerifiedLinuxLibMpvSoname({
...fixture,
readDynamicSection: () =>
readelfDynamic([['SONAME', 'libmpv.so.3']]),
})
).toThrow(/does not match validated closure SONAME/i);
});
(process.platform === 'win32' ? it.skip : it)(
'rejects a symlinked copied linker input',
() => {
const { resolveVerifiedLinuxLibMpvSoname } = loadLinkageModule();
const fixture = createSonameFixture();
const aliasPath = path.join(fixture.outputLibDir, 'libmpv.so');
unlinkSync(aliasPath);
symlinkSync(path.basename(fixture.exactPath), aliasPath);
expect(() =>
resolveVerifiedLinuxLibMpvSoname({
...fixture,
readDynamicSection: () =>
readelfDynamic([['SONAME', 'libmpv.so.2']]),
})
).toThrow(/must be a regular non-symbolic-link file/i);
}
);
it('rejects a missing exact runtime record and a mismatched exact file hash', () => {
const { resolveVerifiedLinuxLibMpvSoname } = loadLinkageModule();
const missingRecordFixture = createSonameFixture();
expect(() =>
resolveVerifiedLinuxLibMpvSoname({
...missingRecordFixture,
runtimeFiles: missingRecordFixture.runtimeFiles.filter(
({ name }) => name !== 'libmpv.so.2'
),
readDynamicSection: () =>
readelfDynamic([['SONAME', 'libmpv.so.2']]),
})
).toThrow(/exact runtimeFiles record/i);
const mismatchedFileFixture = createSonameFixture();
writeFileSync(
mismatchedFileFixture.exactPath,
Buffer.from('tampered exact SONAME file')
);
expect(() =>
resolveVerifiedLinuxLibMpvSoname({
...mismatchedFileFixture,
readDynamicSection: () =>
readelfDynamic([['SONAME', 'libmpv.so.2']]),
})
).toThrow(/size|SHA-256/i);
});
function createArtifactFixture(): {
outputDir: string;
readDynamicSection: (filePath: string) => string;
outputs: Record<string, string>;
} {
const outputDir = temporaryDirectory();
const outputs: Record<string, string> = {
'embedded_mpv.node': readelfDynamic([['NEEDED', 'libX11.so.6']]),
'embedded_mpv_frame_reader.node': readelfDynamic([]),
iptvnator_mpv_helper: readelfDynamic([
['NEEDED', 'libmpv.so.2'],
['NEEDED', 'libEGL.so.1'],
['RUNPATH', '$ORIGIN/lib'],
]),
};
for (const artifact of Object.keys(outputs)) {
writeFileSync(path.join(outputDir, artifact), 'ELF');
}
return {
outputDir,
outputs,
readDynamicSection: (filePath: string) =>
outputs[path.basename(filePath)],
};
}
it('accepts only process-isolated Linux frame-copy linkage', () => {
const { validateLinuxFrameCopyLinkage } = loadLinkageModule();
const fixture = createArtifactFixture();
expect(() =>
validateLinuxFrameCopyLinkage({
expectedLibmpvSoname: 'libmpv.so.2',
outputDir: fixture.outputDir,
readDynamicSection: fixture.readDynamicSection,
})
).not.toThrow();
});
it('rejects a helper linked to the wrong libmpv SONAME', () => {
const { validateLinuxFrameCopyLinkage } = loadLinkageModule();
const fixture = createArtifactFixture();
fixture.outputs.iptvnator_mpv_helper = readelfDynamic([
['NEEDED', 'libmpv.so.3'],
['RUNPATH', '$ORIGIN/lib'],
]);
expect(() =>
validateLinuxFrameCopyLinkage({
expectedLibmpvSoname: 'libmpv.so.2',
outputDir: fixture.outputDir,
readDynamicSection: fixture.readDynamicSection,
})
).toThrow(/helper.*DT_NEEDED must contain exactly libmpv\.so\.2/i);
});
it('rejects helper RPATH and any RUNPATH other than $ORIGIN/lib', () => {
const { validateLinuxFrameCopyLinkage } = loadLinkageModule();
const rpathFixture = createArtifactFixture();
rpathFixture.outputs.iptvnator_mpv_helper = readelfDynamic([
['NEEDED', 'libmpv.so.2'],
['RPATH', '/host/lib'],
['RUNPATH', '$ORIGIN/lib'],
]);
expect(() =>
validateLinuxFrameCopyLinkage({
expectedLibmpvSoname: 'libmpv.so.2',
outputDir: rpathFixture.outputDir,
readDynamicSection: rpathFixture.readDynamicSection,
})
).toThrow(/helper must not contain RPATH/i);
const runpathFixture = createArtifactFixture();
runpathFixture.outputs.iptvnator_mpv_helper = readelfDynamic([
['NEEDED', 'libmpv.so.2'],
['RUNPATH', '$ORIGIN'],
]);
expect(() =>
validateLinuxFrameCopyLinkage({
expectedLibmpvSoname: 'libmpv.so.2',
outputDir: runpathFixture.outputDir,
readDynamicSection: runpathFixture.readDynamicSection,
})
).toThrow(/helper RUNPATH must be exactly \$ORIGIN\/lib/i);
});
it.each([
['embedded_mpv.node', 'addon'],
['embedded_mpv_frame_reader.node', 'frame reader'],
])('rejects Electron-side libmpv linkage from %s', (fileName, label) => {
const { validateLinuxFrameCopyLinkage } = loadLinkageModule();
const fixture = createArtifactFixture();
fixture.outputs[fileName] = readelfDynamic([['NEEDED', 'libmpv.so.2']]);
expect(() =>
validateLinuxFrameCopyLinkage({
expectedLibmpvSoname: 'libmpv.so.2',
outputDir: fixture.outputDir,
readDynamicSection: fixture.readDynamicSection,
})
).toThrow(new RegExp(`${label} must not have a direct libmpv`, 'i'));
});
it('rejects missing artifacts and readelf failures', () => {
const { validateLinuxFrameCopyLinkage } = loadLinkageModule();
const missingArtifactFixture = createArtifactFixture();
unlinkSync(
path.join(
missingArtifactFixture.outputDir,
'embedded_mpv_frame_reader.node'
)
);
expect(() =>
validateLinuxFrameCopyLinkage({
expectedLibmpvSoname: 'libmpv.so.2',
outputDir: missingArtifactFixture.outputDir,
readDynamicSection: missingArtifactFixture.readDynamicSection,
})
).toThrow(/missing.*frame reader/i);
const readelfFailureFixture = createArtifactFixture();
expect(() =>
validateLinuxFrameCopyLinkage({
expectedLibmpvSoname: 'libmpv.so.2',
outputDir: readelfFailureFixture.outputDir,
readDynamicSection: () => {
throw new Error('readelf is unavailable');
},
})
).toThrow(/readelf is unavailable/);
});
it('runs cleanup before rethrowing the original transaction failure', () => {
const { runWithCleanup } = loadLinkageModule();
const calls: string[] = [];
const failure = new Error('post-link validation failed');
expect(() =>
runWithCleanup(
() => {
calls.push('operation');
throw failure;
},
() => calls.push('cleanup')
)
).toThrow(failure);
expect(calls).toEqual(['operation', 'cleanup']);
});
});
@@ -590,11 +590,14 @@ describe('Embedded MPV native source recording invariants', () => {
'SHA-256 mismatch for staged Linux runtime file'
);
expect(buildScriptSource).toContain(
'LINUX_NATIVE_LIBRARY_DIR: runtime.libDir'
'LINUX_VERIFIED_RUNTIME_LIBRARY_DIR: outputLibDir'
);
expect(buildScriptSource).not.toContain(
'process.env.LINUX_NATIVE_LIBRARY_DIR || runtime.libDir'
);
expect(buildScriptSource).not.toContain(
'LINUX_NATIVE_LIBRARY_DIR: runtime.libDir'
);
});
it('writes a profile-neutral Linux frame-copy build manifest', () => {
@@ -636,15 +639,15 @@ describe('Embedded MPV native source recording invariants', () => {
);
});
it('derives packaged Linux libmpv identity from the validated runtime', () => {
it('derives packaged Linux libmpv identity from validated closure SONAME metadata', () => {
expect(buildScriptSource).toContain('resolveVerifiedLinuxLibMpvSoname');
expect(buildScriptSource).toContain(
'function deriveLinuxLibMpvSoname(runtime)'
'runtime.sourceRuntimeManifest.runtimeDependencyClosure'
);
expect(buildScriptSource).toContain(
'runtime.sourceRuntimeManifest.runtimeFiles'
);
expect(buildScriptSource).toContain('VERSIONED_LINUX_LIBMPV_PATTERN');
expect(buildScriptSource).toContain('libmpvSoname,');
expect(buildScriptSource).not.toContain(
'VERSIONED_LINUX_LIBMPV_PATTERN'
);
expect(buildScriptSource).not.toContain("libmpvSoname: 'libmpv.so.2'");
});
@@ -659,6 +662,24 @@ describe('Embedded MPV native source recording invariants', () => {
expect(buildScriptSource).toContain('bundled: packageRuntimeAvailable');
});
it('validates Linux post-link isolation before marking the build available', () => {
const postLinkValidation = buildScriptSource.indexOf(
'validateLinuxFrameCopyLinkage({'
);
const availabilityMarkerRemoval = buildScriptSource.lastIndexOf(
'fs.rmSync(unavailableMarkerFile'
);
expect(buildScriptSource).toContain(
"spawnSync('readelf', ['-d', filePath]"
);
expect(postLinkValidation).toBeGreaterThanOrEqual(0);
expect(availabilityMarkerRemoval).toBeGreaterThan(postLinkValidation);
expect(buildScriptSource).toContain(
'runWithCleanup(buildNativeArtifacts, cleanOutput)'
);
});
it('uses platform-specific embedded MPV runtime cache key inputs in CI', () => {
expect(buildAndMakeWorkflowSource).toContain(
"const targetPlatform = '${{ matrix.embedded_mpv_platform }}';"
@@ -907,7 +928,13 @@ describe('Embedded MPV native build configuration', () => {
linuxHelperConfig?.libraries.find((library: string) =>
library.includes("path.join(dir, 'libmpv.so')")
)
).toContain('LINUX_NATIVE_LIBRARY_DIR');
).toContain('LINUX_VERIFIED_RUNTIME_LIBRARY_DIR');
expect(JSON.stringify(linuxHelperConfig)).not.toContain(
"LINUX_VERIFIED_RUNTIME_LIBRARY_DIR || '/usr/lib'"
);
expect(JSON.stringify(linuxHelperConfig)).toContain(
'Missing LINUX_VERIFIED_RUNTIME_LIBRARY_DIR'
);
const runtimeLinkerFlags = linuxHelperConfig?.ldflags.filter(
(flag: string) => flag.startsWith('-Wl,')
+1
View File
@@ -3,6 +3,7 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"esModuleInterop": true,
"allowJs": true,
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]