From 08e92d4f276764d8c1ba869f0bdc2a1e8d89f45f Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 17 Jul 2026 21:31:30 +0200 Subject: [PATCH] fix(embedded-mpv): enable private Snap shared memory --- .../native/helper/mpv_frame_helper.cpp | 39 ++++++++++++++- .../embedded-mpv-native-source.spec.ts | 49 ++++++++++++++++++- docs/architecture/embedded-mpv-native.md | 20 ++++++-- electron-builder.json | 9 ++++ tools/embedded-mpv/README.md | 14 ++++-- 5 files changed, 120 insertions(+), 11 deletions(-) diff --git a/apps/electron-backend/native/helper/mpv_frame_helper.cpp b/apps/electron-backend/native/helper/mpv_frame_helper.cpp index 60b484cc9..99a9f1626 100644 --- a/apps/electron-backend/native/helper/mpv_frame_helper.cpp +++ b/apps/electron-backend/native/helper/mpv_frame_helper.cpp @@ -653,10 +653,20 @@ std::string libmpvClientApiVersion() { std::to_string(version & 0xffff); } +std::string runtimeProbeShmName() { +#if defined(_WIN32) + const uint64_t processId = (uint64_t)GetCurrentProcessId(); +#else + const uint64_t processId = (uint64_t)getpid(); +#endif + return "/impv-fc-runtime-probe-" + std::to_string(processId); +} + /* * Bounded startup capability probe: initialize an idle libmpv client and - * create the platform GL + mpv OpenGL render contexts. It deliberately does - * not create an FBO/shm ring, open media, or enter either command loop. + * create the platform GL + mpv OpenGL render contexts, then create, validate, + * and destroy a minimal shared-memory ring. It deliberately does not create + * an FBO, open media, or enter either command loop. */ int runRuntimeProbe() { mpv_handle* mpv = mpv_create(); @@ -711,6 +721,31 @@ int runRuntimeProbe() { return runtimeProbeFailure("mpv-render-context-failed", renderError); } + frame_helper::ShmRing runtimeProbeRing; + const std::string shmName = runtimeProbeShmName(); + if (!runtimeProbeRing.create(shmName, 16, 16, 1)) { + runtimeProbeRing.destroy(); + mpv_render_context_free(renderContext); + gl.destroy(); + mpv_terminate_destroy(mpv); + return runtimeProbeFailure("shared-memory-create-failed"); + } + const bool sharedMemoryInitialized = + runtimeProbeRing.base != nullptr && + runtimeProbeRing.header != nullptr && + runtimeProbeRing.header->magic == FRAME_SHM_MAGIC && + runtimeProbeRing.header->version == FRAME_SHM_VERSION && + runtimeProbeRing.header->width == 16 && + runtimeProbeRing.header->height == 16 && + runtimeProbeRing.header->generation == 1; + runtimeProbeRing.destroy(); + if (!sharedMemoryInitialized) { + mpv_render_context_free(renderContext); + gl.destroy(); + mpv_terminate_destroy(mpv); + return runtimeProbeFailure("shared-memory-initialize-failed"); + } + const std::string libmpvVersion = libmpvClientApiVersion(); mpv_render_context_free(renderContext); gl.destroy(); diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts b/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts index fe12e6bc9..e5c274e6c 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts @@ -32,6 +32,16 @@ describe('Embedded MPV native source recording invariants', () => { ), 'utf8' ); + const electronBuilderConfig = JSON.parse( + readFileSync( + path.resolve(__dirname, '../../../../../electron-builder.json'), + 'utf8' + ) + ) as { + snap?: { + plugs?: unknown; + }; + }; const stageRuntimeSource = readFileSync( path.resolve( __dirname, @@ -778,12 +788,29 @@ describe('Embedded MPV native source recording invariants', () => { ); }); - it('runs the helper runtime probe without shared memory, media, or command loops', () => { + it('keeps Electron Builder defaults and requests private Snap shared memory', () => { + expect(electronBuilderConfig.snap?.plugs).toEqual([ + 'default', + { + 'shared-memory': { + interface: 'shared-memory', + private: true, + }, + }, + ]); + }); + + it('runs the helper runtime probe through shared memory without media or command loops', () => { const runtimeProbe = sourceFunctionBody( frameHelperSource, 'int runRuntimeProbe(', 'runRuntimeProbe' ); + const runtimeProbeShmName = sourceFunctionBody( + frameHelperSource, + 'std::string runtimeProbeShmName(', + 'runtimeProbeShmName' + ); const main = sourceFunctionBody(frameHelperSource, 'int main(', 'main'); const runtimeProbeFailure = sourceFunctionBody( frameHelperSource, @@ -804,12 +831,30 @@ describe('Embedded MPV native source recording invariants', () => { expect(runtimeProbe).toContain('mpv_render_context_free('); expect(runtimeProbe).toContain('gl.destroy()'); expect(runtimeProbe).toContain('mpv_terminate_destroy(mpv)'); + expect(runtimeProbe).toContain( + 'frame_helper::ShmRing runtimeProbeRing;' + ); + expect(runtimeProbe).toContain( + 'const std::string shmName = runtimeProbeShmName();' + ); + expect(runtimeProbe).toContain( + 'runtimeProbeRing.create(shmName, 16, 16, 1)' + ); + expect(runtimeProbe).toContain( + 'runtimeProbeRing.header->magic == FRAME_SHM_MAGIC' + ); + expect(runtimeProbe).toContain('runtimeProbeRing.destroy();'); + expect(runtimeProbe).toContain('"shared-memory-create-failed"'); + expect(runtimeProbe).toContain('"shared-memory-initialize-failed"'); + expect(runtimeProbeShmName).toContain('"/impv-fc-runtime-probe-"'); + expect(runtimeProbeShmName).toContain('getpid()'); + expect(runtimeProbeShmName).toContain('GetCurrentProcessId()'); + expect(runtimeProbeShmName).toContain('std::to_string(processId)'); expect(runtimeProbe).toContain('.num("protocol", 1)'); expect(runtimeProbe).toContain('.boolean("usable", true)'); expect(runtimeProbe).toContain('.str("libmpv",'); expect(runtimeProbe).toContain('.str("renderApi", gl.renderApiName())'); expect(runtimeProbe).not.toContain('pipeline'); - expect(runtimeProbe).not.toContain('shm'); expect(runtimeProbe).not.toContain('runStdinLoop'); expect(runtimeProbe).not.toContain('runMpvEventLoop'); expect(runtimeProbe).not.toContain('loadfile'); diff --git a/docs/architecture/embedded-mpv-native.md b/docs/architecture/embedded-mpv-native.md index 78627ddb4..1c8ca768f 100644 --- a/docs/architecture/embedded-mpv-native.md +++ b/docs/architecture/embedded-mpv-native.md @@ -241,10 +241,22 @@ has succeeded. On Linux x64 that decision validates the profile manifest, regular-file/access modes, the complete declared bundled closure and hashes, then runs `iptvnator_mpv_helper --runtime-probe` with a three-second timeout. The probe loads dependencies through the normal ELF loader, initializes an -idle libmpv client, and creates EGL/OpenGL plus mpv render contexts without -opening media or shared memory. It must emit exactly one protocol-v1 JSON line -and return zero. Bundled profiles prepend only their packaged `native/lib` to -`LD_LIBRARY_PATH`; the system profile never injects a private loader path. +idle libmpv client, creates EGL/OpenGL plus mpv render contexts, then +creates, maps, validates, and destroys a minimal `16x16` shared-memory ring +named `/impv-fc-runtime-probe-`. It never opens media or enters the media +or command loops. Shared-memory creation/mapping and header-initialization +failures emit the stable helper reasons `shared-memory-create-failed` and +`shared-memory-initialize-failed`. The probe must emit exactly one protocol-v1 +JSON line and return zero. Bundled profiles prepend only their packaged +`native/lib` to `LD_LIBRARY_PATH`; the system profile never injects a private +loader path. + +The strict Snap keeps Electron Builder's default plugs and adds an +auto-connected private `shared-memory` plug. Private shared memory gives the +app a confined, snap-specific POSIX shm namespace rather than global +cross-snap access. Consequently the installed-Snap `--runtime-probe` validates +the actual confinement and shm lifecycle required by frame-copy, not only the +loader and graphics contexts. Packaged discovery is limited to packaged resource locations and never falls through to writable cwd/dist development paths. A disabled base experiment or any failed capability check keeps the renderer sandbox enabled and falls back diff --git a/electron-builder.json b/electron-builder.json index 4e0306951..7d4cb2887 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -139,6 +139,15 @@ "grade": "stable", "summary": "IPTV application for M3U playlists, Xtream Codes API, and Stalker portals", "executableArgs": ["--ozone-platform=x11"], + "plugs": [ + "default", + { + "shared-memory": { + "interface": "shared-memory", + "private": true + } + } + ], "environment": { "DISABLE_WAYLAND": "1" } diff --git a/tools/embedded-mpv/README.md b/tools/embedded-mpv/README.md index b5871a4a4..5cd25f366 100644 --- a/tools/embedded-mpv/README.md +++ b/tools/embedded-mpv/README.md @@ -114,6 +114,10 @@ The DEB metadata requires `libmpv2` and is release-tested on Ubuntu 24.04 (Noble). Ubuntu 22.04 (Jammy) only provides `libmpv1`; use the x64 AppImage on that distribution rather than relaxing the runtime contract. +The strict Snap retains Electron Builder's default plugs and adds an +auto-connected private `shared-memory` plug. This supplies a snap-specific +POSIX shm namespace without granting global cross-snap shared-memory access. + Profiles cannot share one Electron Builder pass because its targets reuse the same unpacked application directory. A missing or unsupported profile, or a target from another profile, fails packaging. @@ -147,9 +151,13 @@ iptvnator_mpv_helper --runtime-probe ``` The bounded probe initializes idle libmpv plus EGL/OpenGL and mpv render -contexts without media or shared memory. A timeout, loader failure, malformed -protocol, missing file, hash mismatch, or unusable graphics path returns a -stable reason and keeps the BrowserWindow sandbox enabled. +contexts, then creates, maps, validates, and destroys a minimal `16x16` +shared-memory ring named `/impv-fc-runtime-probe-`. It does not open media +or enter media/command loops. A timeout, loader failure, malformed protocol, +missing file, hash mismatch, unusable graphics path, or shm lifecycle failure +returns a stable reason and keeps the BrowserWindow sandbox enabled. The +installed-Snap probe therefore tests the private shared-memory confinement +needed by playback rather than only loader and graphics startup. ## CI And Source Distribution