From f7add4e6994fed119c47c113ac7b32e681d1fa39 Mon Sep 17 00:00:00 2001 From: techartdev Date: Wed, 23 Sep 2026 10:04:55 +0300 Subject: [PATCH 1/2] Verify native app isolation on Linux and macOS --- .github/workflows/native-app-frame-probe.yml | 49 ++++++++++++++++++++ scripts/run-extension-probe.mjs | 5 +- src-tauri/src/extension_frames.rs | 8 ++-- src-tauri/src/native_ipc.rs | 22 +++++++-- 4 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/native-app-frame-probe.yml diff --git a/.github/workflows/native-app-frame-probe.yml b/.github/workflows/native-app-frame-probe.yml new file mode 100644 index 0000000..3356fe7 --- /dev/null +++ b/.github/workflows/native-app-frame-probe.yml @@ -0,0 +1,49 @@ +name: Verify Linux and macOS app frames + +on: + pull_request: + branches: [main] + paths: + - 'src-tauri/src/extension_frames.rs' + - 'src-tauri/src/native_ipc.rs' + - 'scripts/run-extension-probe.mjs' + - 'tests/fixtures/native-frame-probe*' + - '.github/workflows/native-app-frame-probe.yml' + +permissions: + contents: read + +jobs: + native-isolation: + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-22.04 + platform: linux + - runner: macos-15 + platform: macos + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24 + cache: npm + - name: Linux prerequisites + if: matrix.platform == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev libsecret-1-dev xvfb dbus-x11 + - run: npm ci + - name: Build the dedicated native probe + run: npm run tauri -- build --debug --no-bundle --config src-tauri/tauri.extension-probe.conf.json + - name: Exercise app frame isolation + env: + PLATFORM: ${{ matrix.platform }} + run: | + if [ "$PLATFORM" = linux ]; then + dbus-run-session -- xvfb-run -a node scripts/run-extension-probe.mjs + else + node scripts/run-extension-probe.mjs + fi diff --git a/scripts/run-extension-probe.mjs b/scripts/run-extension-probe.mjs index 338653d..def55ba 100644 --- a/scripts/run-extension-probe.mjs +++ b/scripts/run-extension-probe.mjs @@ -7,7 +7,8 @@ import { resolve } from "node:path"; const root = fileURLToPath(new URL("../", import.meta.url)); const result = resolve(root, ".local/native-extension-probe/result.json"); await rm(result, { force: true }); -const child = spawn(resolve(root, "target/debug/shellcanvas.exe"), [], { +const binary = resolve(root, `target/debug/shellcanvas${process.platform === "win32" ? ".exe" : ""}`); +const child = spawn(binary, [], { cwd: root, windowsHide: true, env: { ...process.env, SHELLCANVAS_EXTENSION_PROBE: "1" }, @@ -21,7 +22,7 @@ try { }); const report = JSON.parse(await readFile(result, "utf8")); console.log(JSON.stringify(report, null, 2)); - // The structured report is authoritative; Windows GUI exit codes are insufficient. + // The structured report is authoritative; GUI exit codes are insufficient. if (report.success !== true) { console.error( await readFile( diff --git a/src-tauri/src/extension_frames.rs b/src-tauri/src/extension_frames.rs index 89ebe13..f566535 100644 --- a/src-tauri/src/extension_frames.rs +++ b/src-tauri/src/extension_frames.rs @@ -142,9 +142,6 @@ pub fn publish_app_frame( style: String, instance_token: String, ) -> Result { - if !cfg!(windows) { - return Err("Native runtime app frames are not yet verified on this platform.".into()); - } if webview.label() != "main" { return Err("Only the desktop can create app frames.".into()); } @@ -161,8 +158,9 @@ pub fn release_app_frame( pub fn plugin() -> TauriPlugin { tauri::plugin::Builder::new("runtime-app-documents") - // Wry's Windows navigation callback handles the top-level WebView. Never promote an - // app resource into that privileged document. Other platforms remain gated above. + // Wry's Windows navigation callback handles the top-level WebView. The + // IPC transport also checks the trusted desktop origin before creating + // its invocation key on every platform. .on_navigation(|_, url| { !cfg!(windows) || (url.scheme() != SCHEME && url.host_str() != Some("shellcanvas-app.localhost")) diff --git a/src-tauri/src/native_ipc.rs b/src-tauri/src/native_ipc.rs index 59a8767..e6dca7e 100644 --- a/src-tauri/src/native_ipc.rs +++ b/src-tauri/src/native_ipc.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MPL-2.0 -//! Preserve Tauri's IPC transport, but never initialize its secret-bearing closure in a subframe. +//! Preserve Tauri's IPC transport, but initialize its secret-bearing closure +//! only in the desktop's own top-level document. pub fn initialization_script() -> String { let transport = include_str!("../vendor/tauri-ipc/ipc-protocol.js") .replace("__TEMPLATE_invoke_key__", "__INVOKE_KEY__") @@ -16,7 +17,20 @@ pub fn initialization_script() -> String { // Private Tauri 2.11.5 wire constant, kept with the matching vendored templates. "\"plugin:__TAURI_CHANNEL__|fetch\"", ); - // WebView2 ignores Wry's main-frame-only flag. This check runs before app code, and - // Window.top is browser-owned. Keep the invocation key inside the guarded closure. - format!("if (window === window.top) {{\n{transport}\n}}") + // WebView2 ignores Wry's main-frame-only flag. On WebKit, an app document + // could also become the top-level page after a navigation. Check both the + // browser-owned frame identity and the exact trusted desktop origin before + // constructing the invocation-key closure. + let trusted = if cfg!(dev) { + "http://127.0.0.1:1420" + } else { + if cfg!(windows) { + "http://tauri.localhost" + } else { + "tauri://localhost" + } + }; + format!( + "if (window === window.top && window.location.protocol + '//' + window.location.host === {trusted:?}) {{\n{transport}\n}}" + ) } From d348423495b52d7b3d528d32754ce5dd54f4ade6 Mon Sep 17 00:00:00 2001 From: techartdev Date: Wed, 23 Sep 2026 10:13:46 +0300 Subject: [PATCH 2/2] Serve installed app frames through each platform's native scheme --- .github/workflows/native-app-frame-probe.yml | 2 +- src-tauri/src/extension_frames.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/native-app-frame-probe.yml b/.github/workflows/native-app-frame-probe.yml index 3356fe7..1dbdd03 100644 --- a/.github/workflows/native-app-frame-probe.yml +++ b/.github/workflows/native-app-frame-probe.yml @@ -43,7 +43,7 @@ jobs: PLATFORM: ${{ matrix.platform }} run: | if [ "$PLATFORM" = linux ]; then - dbus-run-session -- xvfb-run -a node scripts/run-extension-probe.mjs + xvfb-run -a dbus-run-session -- node scripts/run-extension-probe.mjs else node scripts/run-extension-probe.mjs fi diff --git a/src-tauri/src/extension_frames.rs b/src-tauri/src/extension_frames.rs index f566535..22cd84f 100644 --- a/src-tauri/src/extension_frames.rs +++ b/src-tauri/src/extension_frames.rs @@ -74,7 +74,11 @@ impl FrameDocuments { } documents.insert(id.clone(), document); Ok(FrameLocation { - url: format!("http://{SCHEME}.localhost/{id}/index.html"), + url: if cfg!(windows) { + format!("http://{SCHEME}.localhost/{id}/index.html") + } else { + format!("{SCHEME}://localhost/{id}/index.html") + }, id, }) } @@ -191,6 +195,11 @@ mod tests { "body{color:red}".into(), ) .unwrap(); + assert!(one.url.starts_with(if cfg!(windows) { + "http://shellcanvas-app.localhost/" + } else { + "shellcanvas-app://localhost/" + })); let other = documents .publish( "main",