fix(packaging): expose bounded helper probe details

This commit is contained in:
4gray committed 2026-07-18 12:54:50 +02:00
1 parent 4302aa4058
commit a575af78bf
10 files changed
+122 -22

No files matched your search

+2
View File
@@ -999,6 +999,8 @@ jobs:
'$2 == "iptvnator:graphics-core22" && $3 == "mesa-core22:graphics-core22" { found=1 } END { exit !found }'
snap connections iptvnator | awk \
'$2 == "iptvnator:gnome-3-28-1804" && $3 == "gnome-3-28-1804:gnome-3-28-1804" { found=1 } END { exit !found }'
snap connections iptvnator | awk \
'$1 == "shared-memory" && $2 == "iptvnator:shared-memory" && $3 == ":shared-memory" { found=1 } END { exit !found }'
xvfb-run -a env \
LIBGL_ALWAYS_SOFTWARE=1 \
__EGL_VENDOR_LIBRARY_FILENAMES=/tmp/hostile-egl-vendor.json \
+4 -2
View File
@@ -278,8 +278,10 @@ Key files:
runs the complete cached manifest/hash/helper gate before BrowserWindow
startup and exits with one availability JSON line. A nonzero helper exit
keeps top-level reason `helper-probe-failed`; `helperReason` is present only
for an exact protocol-v1 line carrying a fixed allowlisted reason. Any loader
failure remains a stable native-view fallback, never a flag-enabled success.
for an exact protocol-v1 line carrying a fixed allowlisted reason, and its
optional `helperDetail` must be 1–1024 printable ASCII characters. Invalid
detail suppresses both helper fields. Any loader failure remains a stable
native-view fallback, never a flag-enabled success.
- In the exact packaged Flatpak `/app` context, reconstruct only Freedesktop
Platform 24.08's immutable `__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS`; its GL
extension loader path comes from the sandbox cache. Flatpak CI must invoke
+3 -2
View File
@@ -670,8 +670,9 @@ engine` (restart required) or
before BrowserWindow startup and emits one availability JSON line. A nonzero
helper exit keeps top-level reason `helper-probe-failed`; `helperReason` is
present only for an exact protocol-v1 line carrying a fixed allowlisted
reason. The exact packaged Flatpak `/app` context reconstructs only
Freedesktop Platform 24.08's immutable
reason, and its optional `helperDetail` must be 1–1024 printable ASCII
characters. Invalid detail suppresses both helper fields. The exact packaged
Flatpak `/app` context reconstructs only Freedesktop Platform 24.08's immutable
`__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS`; its CI smoke invokes that
application-level probe instead of the helper directly. Bundled
Linux packages carry hash-validated
@@ -134,6 +134,10 @@ describe('embedded-mpv frame-copy helper failures', () => {
'shared-memory-initialize-failed',
])('preserves the allowlisted helper reason %s', (helperReason) => {
const fixture = createFixture(context.rootDir);
const helperDetail =
helperReason === 'mpv-create-failed'
? undefined
: 'EGL initialization failed: display unavailable';
context.spawnRuntimeProbe.mockReturnValue({
status: 1,
signal: null,
@@ -141,11 +145,7 @@ describe('embedded-mpv frame-copy helper failures', () => {
protocol: 1,
usable: false,
reason: helperReason,
...(helperReason === 'mpv-create-failed'
? {}
: {
detail: 'diagnostic detail is intentionally not propagated',
}),
...(helperDetail ? { detail: helperDetail } : {}),
})}\n`,
stderr: '',
});
@@ -154,6 +154,32 @@ describe('embedded-mpv frame-copy helper failures', () => {
usable: false,
reason: 'helper-probe-failed',
helperReason,
...(helperDetail ? { helperDetail } : {}),
});
});
it.each([
['one printable ASCII character', 'x'],
['1024 printable ASCII characters', 'x'.repeat(1024)],
])('preserves %s as helper detail', (_label, helperDetail) => {
const fixture = createFixture(context.rootDir);
context.spawnRuntimeProbe.mockReturnValue({
status: 1,
signal: null,
stdout: `${JSON.stringify({
protocol: 1,
usable: false,
reason: 'gl-context-create-failed',
detail: helperDetail,
})}\n`,
stderr: '',
});
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'helper-probe-failed',
helperReason: 'gl-context-create-failed',
helperDetail,
});
});
@@ -197,4 +223,34 @@ describe('embedded-mpv frame-copy helper failures', () => {
reason: 'helper-probe-failed',
});
});
it.each([
['empty', ''],
['control character', 'EGL\tfailure'],
['trailing line feed', 'EGL failure\n'],
['DEL character', `EGL${String.fromCharCode(0x7f)}failure`],
['non-ASCII character', 'EGL échoué'],
['1025 characters', 'x'.repeat(1025)],
])(
'does not propagate any helper fields from %s detail',
(_label, detail) => {
const fixture = createFixture(context.rootDir);
context.spawnRuntimeProbe.mockReturnValue({
status: 1,
signal: null,
stdout: `${JSON.stringify({
protocol: 1,
usable: false,
reason: 'gl-context-create-failed',
detail,
})}\n`,
stderr: '',
});
expect(context.createProbe()(fixture.helperPath)).toEqual({
usable: false,
reason: 'helper-probe-failed',
});
}
);
});
@@ -31,9 +31,21 @@ const HELPER_RUNTIME_PROBE_FAILURE_REASON_ALLOWLIST = new Set<string>(
Object.values(EMBEDDED_MPV_HELPER_RUNTIME_PROBE_FAILURE_REASONS)
);
function parseFailedProbeReason(
stdout: unknown
): EmbeddedMpvHelperRuntimeProbeFailureReason | null {
interface ParsedFailedProbe {
helperReason: EmbeddedMpvHelperRuntimeProbeFailureReason;
helperDetail?: string;
}
function isSafeHelperDetail(value: unknown): value is string {
return (
typeof value === 'string' &&
value.length >= 1 &&
value.length <= 1024 &&
!/[^\x20-\x7e]/.test(value)
);
}
function parseFailedProbe(stdout: unknown): ParsedFailedProbe | null {
if (typeof stdout !== 'string' || !/^[^\r\n]+\n$/.test(stdout)) {
return null;
}
@@ -58,12 +70,15 @@ function parseFailedProbeReason(
parsed.usable !== false ||
typeof parsed.reason !== 'string' ||
!HELPER_RUNTIME_PROBE_FAILURE_REASON_ALLOWLIST.has(parsed.reason) ||
(hasDetail &&
(typeof parsed.detail !== 'string' || parsed.detail.length === 0))
(hasDetail && !isSafeHelperDetail(parsed.detail))
) {
return null;
}
return parsed.reason as EmbeddedMpvHelperRuntimeProbeFailureReason;
return {
helperReason:
parsed.reason as EmbeddedMpvHelperRuntimeProbeFailureReason,
...(hasDetail ? { helperDetail: parsed.detail as string } : {}),
};
}
function parseSuccessfulProbe(
@@ -157,12 +172,12 @@ function runHelperProbe(
return failure('helper-probe-signaled');
}
if (result.status !== 0) {
const helperReason = parseFailedProbeReason(result.stdout);
return helperReason
const helperFailure = parseFailedProbe(result.stdout);
return helperFailure
? {
usable: false,
reason: 'helper-probe-failed',
helperReason,
...helperFailure,
}
: failure('helper-probe-failed');
}
@@ -55,6 +55,7 @@ export type EmbeddedMpvFrameCopyRuntimeResult =
usable: false;
reason: EmbeddedMpvFrameCopyRuntimeFailureReason;
helperReason?: EmbeddedMpvHelperRuntimeProbeFailureReason;
helperDetail?: string;
};
export interface EmbeddedMpvFrameCopyRuntimeFileSystem {
@@ -91,6 +91,7 @@ describe('embedded MPV runtime diagnostic', () => {
usable: false,
reason: 'helper-probe-failed',
helperReason: 'gl-context-create-failed',
helperDetail: 'EGL initialization failed: display unavailable',
};
const harness = createHarness(availability);
@@ -101,7 +102,7 @@ describe('embedded MPV runtime diagnostic', () => {
expect(harness.getRuntimeAvailability).toHaveBeenCalledTimes(1);
expect(harness.writeStdout).toHaveBeenCalledWith(
'{"usable":false,"reason":"helper-probe-failed","helperReason":"gl-context-create-failed"}\n'
'{"usable":false,"reason":"helper-probe-failed","helperReason":"gl-context-create-failed","helperDetail":"EGL initialization failed: display unavailable"}\n'
);
expect(harness.writeStdout).toHaveBeenCalledTimes(1);
expect(harness.exit).toHaveBeenCalledWith(1);
+5 -2
View File
@@ -278,8 +278,11 @@ failures emit the stable helper reasons `shared-memory-create-failed` and
JSON line and return zero. When the helper exits nonzero with an otherwise
exact failure line, the application availability diagnostic keeps the
fail-closed top-level reason `helper-probe-failed` and may add only the
allowlisted helper reason as `helperReason`; malformed, multi-line,
wrong-protocol, or unknown failure output never reaches that field.
allowlisted helper reason as `helperReason`. An optional `helperDetail` is
copied only from the same exact line when it contains 1–1024 printable ASCII
characters; an invalid detail rejects both helper fields. Malformed,
multi-line, wrong-protocol, or unknown failure output never reaches either
field.
The startup probe and every playback helper session use the same sanitized
loader environment selected by the validated manifest's cached `runtimeMode`.
+3 -1
View File
@@ -234,7 +234,9 @@ missing file, hash mismatch, unusable graphics path, or shm lifecycle failure
returns a stable reason and keeps the BrowserWindow sandbox enabled. The
application diagnostic retains `helper-probe-failed` as the top-level reason
for nonzero helper exits and adds `helperReason` only when the helper emitted
one exact protocol-v1 line with a fixed allowlisted reason. The
one exact protocol-v1 line with a fixed allowlisted reason. Its optional
`helperDetail` is restricted to 1–1024 printable ASCII characters; invalid
detail suppresses both helper fields. The
installed-Snap probe therefore tests the private shared-memory confinement
needed by playback rather than only loader and graphics startup.
Packaging CI invokes the same gate through
@@ -650,6 +650,10 @@ test('Snap verifier preserves fail-closed status while exposing captured diagnos
snapStep,
/\$2 == "iptvnator:gnome-3-28-1804" && \$3 == "gnome-3-28-1804:gnome-3-28-1804"/
);
assert.match(
snapStep,
/\$1 == "shared-memory" && \$2 == "iptvnator:shared-memory" && \$3 == ":shared-memory"/
);
assert.match(
snapStep,
/sudo snap disconnect iptvnator:graphics-core22 mesa-core22:graphics-core22/
@@ -688,9 +692,22 @@ test('Snap verifier preserves fail-closed status while exposing captured diagnos
const successfulRuntimeProbe = snapStep.lastIndexOf(
'snap run iptvnator --embedded-mpv-runtime-probe'
);
const graphicsConnectionAssertion = snapStep.indexOf(
'$2 == "iptvnator:graphics-core22" && $3 == "mesa-core22:graphics-core22"'
);
const gnomeConnectionAssertion = snapStep.indexOf(
'$2 == "iptvnator:gnome-3-28-1804" && $3 == "gnome-3-28-1804:gnome-3-28-1804"'
);
const sharedMemoryConnectionAssertion = snapStep.indexOf(
'$1 == "shared-memory" && $2 == "iptvnator:shared-memory" && $3 == ":shared-memory"'
);
assert.ok(firstRuntimeProbe < secondGraphicsConnect);
assert.ok(firstRuntimeProbe < successfulRuntimeProbe);
assert.ok(secondGraphicsConnect < successfulRuntimeProbe);
assert.ok(secondGraphicsConnect < graphicsConnectionAssertion);
assert.ok(graphicsConnectionAssertion < gnomeConnectionAssertion);
assert.ok(gnomeConnectionAssertion < sharedMemoryConnectionAssertion);
assert.ok(sharedMemoryConnectionAssertion < successfulRuntimeProbe);
assert.match(snapStep, /snap run iptvnator --embedded-mpv-runtime-probe/);
for (const hostileOverride of [
'__EGL_VENDOR_LIBRARY_FILENAMES',