chainlesschain 0.162.181 → 0.162.182
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/scripts/build-windows-sandbox-helper.ps1 +163 -0
- package/src/assets/web-panel/assets/{AIOps-cq0dwbnf.js → AIOps-DzZ5_xoj.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CvcgGl3w.js → ActionButton-DeLXddIg.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-hM2-jFqg.js → Analytics-rvDCmet1.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-B9_Gsd_g.js → AppLayout-Yt6QVDnp.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-B7bu52Og.js → Artifacts-BS112K5L.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DThfDgqB.js → Audit-0wJuQrNu.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DjlFiD4v.js → BackgroundAgents-B90PMgbn.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CK9xJKnG.js → Backup-TkgMglQQ.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DEiSU8K8.js → BaseInput-CQVS9p1l.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CpT96czj.js → Chat-YTjQHoC7.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BKPPNmZI.js → ChatBubbleRenderer-PR074hUN.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-C1GfDnO0.js → Checkbox-5bwaOabq.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-3DAKgiLx.js → Codegen-BFmNQ_Xe.js} +1 -1
- package/src/assets/web-panel/assets/{Col-74cOwZU1.js → Col-BT_u4Dzp.js} +1 -1
- package/src/assets/web-panel/assets/{Community-tn9Kkz9j.js → Community-CJP52u5b.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BcoXH7m4.js → Compact-BpzhJE9i.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DIPGUBgi.js → Compliance-Cg7rfslp.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-doD5_91c.js → Cowork-JisxL6Yy.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-C-IDvubz.js → Cron-Bu7Ug_MC.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-CdcCoBdj.js → Crosschain-CvuqoLdj.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DdH5aMMA.js → DID-C2u9G8Pu.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DHLA4mm0.js → Dashboard-BFt5Yv3d.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-dEeCt7Md.js → Dropdown-DIaT9Wxq.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BMaecFv9.js → EmailListRenderer-D_A601Y4.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DUaUukTN.js → FamilyGuardDashboard--IyQ8I8w.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CMBE0_rp.js → Federation-BihWHxuY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-PG-JDbXq.js → FormItemContext-BB8dOXc_.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-Bcb0p88m.js → GenericCardRenderer-DQYhj1HH.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Bgv15myB.js → Git-B-2QZxGf.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DB8YTArI.js → Governance-CusSb-nG.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BQMwCWaS.js → Inference-BAjmvPPL.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-B1_ZziiB.js → KnowledgeGraph-DoNYWKYq.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DovEEnt_.js → Logs-B8fKWD-n.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-D201OL4T.js → MarkdownRenderer-BlJm1J7E.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BlCXSpyB.js → Marketplace-CyOscbSL.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CsCR09Wj.js → McpTools-Ex6Om5da.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-CBVRLKno.js → Memory-CDcD8l8r.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-C9OIafgA.js → MobileBridge-xfBqoFDg.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-D_zoJ8Xs.js → MobileProjects-BwObMMA2.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-vE-HdvLj.js → Mtc-CIWd36OV.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-CPLaekCd.js → MtcAudit-CPX0ouuA.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-C_UP9C2o.js → Multisig-BDJsIUuW.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-2nODJcU1.js → NLProgramming-DEkbuD3N.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BhE-SJGu.js → Notes-DJpHRhH1.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-D21q7BNU.js → NotificationSettings-C1UZ-Qd5.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-0b62GYOD.js → OrderTableRenderer-GlJtqLjT.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CEqmw_sb.js → Organization-Bbhj0fac.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-MW-gMDO_.js → Overflow-1H04x3kx.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CY0hjI_G.js → P2P-CDpPe3et.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BbWh7Man.js → PdhVaultBrowser-MUX0Bpvw.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-ChqEA_4Q.js → Permissions-A07-0Ci1.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-B3YrbYfD.js → PersonalDataHub-DhhDWxAZ.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-B8WBhhlW.js → Pipeline-CyfbJfe-.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Cmb0PXNx.js → Privacy-CNFps3ED.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-hF6zW3Nz.js → ProjectInit--T-u3Dr5.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BFSVfpW0.js → ProjectSettings-DJmrK2fC.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-DAWkVFkc.js → Projects-Qx_MdX9k.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-B8yux8Dx.js → Providers-DLzzbOZd.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-N76cQluO.js → QrScannerModal-CexOomKS.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-ClvZ0KYR.js → QuickAsk-QklGi19_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-B-GfgBpT.js → Recommend-Bb9kwSw8.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-mlglCdQi.js → RemoteSession-DZuVNNN5.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DJIZzQeG.js → Reputation-Dr5cLP0N.js} +1 -1
- package/src/assets/web-panel/assets/{Row-COzxu_bb.js → Row-oCQHYv5t.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CoYsJQuE.js → RssFeed-IFDEykIR.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DC77Mq9S.js → Search-CspVIxDc.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Loy2EdMO.js → Security-Bm66o_DH.js} +3 -3
- package/src/assets/web-panel/assets/{Services-GUD07Eu0.js → Services-DvanNqXl.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DWzkptYU.js → Skeleton-CHjEe9_O.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CEDn1r-Z.js → Skills-Cb8jWjMX.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-ub_k4pXh.js → Sla-DxZm1491.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-D7HLX9CA.js → SpeechSettings-DWxEEclC.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DbwOvl6m.js → SyncSettings-FIvOFw5p.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CBQzoAu5.js → Tasks-CyuLnDFR.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BNVeIhud.js → Templates-CaNjzV_t.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-PS9AzhXU.js → Tenant-DjlP6TmH.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-DwzGMniO.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-CrmXMOkg.js → TimelineRenderer-DoZ8y7y8.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DD-VDHw1.js → Tokens-390cg2TC.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BsoN_n-n.js → Trigger-CLrtCj5_.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-n8LmN4ZV.js → Trust-DpESfSPn.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-C9_7MImA.js → UkeySign-CN6279O0.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DyBteNgn.js → VideoEditing-DPtAUlSe.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BS5ixeXI.js → Wallet-nKOMV_1n.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-spc4UImU.js → WebAuthn-HuXlSOc4.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-D7iVvaBO.js → WorkflowEditor-CUfFjARi.js} +1 -1
- package/src/assets/web-panel/assets/{chat-6CmBkHrL.js → chat-6Eq0PcZJ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BGoCLkxI.js → colors-BP_Sf6Iy.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-fYZBetMO.js → compact-item-BufMKIh5.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BeM-1UVj.js → createContext-BBMXxWSW.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DgVKduH2.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CE7ZuEnV.js → hasIn-fUK_tRO2.js} +1 -1
- package/src/assets/web-panel/assets/{index-QIpnpZgz.js → index-2A5ph8b2.js} +1 -1
- package/src/assets/web-panel/assets/{index-BU_2qiDk.js → index-3FuwBewR.js} +1 -1
- package/src/assets/web-panel/assets/{index--lap6cf7.js → index-6GmBa4Kg.js} +1 -1
- package/src/assets/web-panel/assets/{index-De0quFDw.js → index-B2vS66eI.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1r9tT3J.js → index-B7Etk0Te.js} +1 -1
- package/src/assets/web-panel/assets/{index-YjcumOZ4.js → index-B9WYSn4m.js} +1 -1
- package/src/assets/web-panel/assets/{index-MtOuq2i6.js → index-BHy6yNiF.js} +1 -1
- package/src/assets/web-panel/assets/{index-RMZ8a7Yt.js → index-BMLHv389.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0v6YKKO.js → index-BWKoBU8R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRnUE8Cb.js → index-Bkg4ZePE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsMfoaeo.js → index-C44atv4k.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoXk5KJ8.js → index-CE2DdTP2.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4wiTEOM.js → index-CGjmILDm.js} +1 -1
- package/src/assets/web-panel/assets/{index-zb7rVOhV.js → index-CW_G-MXw.js} +1 -1
- package/src/assets/web-panel/assets/{index-ATcSr_2O.js → index-CZ6Yt17L.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9Y3k0HI.js → index-CeASi4fr.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2U-ObMq.js → index-CgiECHu2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Puq6Luf1.js → index-Cp8vhyZy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BV551878.js → index-CwSf5pnf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BowH1eLQ.js → index-D4_MuGkd.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3CDgvNW.js → index-D9vU25ij.js} +1 -1
- package/src/assets/web-panel/assets/{index-WUrJ6S1j.js → index-DASluSqY.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRLjEP1H.js → index-DLpIs_P_.js} +1 -1
- package/src/assets/web-panel/assets/index-DOZTSoS7.js +1 -0
- package/src/assets/web-panel/assets/{index-DT3zPSVX.js → index-DPdUNk1i.js} +1 -1
- package/src/assets/web-panel/assets/{index-BunaQCa0.js → index-DVsqvI4M.js} +1 -1
- package/src/assets/web-panel/assets/index-DhpvjpqI.js +1 -0
- package/src/assets/web-panel/assets/{index-CIpVrf5P.js → index-DmkMXwnR.js} +1 -1
- package/src/assets/web-panel/assets/{index-lJBjB1QX.js → index-DocdRcbd.js} +1 -1
- package/src/assets/web-panel/assets/{index-BEVm0lCS.js → index-Dyngbd29.js} +19 -19
- package/src/assets/web-panel/assets/{index-C4SJVjs4.js → index-GwdRUZ-x.js} +1 -1
- package/src/assets/web-panel/assets/{index-ukg99D1V.js → index-LvCBfTLo.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfPL-m46.js → index-cpEli44X.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpIGIB85.js → index-fzaMtSJ8.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNZDILcO.js → index-pum7HYBA.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dw1PyYPm.js → index-t8N1tSeX.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cip8emOQ.js → index-uCbEN7cY.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsvfrESp.js → index-wKTLbKyc.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8BsdNV_.js → index-wV6iGdpY.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CYHMs1ag.js → initDefaultProps-V5Fo95FM.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DDB2Ffk5.js → motion-BYWU0hUT.js} +1 -1
- package/src/assets/web-panel/assets/{move-Bg3cD-dX.js → move-Cr1M-J2h.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-B5nedjTS.js → mtc-parser-BysQKi2N.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BHyuUILu.js → omit-D1zHU8GC.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-C9YQnEN9.js → pickAttrs-CUArxtfi.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DBUFYUe7.js → placementArrow-ChoYEys0.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-nBs2Obnk.js → responsiveObserve-BQRs3Mo7.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DkCEaXNU.js → slide-B8zbsCkg.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-aqE1Wi6U.js → statusUtils-C2U8Gs6h.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CN5bhfHB.js → styleChecker-Dd8MgL_V.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-B_oX_kvJ.js → useFlexGapSupport-hknqY5NH.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-rCuXHeme.js → useFs-CLTAmcfw.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DZR9Abyd.js → usePersonalDataHub-ByegHbiB.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-ConCYQlX.js → vnode-DqlAy5DP.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CUdDxG5M.js → zoom-DiXiztQn.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +115 -3
- package/src/commands/agent.js +6 -0
- package/src/data/changelog.json +13 -0
- package/src/gateways/terminal/PtyManager.js +45 -4
- package/src/gateways/ws/ws-server.js +10 -1
- package/src/harness/background-task-manager.js +70 -2
- package/src/harness/worktree-isolator.js +14 -3
- package/src/lib/agent-core.js +1 -0
- package/src/lib/agent-schedule-store.js +45 -22
- package/src/lib/hook-runner.js +6 -2
- package/src/lib/packer/pkg-config-generator.js +3 -1
- package/src/lib/plugin-runtime/bin.js +1148 -70
- package/src/lib/plugin-runtime/manifest.js +68 -12
- package/src/lib/plugin-runtime/scopes.js +20 -10
- package/src/lib/process-execution-broker/index.js +375 -48
- package/src/lib/process-execution-broker/platform-sandbox.js +3831 -276
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +3856 -0
- package/src/lib/repl-bang-memorize.js +67 -10
- package/src/lib/slash-commands.js +27 -4
- package/src/repl/agent-repl.js +4 -3
- package/src/runtime/agent-core.js +376 -52
- package/src/runtime/agent-runtime.js +13 -1
- package/src/runtime/headless-runner.js +4 -3
- package/src/assets/web-panel/assets/Terminal-Cgb4eLB_.js +0 -3
- package/src/assets/web-panel/assets/devWarning-BzPwHnw8.js +0 -1
- package/src/assets/web-panel/assets/index-DacrAdc-.js +0 -1
- package/src/assets/web-panel/assets/index-EHzNJozo.js +0 -1
- package/src/lib/process-execution-broker/windows-sandbox.ps1 +0 -999
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Platform enforcement currently available:
|
|
5
5
|
* - macOS: Seatbelt sandbox-exec profiles
|
|
6
|
-
* - Linux: prlimit resource-limit wrapper, plus a
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* - Linux: prlimit resource-limit wrapper, plus a narrow bubblewrap backend
|
|
7
|
+
* for an attested direct strict Plugin Node bin or static ELF native bin.
|
|
8
|
+
* Shells, dynamically linked binaries, broader working directories,
|
|
9
|
+
* host-writable paths, inherited descriptors, and network egress remain
|
|
10
|
+
* fail-closed.
|
|
11
|
+
* - Windows: Win32 Job Object + restricted-token wrapper, with an attested
|
|
12
|
+
* zero-capability AppContainer for explicit filesystem/network boundaries
|
|
9
13
|
*
|
|
10
14
|
* Security model:
|
|
11
15
|
* Adapters return a truthful spawn plan. An unavailable primitive is never
|
|
@@ -55,40 +59,187 @@ export const SANDBOX_BOUNDARIES = Object.freeze({
|
|
|
55
59
|
*/
|
|
56
60
|
|
|
57
61
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
const WINDOWS_SANDBOX_HELPER_SOURCE = fs.readFileSync(
|
|
63
|
+
path.join(MODULE_DIR, "windows-sandbox.cs"),
|
|
64
|
+
);
|
|
65
|
+
const WINDOWS_SANDBOX_HELPER_SOURCE_DIGEST = crypto
|
|
66
|
+
.createHash("sha256")
|
|
67
|
+
.update(WINDOWS_SANDBOX_HELPER_SOURCE)
|
|
68
|
+
.digest("hex");
|
|
69
|
+
const WINDOWS_SANDBOX_HELPER_ASSEMBLY = fs.readFileSync(
|
|
70
|
+
path.join(MODULE_DIR, "windows-sandbox-helper.dll"),
|
|
71
|
+
);
|
|
72
|
+
const WINDOWS_SANDBOX_HELPER_ASSEMBLY_DIGEST = crypto
|
|
73
|
+
.createHash("sha256")
|
|
74
|
+
.update(WINDOWS_SANDBOX_HELPER_ASSEMBLY)
|
|
75
|
+
.digest("hex");
|
|
76
|
+
const WINDOWS_SANDBOX_HELPER_EXECUTABLE = fs.readFileSync(
|
|
77
|
+
path.join(MODULE_DIR, "windows-sandbox-helper.exe"),
|
|
78
|
+
);
|
|
79
|
+
const WINDOWS_SANDBOX_HELPER_EXECUTABLE_DIGEST = crypto
|
|
80
|
+
.createHash("sha256")
|
|
81
|
+
.update(WINDOWS_SANDBOX_HELPER_EXECUTABLE)
|
|
82
|
+
.digest("hex");
|
|
83
|
+
const WINDOWS_TRUSTED_SYSTEM_ROOT =
|
|
84
|
+
os.platform() === "win32"
|
|
85
|
+
? (() => {
|
|
86
|
+
try {
|
|
87
|
+
const resolved = fs.realpathSync.native(
|
|
88
|
+
String.raw`\\?\GLOBALROOT\SystemRoot`,
|
|
89
|
+
);
|
|
90
|
+
const normalized = path.win32.resolve(resolved);
|
|
91
|
+
return /^[A-Za-z]:\\/.test(normalized) && !normalized.includes("\0")
|
|
92
|
+
? normalized
|
|
93
|
+
: null;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
})()
|
|
98
|
+
: "C:\\Windows";
|
|
58
99
|
const WINDOWS_IDENTITY_WAIT = new Int32Array(new SharedArrayBuffer(4));
|
|
100
|
+
const WINDOWS_RESTRICTED_TOKEN_BACKEND = "windows-job-restricted-token";
|
|
101
|
+
const WINDOWS_APPCONTAINER_BACKEND =
|
|
102
|
+
"windows-appcontainer-job-restricted-token";
|
|
103
|
+
const WINDOWS_ADAPTER_IDLE_TTL_MS = 60_000;
|
|
104
|
+
// Every native helper operation starts a trusted System32 PowerShell host and
|
|
105
|
+
// byte-loads the checked-in assembly. Hosted Windows runners can spend well
|
|
106
|
+
// over ten seconds in that bootstrap before the managed operation begins.
|
|
107
|
+
const WINDOWS_SANDBOX_HELPER_OPERATION_TIMEOUT_MS = 120_000;
|
|
108
|
+
const WINDOWS_SANDBOX_HOST_ENV_DENYLIST = new Set([
|
|
109
|
+
"APPDOMAIN_MANAGER_ASM",
|
|
110
|
+
"APPDOMAIN_MANAGER_TYPE",
|
|
111
|
+
"COMPLUS_APPLICATIONMIGRATIONRUNTIMEACTIVATIONCONFIGPATH",
|
|
112
|
+
"COMPLUS_INSTALLROOT",
|
|
113
|
+
"COMPLUS_STARTUPHOOK",
|
|
114
|
+
"COMPLUS_VERSION",
|
|
115
|
+
"CORECLR_ENABLE_PROFILING",
|
|
116
|
+
"CORECLR_PROFILER",
|
|
117
|
+
"CORECLR_PROFILER_PATH",
|
|
118
|
+
"CORECLR_PROFILER_PATH_32",
|
|
119
|
+
"CORECLR_PROFILER_PATH_64",
|
|
120
|
+
"CORECLR_PROFILER_PATH_ARM32",
|
|
121
|
+
"CORECLR_PROFILER_PATH_ARM64",
|
|
122
|
+
"COR_ENABLE_PROFILING",
|
|
123
|
+
"COR_PROFILER",
|
|
124
|
+
"COR_PROFILER_PATH",
|
|
125
|
+
"COR_PROFILER_PATH_32",
|
|
126
|
+
"COR_PROFILER_PATH_64",
|
|
127
|
+
"DOTNET_ENABLE_PROFILING",
|
|
128
|
+
"DOTNET_PROFILER",
|
|
129
|
+
"DOTNET_PROFILER_PATH",
|
|
130
|
+
"DOTNET_PROFILER_PATH_32",
|
|
131
|
+
"DOTNET_PROFILER_PATH_64",
|
|
132
|
+
"DOTNET_PROFILER_PATH_ARM32",
|
|
133
|
+
"DOTNET_PROFILER_PATH_ARM64",
|
|
134
|
+
"DOTNET_STARTUP_HOOKS",
|
|
135
|
+
]);
|
|
136
|
+
const WINDOWS_PLUGIN_NODE_ENV_DENYLIST = new Set([
|
|
137
|
+
"NODE_CHANNEL_FD",
|
|
138
|
+
"NODE_OPTIONS",
|
|
139
|
+
"OPENSSL_CONF",
|
|
140
|
+
"OPENSSL_CONF_INCLUDE",
|
|
141
|
+
"OPENSSL_ENGINES",
|
|
142
|
+
"OPENSSL_MODULES",
|
|
143
|
+
]);
|
|
59
144
|
const LINUX_BWRAP_PATH = "/usr/bin/bwrap";
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
145
|
+
const LINUX_LDD_PATH = "/usr/bin/ldd";
|
|
146
|
+
const LINUX_BWRAP_BACKEND = "linux-bwrap";
|
|
147
|
+
const LINUX_BWRAP_NODE_PROBE_SENTINEL = "chainless-linux-bwrap-plugin-node-v1";
|
|
148
|
+
const LINUX_BWRAP_MAX_PLUGIN_ENTRIES = 512;
|
|
149
|
+
const LINUX_BWRAP_FIRST_MOUNT_FD = 3;
|
|
150
|
+
const LINUX_ATTESTED_FILE_MAX_BYTES = 256 * 1024 * 1024;
|
|
151
|
+
const LINUX_ATTESTATION_HASH_CHUNK_BYTES = 1024 * 1024;
|
|
152
|
+
const LINUX_ELF64_HEADER_BYTES = 64;
|
|
153
|
+
const LINUX_ELF64_PROGRAM_HEADER_BYTES = 56;
|
|
154
|
+
const LINUX_ELF64_DYNAMIC_ENTRY_BYTES = 16;
|
|
155
|
+
const LINUX_ELF_MAX_PROGRAM_HEADERS = 128;
|
|
156
|
+
const LINUX_ELF_MAX_DYNAMIC_ENTRIES = 4096;
|
|
157
|
+
const LINUX_ELF_TYPE_EXEC = 2;
|
|
158
|
+
const LINUX_ELF_TYPE_DYN = 3;
|
|
159
|
+
const LINUX_ELF_PROGRAM_LOAD = 1;
|
|
160
|
+
const LINUX_ELF_PROGRAM_DYNAMIC = 2;
|
|
161
|
+
const LINUX_ELF_PROGRAM_INTERP = 3;
|
|
162
|
+
const LINUX_ELF_PROGRAM_GNU_STACK = 0x6474e551;
|
|
163
|
+
const LINUX_ELF_DYNAMIC_NULL = 0n;
|
|
164
|
+
const LINUX_ELF_DYNAMIC_NEEDED = 1n;
|
|
165
|
+
const LINUX_ELF_DYNAMIC_FLAGS_1 = 0x6ffffffbn;
|
|
166
|
+
const LINUX_ELF_DYNAMIC_FLAG_PIE = 0x08000000n;
|
|
167
|
+
const LINUX_ELF_MACHINES = Object.freeze({
|
|
168
|
+
x64: 62,
|
|
169
|
+
arm64: 183,
|
|
170
|
+
riscv64: 243,
|
|
171
|
+
});
|
|
172
|
+
// Linux asm-generic: __O_TMPFILE (0x400000) | O_DIRECTORY (0x010000).
|
|
173
|
+
// x64, arm64, and riscv64 use this value; unsupported seccomp architectures
|
|
174
|
+
// already fail closed before the anonymous filter is created.
|
|
175
|
+
const LINUX_O_TMPFILE = 0x410000;
|
|
176
|
+
const LINUX_SECCOMP_FILTERS = Object.freeze({
|
|
177
|
+
x64: Object.freeze({
|
|
178
|
+
auditArch: 0xc000003e,
|
|
179
|
+
socketSyscall: 41,
|
|
180
|
+
socketpairSyscall: 53,
|
|
181
|
+
ioUringSetupSyscall: 425,
|
|
182
|
+
x32SyscallBit: 0x40000000,
|
|
183
|
+
}),
|
|
184
|
+
arm64: Object.freeze({
|
|
185
|
+
auditArch: 0xc00000b7,
|
|
186
|
+
socketSyscall: 198,
|
|
187
|
+
socketpairSyscall: 199,
|
|
188
|
+
ioUringSetupSyscall: 425,
|
|
189
|
+
}),
|
|
190
|
+
riscv64: Object.freeze({
|
|
191
|
+
auditArch: 0xc00000f3,
|
|
192
|
+
socketSyscall: 198,
|
|
193
|
+
socketpairSyscall: 199,
|
|
194
|
+
ioUringSetupSyscall: 425,
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
const LINUX_LOCAL_FILESYSTEM_TYPES = new Set([
|
|
198
|
+
"btrfs",
|
|
199
|
+
"erofs",
|
|
200
|
+
"ext2",
|
|
201
|
+
"ext3",
|
|
202
|
+
"ext4",
|
|
203
|
+
"overlay",
|
|
204
|
+
"squashfs",
|
|
205
|
+
"tmpfs",
|
|
206
|
+
"xfs",
|
|
207
|
+
"zfs",
|
|
75
208
|
]);
|
|
76
209
|
const DEFAULT_RUNTIME = Object.freeze({
|
|
77
210
|
platform: os.platform(),
|
|
211
|
+
arch: process.arch,
|
|
78
212
|
fs,
|
|
79
213
|
tmpdir: () => os.tmpdir(),
|
|
80
214
|
homedir: () => os.homedir(),
|
|
81
215
|
randomBytes: (size) => crypto.randomBytes(size),
|
|
82
216
|
resolvePath: (value) => path.resolve(value),
|
|
83
217
|
joinPath: (...parts) => path.join(...parts),
|
|
84
|
-
|
|
218
|
+
// GLOBALROOT resolves the kernel-owned SystemRoot object rather than the
|
|
219
|
+
// caller-controlled WINDIR/SystemRoot environment variables.
|
|
220
|
+
windowsDir: () => WINDOWS_TRUSTED_SYSTEM_ROOT,
|
|
85
221
|
moduleDir: MODULE_DIR,
|
|
86
222
|
now: () => Date.now(),
|
|
87
223
|
sleepSync: (milliseconds) =>
|
|
88
224
|
Atomics.wait(WINDOWS_IDENTITY_WAIT, 0, 0, milliseconds),
|
|
89
225
|
spawnSync: nativeSpawnSync,
|
|
226
|
+
warn: (message) => process.emitWarning(message),
|
|
90
227
|
});
|
|
91
228
|
|
|
229
|
+
let windowsAdapterCache = null;
|
|
230
|
+
const windowsAdapterCacheEntries = new Set();
|
|
231
|
+
const windowsTemporaryCleanupBacklog = new Set();
|
|
232
|
+
let windowsTemporaryCleanupRetryTimer = null;
|
|
233
|
+
const windowsAppContainerCleanupBacklog = new Set();
|
|
234
|
+
// Each AppContainer retry starts a synchronous, digest-attested PowerShell
|
|
235
|
+
// helper. Bound automatic retries so a permanently unsupported Windows host
|
|
236
|
+
// cannot repeatedly block a long-lived CLI; explicit cleanup/reset and process
|
|
237
|
+
// teardown remain available after this backoff is exhausted.
|
|
238
|
+
const WINDOWS_APPCONTAINER_AUTOMATIC_CLEANUP_DELAYS_MS = Object.freeze([
|
|
239
|
+
250, 1_000, 5_000,
|
|
240
|
+
]);
|
|
241
|
+
let windowsAdapterExitCleanupRegistered = false;
|
|
242
|
+
|
|
92
243
|
function resolveRuntime(overrides = {}) {
|
|
93
244
|
return { ...DEFAULT_RUNTIME, ...overrides };
|
|
94
245
|
}
|
|
@@ -121,7 +272,17 @@ function normalizeSandboxRequest(profileOrRequest, explicitRequest) {
|
|
|
121
272
|
),
|
|
122
273
|
]
|
|
123
274
|
: [];
|
|
124
|
-
return {
|
|
275
|
+
return {
|
|
276
|
+
profile,
|
|
277
|
+
requiredBoundaries,
|
|
278
|
+
sync: request.sync === true,
|
|
279
|
+
executionContract:
|
|
280
|
+
request.executionContract &&
|
|
281
|
+
typeof request.executionContract === "object" &&
|
|
282
|
+
!Array.isArray(request.executionContract)
|
|
283
|
+
? request.executionContract
|
|
284
|
+
: null,
|
|
285
|
+
};
|
|
125
286
|
}
|
|
126
287
|
|
|
127
288
|
/**
|
|
@@ -204,7 +365,6 @@ function createSandboxPlan(input) {
|
|
|
204
365
|
applied: false,
|
|
205
366
|
enforcement: null,
|
|
206
367
|
backend: null,
|
|
207
|
-
guarantees: Object.freeze([]),
|
|
208
368
|
reason: null,
|
|
209
369
|
...input,
|
|
210
370
|
args: Object.freeze([...(input.args || [])]),
|
|
@@ -401,60 +561,771 @@ export function applyMacSandbox(
|
|
|
401
561
|
// Windows Job Object + Restricted Token
|
|
402
562
|
// ---------------------------------------------------------------------------
|
|
403
563
|
|
|
564
|
+
function cleanupWindowsTemporaryPath(
|
|
565
|
+
runtime,
|
|
566
|
+
targetPath,
|
|
567
|
+
attempts = 100,
|
|
568
|
+
delayMs = 10,
|
|
569
|
+
) {
|
|
570
|
+
if (!targetPath) return true;
|
|
571
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
572
|
+
try {
|
|
573
|
+
runtime.fs.unlinkSync(targetPath);
|
|
574
|
+
return true;
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (error?.code === "ENOENT") return true;
|
|
577
|
+
}
|
|
578
|
+
if (attempt + 1 < attempts) runtime.sleepSync(delayMs);
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function cleanupOrTrackWindowsTemporaryPath(
|
|
584
|
+
runtime,
|
|
585
|
+
targetPath,
|
|
586
|
+
attempts = 100,
|
|
587
|
+
) {
|
|
588
|
+
if (cleanupWindowsTemporaryPath(runtime, targetPath, attempts)) {
|
|
589
|
+
for (const residual of [...windowsTemporaryCleanupBacklog]) {
|
|
590
|
+
if (residual.runtime === runtime && residual.targetPath === targetPath) {
|
|
591
|
+
windowsTemporaryCleanupBacklog.delete(residual);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (
|
|
595
|
+
windowsTemporaryCleanupBacklog.size === 0 &&
|
|
596
|
+
windowsTemporaryCleanupRetryTimer
|
|
597
|
+
) {
|
|
598
|
+
clearTimeout(windowsTemporaryCleanupRetryTimer);
|
|
599
|
+
windowsTemporaryCleanupRetryTimer = null;
|
|
600
|
+
}
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
if (
|
|
604
|
+
![...windowsTemporaryCleanupBacklog].some(
|
|
605
|
+
(residual) =>
|
|
606
|
+
residual.runtime === runtime && residual.targetPath === targetPath,
|
|
607
|
+
)
|
|
608
|
+
) {
|
|
609
|
+
windowsTemporaryCleanupBacklog.add({ runtime, targetPath });
|
|
610
|
+
}
|
|
611
|
+
registerWindowsAdapterExitCleanup();
|
|
612
|
+
scheduleWindowsTemporaryCleanupRetry();
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function retryWindowsTemporaryCleanupBacklog(attempts = 100) {
|
|
617
|
+
let cleaned = true;
|
|
618
|
+
for (const residual of [...windowsTemporaryCleanupBacklog]) {
|
|
619
|
+
if (
|
|
620
|
+
cleanupWindowsTemporaryPath(
|
|
621
|
+
residual.runtime,
|
|
622
|
+
residual.targetPath,
|
|
623
|
+
attempts,
|
|
624
|
+
)
|
|
625
|
+
) {
|
|
626
|
+
windowsTemporaryCleanupBacklog.delete(residual);
|
|
627
|
+
} else {
|
|
628
|
+
cleaned = false;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (
|
|
632
|
+
windowsTemporaryCleanupBacklog.size === 0 &&
|
|
633
|
+
windowsTemporaryCleanupRetryTimer
|
|
634
|
+
) {
|
|
635
|
+
clearTimeout(windowsTemporaryCleanupRetryTimer);
|
|
636
|
+
windowsTemporaryCleanupRetryTimer = null;
|
|
637
|
+
}
|
|
638
|
+
return cleaned;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function scheduleWindowsTemporaryCleanupRetry() {
|
|
642
|
+
if (
|
|
643
|
+
windowsTemporaryCleanupRetryTimer ||
|
|
644
|
+
windowsTemporaryCleanupBacklog.size === 0
|
|
645
|
+
) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
windowsTemporaryCleanupRetryTimer = setTimeout(() => {
|
|
649
|
+
windowsTemporaryCleanupRetryTimer = null;
|
|
650
|
+
retryWindowsTemporaryCleanupBacklog(1);
|
|
651
|
+
if (windowsTemporaryCleanupBacklog.size > 0) {
|
|
652
|
+
scheduleWindowsTemporaryCleanupRetry();
|
|
653
|
+
}
|
|
654
|
+
}, 250);
|
|
655
|
+
windowsTemporaryCleanupRetryTimer.unref?.();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function cleanupWindowsTemporaryDirectory(
|
|
659
|
+
runtime,
|
|
660
|
+
targetPath,
|
|
661
|
+
attempts = 100,
|
|
662
|
+
delayMs = 10,
|
|
663
|
+
) {
|
|
664
|
+
if (!targetPath) return true;
|
|
665
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
666
|
+
try {
|
|
667
|
+
runtime.fs.rmdirSync(targetPath);
|
|
668
|
+
return true;
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (error?.code === "ENOENT") return true;
|
|
671
|
+
}
|
|
672
|
+
if (attempt + 1 < attempts) runtime.sleepSync(delayMs);
|
|
673
|
+
}
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function sha256(value) {
|
|
678
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function windowsFileIdentity(stat) {
|
|
682
|
+
const timestamp = (nanosecondsKey, millisecondsKey) => {
|
|
683
|
+
if (stat[nanosecondsKey] !== undefined) {
|
|
684
|
+
return String(stat[nanosecondsKey]);
|
|
685
|
+
}
|
|
686
|
+
return String(Math.trunc(Number(stat[millisecondsKey] || 0) * 1_000_000));
|
|
687
|
+
};
|
|
688
|
+
return Object.freeze({
|
|
689
|
+
dev: String(stat.dev),
|
|
690
|
+
ino: String(stat.ino),
|
|
691
|
+
size: String(stat.size),
|
|
692
|
+
mode: String(stat.mode),
|
|
693
|
+
birthtimeNs: timestamp("birthtimeNs", "birthtimeMs"),
|
|
694
|
+
ctimeNs: timestamp("ctimeNs", "ctimeMs"),
|
|
695
|
+
mtimeNs: timestamp("mtimeNs", "mtimeMs"),
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function sameWindowsFileIdentity(left, right) {
|
|
700
|
+
return (
|
|
701
|
+
left.dev === right.dev &&
|
|
702
|
+
left.ino === right.ino &&
|
|
703
|
+
left.size === right.size &&
|
|
704
|
+
left.mode === right.mode &&
|
|
705
|
+
left.birthtimeNs === right.birthtimeNs &&
|
|
706
|
+
left.ctimeNs === right.ctimeNs &&
|
|
707
|
+
left.mtimeNs === right.mtimeNs
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function inspectWindowsAdapterAssembly(runtime, assemblyPath) {
|
|
712
|
+
if (typeof runtime.fs.lstatSync === "function") {
|
|
713
|
+
const linkStat = runtime.fs.lstatSync(assemblyPath, { bigint: true });
|
|
714
|
+
if (
|
|
715
|
+
linkStat.isSymbolicLink?.() ||
|
|
716
|
+
(typeof linkStat.isFile === "function" && !linkStat.isFile())
|
|
717
|
+
) {
|
|
718
|
+
throw new Error("Windows in-memory adapter is not a regular file");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const before = runtime.fs.statSync(assemblyPath, { bigint: true });
|
|
722
|
+
if (typeof before.isFile === "function" && !before.isFile()) {
|
|
723
|
+
throw new Error("Windows in-memory adapter is not a regular file");
|
|
724
|
+
}
|
|
725
|
+
const beforeIdentity = windowsFileIdentity(before);
|
|
726
|
+
const assemblyDigest = sha256(runtime.fs.readFileSync(assemblyPath));
|
|
727
|
+
const afterIdentity = windowsFileIdentity(
|
|
728
|
+
runtime.fs.statSync(assemblyPath, { bigint: true }),
|
|
729
|
+
);
|
|
730
|
+
if (!sameWindowsFileIdentity(beforeIdentity, afterIdentity)) {
|
|
731
|
+
throw new Error("Windows in-memory adapter changed during attestation");
|
|
732
|
+
}
|
|
733
|
+
return { assemblyDigest, fileIdentity: afterIdentity };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function verifyWindowsAdapterEntry(entry, runtime, source) {
|
|
737
|
+
if (
|
|
738
|
+
entry.retired ||
|
|
739
|
+
entry.cleaned ||
|
|
740
|
+
entry.runtimeFs !== runtime.fs ||
|
|
741
|
+
entry.tempDirectory !== source.tempDirectory ||
|
|
742
|
+
entry.sourceDigest !== source.sourceDigest ||
|
|
743
|
+
entry.sourceContractDigest !== source.sourceContractDigest ||
|
|
744
|
+
entry.loaderMode !== source.loaderMode
|
|
745
|
+
) {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
const attestation = inspectWindowsAdapterAssembly(
|
|
750
|
+
runtime,
|
|
751
|
+
entry.assemblyPath,
|
|
752
|
+
);
|
|
753
|
+
return (
|
|
754
|
+
attestation.assemblyDigest === entry.assemblyDigest &&
|
|
755
|
+
sameWindowsFileIdentity(attestation.fileIdentity, entry.fileIdentity)
|
|
756
|
+
);
|
|
757
|
+
} catch {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function cleanupWindowsAdapterEntry(entry, attempts = 100) {
|
|
763
|
+
if (entry.cleaned) return true;
|
|
764
|
+
if (entry.idleTimer) {
|
|
765
|
+
clearTimeout(entry.idleTimer);
|
|
766
|
+
entry.idleTimer = null;
|
|
767
|
+
}
|
|
768
|
+
const deleted = cleanupOrTrackWindowsTemporaryPath(
|
|
769
|
+
entry.runtime,
|
|
770
|
+
entry.assemblyPath,
|
|
771
|
+
attempts,
|
|
772
|
+
);
|
|
773
|
+
const directoryDeleted =
|
|
774
|
+
deleted &&
|
|
775
|
+
cleanupWindowsTemporaryDirectory(
|
|
776
|
+
entry.runtime,
|
|
777
|
+
entry.adapterDirectory,
|
|
778
|
+
attempts,
|
|
779
|
+
);
|
|
780
|
+
if (deleted && directoryDeleted) {
|
|
781
|
+
entry.cleaned = true;
|
|
782
|
+
windowsAdapterCacheEntries.delete(entry);
|
|
783
|
+
}
|
|
784
|
+
return deleted && directoryDeleted;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function retireWindowsAdapterEntry(entry) {
|
|
788
|
+
if (!entry) return;
|
|
789
|
+
entry.retired = true;
|
|
790
|
+
if (windowsAdapterCache === entry) windowsAdapterCache = null;
|
|
791
|
+
if (entry.refCount === 0) cleanupWindowsAdapterEntry(entry);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function cleanupAllWindowsAdapterEntries(attempts = 100) {
|
|
795
|
+
retryWindowsAppContainerCleanupBacklog();
|
|
796
|
+
windowsAdapterCache = null;
|
|
797
|
+
for (const entry of [...windowsAdapterCacheEntries]) {
|
|
798
|
+
entry.retired = true;
|
|
799
|
+
entry.refCount = 0;
|
|
800
|
+
cleanupWindowsAdapterEntry(entry, attempts);
|
|
801
|
+
}
|
|
802
|
+
retryWindowsTemporaryCleanupBacklog(attempts);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function registerWindowsAdapterExitCleanup() {
|
|
806
|
+
if (windowsAdapterExitCleanupRegistered) return;
|
|
807
|
+
windowsAdapterExitCleanupRegistered = true;
|
|
808
|
+
process.on("beforeExit", () => {
|
|
809
|
+
cleanupAllWindowsAdapterEntries();
|
|
810
|
+
});
|
|
811
|
+
process.once("exit", () => {
|
|
812
|
+
cleanupAllWindowsAdapterEntries();
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
404
816
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
817
|
+
* Explicitly discard the process-local Windows helper cache. Production keeps
|
|
818
|
+
* an attested helper until process teardown; tests and embedding hosts may use
|
|
819
|
+
* this hook at a known quiescent point to prove cleanup.
|
|
408
820
|
*/
|
|
409
|
-
function
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (!
|
|
416
|
-
|
|
821
|
+
export function resetWindowsSandboxAdapterCache() {
|
|
822
|
+
let cleaned = retryWindowsAppContainerCleanupBacklog();
|
|
823
|
+
windowsAdapterCache = null;
|
|
824
|
+
for (const entry of [...windowsAdapterCacheEntries]) {
|
|
825
|
+
entry.retired = true;
|
|
826
|
+
entry.refCount = 0;
|
|
827
|
+
if (!cleanupWindowsAdapterEntry(entry)) cleaned = false;
|
|
828
|
+
}
|
|
829
|
+
if (!retryWindowsTemporaryCleanupBacklog()) cleaned = false;
|
|
830
|
+
return cleaned;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function scheduleWindowsAdapterIdleCleanup(entry) {
|
|
834
|
+
if (
|
|
835
|
+
entry.retired ||
|
|
836
|
+
entry.cleaned ||
|
|
837
|
+
entry.refCount !== 0 ||
|
|
838
|
+
entry.idleTimer
|
|
839
|
+
) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const configuredTtl = Number(entry.runtime.windowsAdapterIdleTtlMs);
|
|
843
|
+
const idleTtlMs =
|
|
844
|
+
Number.isFinite(configuredTtl) && configuredTtl >= 0
|
|
845
|
+
? configuredTtl
|
|
846
|
+
: WINDOWS_ADAPTER_IDLE_TTL_MS;
|
|
847
|
+
entry.idleTimer = setTimeout(() => {
|
|
848
|
+
entry.idleTimer = null;
|
|
849
|
+
if (
|
|
850
|
+
entry.refCount === 0 &&
|
|
851
|
+
!entry.retired &&
|
|
852
|
+
!entry.cleaned &&
|
|
853
|
+
windowsAdapterCache === entry
|
|
854
|
+
) {
|
|
855
|
+
retireWindowsAdapterEntry(entry);
|
|
417
856
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
857
|
+
}, idleTtlMs);
|
|
858
|
+
entry.idleTimer.unref?.();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function loadWindowsAdapterSource(runtime, loaderMode) {
|
|
862
|
+
const bundled = runtime.windowsAdapterContent === undefined;
|
|
863
|
+
const content = bundled
|
|
864
|
+
? loaderMode === "managed-executable"
|
|
865
|
+
? WINDOWS_SANDBOX_HELPER_EXECUTABLE
|
|
866
|
+
: WINDOWS_SANDBOX_HELPER_ASSEMBLY
|
|
867
|
+
: Buffer.from(runtime.windowsAdapterContent);
|
|
868
|
+
return {
|
|
869
|
+
content,
|
|
870
|
+
sourceDigest: bundled
|
|
871
|
+
? loaderMode === "managed-executable"
|
|
872
|
+
? WINDOWS_SANDBOX_HELPER_EXECUTABLE_DIGEST
|
|
873
|
+
: WINDOWS_SANDBOX_HELPER_ASSEMBLY_DIGEST
|
|
874
|
+
: sha256(content),
|
|
875
|
+
sourceContractDigest: bundled ? WINDOWS_SANDBOX_HELPER_SOURCE_DIGEST : null,
|
|
876
|
+
tempDirectory: runtime.tmpdir(),
|
|
877
|
+
loaderMode,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Materialize the trusted in-process assembly bytes at a fresh path. The
|
|
883
|
+
* managed image is never executed from this user-writable location. A
|
|
884
|
+
* protected PowerShell host reads the bytes once, verifies the expected
|
|
885
|
+
* digest, and loads those exact bytes with Assembly.Load(byte[]).
|
|
886
|
+
*/
|
|
887
|
+
function materializeWindowsAdapter(runtime, source) {
|
|
888
|
+
const adapterBaseName = `chainless-win-sandbox-${runtime
|
|
889
|
+
.randomBytes(24)
|
|
890
|
+
.toString("hex")}`;
|
|
891
|
+
const adapterDirectory =
|
|
892
|
+
source.loaderMode === "managed-executable"
|
|
893
|
+
? runtime.joinPath(source.tempDirectory, adapterBaseName)
|
|
894
|
+
: null;
|
|
895
|
+
const assemblyPath = runtime.joinPath(
|
|
896
|
+
adapterDirectory || source.tempDirectory,
|
|
897
|
+
source.loaderMode === "managed-executable"
|
|
898
|
+
? "windows-sandbox-helper.exe"
|
|
899
|
+
: `${adapterBaseName}.dll`,
|
|
900
|
+
);
|
|
901
|
+
if (
|
|
902
|
+
(adapterDirectory && runtime.fs.existsSync(adapterDirectory)) ||
|
|
903
|
+
runtime.fs.existsSync(assemblyPath)
|
|
904
|
+
) {
|
|
905
|
+
return { reason: "windows_native_adapter_random_path_collision" };
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
if (adapterDirectory) {
|
|
909
|
+
runtime.fs.mkdirSync(adapterDirectory, {
|
|
910
|
+
mode: 0o700,
|
|
911
|
+
recursive: false,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
runtime.fs.writeFileSync(assemblyPath, source.content, {
|
|
915
|
+
mode: adapterDirectory ? 0o500 : 0o600,
|
|
916
|
+
flag: "wx",
|
|
917
|
+
});
|
|
918
|
+
} catch (error) {
|
|
919
|
+
const assemblyDeleted = cleanupOrTrackWindowsTemporaryPath(
|
|
920
|
+
runtime,
|
|
921
|
+
assemblyPath,
|
|
922
|
+
);
|
|
923
|
+
const directoryDeleted =
|
|
924
|
+
assemblyDeleted &&
|
|
925
|
+
cleanupWindowsTemporaryDirectory(runtime, adapterDirectory);
|
|
926
|
+
if (!assemblyDeleted || !directoryDeleted) {
|
|
927
|
+
return { reason: "windows_native_adapter_compile_cleanup_unverified" };
|
|
422
928
|
}
|
|
929
|
+
if (error?.code === "EEXIST") {
|
|
930
|
+
return { reason: "windows_native_adapter_random_path_collision" };
|
|
931
|
+
}
|
|
932
|
+
return { reason: "windows_native_adapter_materialize_failed" };
|
|
423
933
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
934
|
+
return {
|
|
935
|
+
adapterDirectory,
|
|
936
|
+
assemblyPath,
|
|
937
|
+
cleanupAssembly: () => {
|
|
938
|
+
const assemblyDeleted = cleanupOrTrackWindowsTemporaryPath(
|
|
939
|
+
runtime,
|
|
940
|
+
assemblyPath,
|
|
941
|
+
);
|
|
942
|
+
return (
|
|
943
|
+
assemblyDeleted &&
|
|
944
|
+
cleanupWindowsTemporaryDirectory(runtime, adapterDirectory)
|
|
945
|
+
);
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function materializeWindowsAdapterInvocation(runtime, helperArgs) {
|
|
951
|
+
const payloadPath = runtime.joinPath(
|
|
430
952
|
runtime.tmpdir(),
|
|
431
|
-
`chainless-win-sandbox-${
|
|
953
|
+
`chainless-win-sandbox-invocation-${runtime
|
|
954
|
+
.randomBytes(24)
|
|
955
|
+
.toString("hex")}.json`,
|
|
432
956
|
);
|
|
433
|
-
if (runtime.fs.existsSync(
|
|
957
|
+
if (runtime.fs.existsSync(payloadPath)) {
|
|
958
|
+
return { reason: "windows_adapter_invocation_random_path_collision" };
|
|
959
|
+
}
|
|
960
|
+
let content;
|
|
961
|
+
try {
|
|
962
|
+
content = Buffer.from(
|
|
963
|
+
[
|
|
964
|
+
"CC_WINDOWS_SANDBOX_INVOCATION_V1",
|
|
965
|
+
...Array.from(helperArgs || [], (value) =>
|
|
966
|
+
Buffer.from(String(value), "utf8").toString("base64"),
|
|
967
|
+
),
|
|
968
|
+
"",
|
|
969
|
+
].join("\n"),
|
|
970
|
+
"utf8",
|
|
971
|
+
);
|
|
972
|
+
} catch {
|
|
973
|
+
return { reason: "windows_adapter_invocation_encode_failed" };
|
|
974
|
+
}
|
|
975
|
+
if (content.length > 8 * 1024 * 1024) {
|
|
976
|
+
return { reason: "windows_adapter_invocation_too_large" };
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
runtime.fs.writeFileSync(payloadPath, content, {
|
|
980
|
+
mode: 0o600,
|
|
981
|
+
flag: "wx",
|
|
982
|
+
});
|
|
983
|
+
} catch (error) {
|
|
984
|
+
if (error?.code === "EEXIST") {
|
|
985
|
+
return { reason: "windows_adapter_invocation_random_path_collision" };
|
|
986
|
+
}
|
|
434
987
|
return {
|
|
435
|
-
|
|
436
|
-
|
|
988
|
+
reason: cleanupOrTrackWindowsTemporaryPath(runtime, payloadPath)
|
|
989
|
+
? "windows_adapter_invocation_materialize_failed"
|
|
990
|
+
: "windows_adapter_invocation_cleanup_unverified",
|
|
437
991
|
};
|
|
438
992
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
993
|
+
return {
|
|
994
|
+
payloadPath,
|
|
995
|
+
payloadDigest: sha256(content),
|
|
996
|
+
cleanup: () => cleanupOrTrackWindowsTemporaryPath(runtime, payloadPath),
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function windowsPowerShellBootstrap(entry, invocation) {
|
|
1001
|
+
const encodeUtf8 = (value) =>
|
|
1002
|
+
Buffer.from(String(value), "utf8").toString("base64");
|
|
1003
|
+
const assemblyPath = encodeUtf8(entry.assemblyPath);
|
|
1004
|
+
const payloadPath = encodeUtf8(invocation.payloadPath);
|
|
1005
|
+
const bootstrap = [
|
|
1006
|
+
"$ErrorActionPreference='Stop'",
|
|
1007
|
+
"$ProgressPreference='SilentlyContinue'",
|
|
1008
|
+
"try {",
|
|
1009
|
+
`$ccAssemblyPath=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${assemblyPath}'))`,
|
|
1010
|
+
`$ccPayloadPath=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${payloadPath}'))`,
|
|
1011
|
+
`$ccExpectedAssembly='${entry.assemblyDigest}'`,
|
|
1012
|
+
`$ccExpectedPayload='${invocation.payloadDigest}'`,
|
|
1013
|
+
"$ccPayloadBytes=[IO.File]::ReadAllBytes($ccPayloadPath)",
|
|
1014
|
+
"[IO.File]::Delete($ccPayloadPath)",
|
|
1015
|
+
"if([IO.File]::Exists($ccPayloadPath)){throw 'Windows sandbox adapter invocation cleanup failed'}",
|
|
1016
|
+
"$ccAssemblyBytes=[IO.File]::ReadAllBytes($ccAssemblyPath)",
|
|
1017
|
+
"if($ccAssemblyBytes.Length -gt 1048576 -or $ccPayloadBytes.Length -gt 8388608){throw 'Windows sandbox adapter input exceeds its limit'}",
|
|
1018
|
+
"$ccSha=[Security.Cryptography.SHA256]::Create()",
|
|
1019
|
+
"try{$ccAssemblyHash=([BitConverter]::ToString($ccSha.ComputeHash($ccAssemblyBytes))).Replace('-','').ToLowerInvariant();$ccSha.Initialize();$ccPayloadHash=([BitConverter]::ToString($ccSha.ComputeHash($ccPayloadBytes))).Replace('-','').ToLowerInvariant()}finally{$ccSha.Dispose()}",
|
|
1020
|
+
"if(![String]::Equals($ccAssemblyHash,$ccExpectedAssembly,[StringComparison]::Ordinal)-or ![String]::Equals($ccPayloadHash,$ccExpectedPayload,[StringComparison]::Ordinal)){throw 'Windows sandbox adapter input digest mismatch'}",
|
|
1021
|
+
"$ccUtf8=New-Object Text.UTF8Encoding($false,$true)",
|
|
1022
|
+
"$ccLines=$ccUtf8.GetString($ccPayloadBytes).Split([char]10)",
|
|
1023
|
+
"if($ccLines.Length -lt 2 -or $ccLines[0] -cne 'CC_WINDOWS_SANDBOX_INVOCATION_V1' -or $ccLines[$ccLines.Length-1] -cne ''){throw 'Windows sandbox adapter invocation is invalid'}",
|
|
1024
|
+
"$ccArgCount=$ccLines.Length-2",
|
|
1025
|
+
"$ccArgs=[Array]::CreateInstance([string],$ccArgCount)",
|
|
1026
|
+
"for($ccIndex=0;$ccIndex -lt $ccArgCount;$ccIndex++){$ccArgs[$ccIndex]=$ccUtf8.GetString([Convert]::FromBase64String($ccLines[$ccIndex+1]))}",
|
|
1027
|
+
"$ccAssembly=[Reflection.Assembly]::Load($ccAssemblyBytes)",
|
|
1028
|
+
"$ccProgram=$ccAssembly.GetType('ChainlessChain.WindowsSandbox.Program',$true)",
|
|
1029
|
+
"$ccMain=$ccProgram.GetMethod('Main',[Reflection.BindingFlags]'Public,Static')",
|
|
1030
|
+
"if($null -eq $ccMain){throw 'Windows sandbox adapter entry point is missing'}",
|
|
1031
|
+
"$ccExit=$ccMain.Invoke($null,[object[]]@(,[string[]]$ccArgs))",
|
|
1032
|
+
"exit [int]$ccExit",
|
|
1033
|
+
"} catch {",
|
|
1034
|
+
"[Console]::Error.WriteLine('CC_WINDOWS_SANDBOX_ERROR: '+$_.Exception.Message)",
|
|
1035
|
+
"exit 125",
|
|
1036
|
+
"}",
|
|
1037
|
+
].join("\n");
|
|
1038
|
+
return Buffer.from(bootstrap, "utf16le").toString("base64");
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function buildWindowsAdapterInvocation(entry, invocation) {
|
|
1042
|
+
if (entry.loaderMode === "managed-executable") {
|
|
1043
|
+
return {
|
|
1044
|
+
command: entry.assemblyPath,
|
|
1045
|
+
args: [
|
|
1046
|
+
"--adapter-path",
|
|
1047
|
+
entry.assemblyPath,
|
|
1048
|
+
"--invocation-file",
|
|
1049
|
+
invocation.payloadPath,
|
|
1050
|
+
invocation.payloadDigest,
|
|
1051
|
+
],
|
|
1052
|
+
cleanup: invocation.cleanup,
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
command: entry.powershellPath,
|
|
1057
|
+
args: [
|
|
1058
|
+
"-NoLogo",
|
|
1059
|
+
"-NoProfile",
|
|
1060
|
+
"-NonInteractive",
|
|
1061
|
+
"-ExecutionPolicy",
|
|
1062
|
+
"Bypass",
|
|
1063
|
+
"-EncodedCommand",
|
|
1064
|
+
windowsPowerShellBootstrap(entry, invocation),
|
|
1065
|
+
],
|
|
1066
|
+
cleanup: invocation.cleanup,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function resolveWindowsPowerShellHost(runtime, loaderMode) {
|
|
1071
|
+
if (loaderMode !== "powershell-byte-assembly") return null;
|
|
1072
|
+
const windowsDirectory = path.win32.resolve(runtime.windowsDir());
|
|
1073
|
+
const driveRoot = path.win32.parse(windowsDirectory).root;
|
|
1074
|
+
const modernHost = runtime.joinPath(
|
|
1075
|
+
driveRoot,
|
|
1076
|
+
"Program Files",
|
|
1077
|
+
"PowerShell",
|
|
1078
|
+
"7",
|
|
1079
|
+
"pwsh.exe",
|
|
1080
|
+
);
|
|
1081
|
+
const realpath =
|
|
1082
|
+
runtime.fs.realpathSync?.native || runtime.fs.realpathSync || null;
|
|
1083
|
+
if (runtime.fs.existsSync(modernHost) && typeof realpath === "function") {
|
|
1084
|
+
try {
|
|
1085
|
+
if (
|
|
1086
|
+
windowsCanonicalPathKey(realpath.call(runtime.fs, modernHost)) ===
|
|
1087
|
+
windowsCanonicalPathKey(modernHost)
|
|
1088
|
+
) {
|
|
1089
|
+
return modernHost;
|
|
1090
|
+
}
|
|
1091
|
+
} catch {
|
|
1092
|
+
// Fall through to the protected in-box host.
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const inboxHost = runtime.joinPath(
|
|
1097
|
+
windowsDirectory,
|
|
1098
|
+
"System32",
|
|
1099
|
+
"WindowsPowerShell",
|
|
1100
|
+
"v1.0",
|
|
1101
|
+
"powershell.exe",
|
|
442
1102
|
);
|
|
1103
|
+
return runtime.fs.existsSync(inboxHost) ? inboxHost : null;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function createFreshWindowsAdapter(runtime, source, hostEnvironment) {
|
|
1107
|
+
const materialized = materializeWindowsAdapter(runtime, source);
|
|
1108
|
+
if (!materialized.assemblyPath) return materialized;
|
|
1109
|
+
|
|
1110
|
+
const powershell = resolveWindowsPowerShellHost(runtime, source.loaderMode);
|
|
1111
|
+
if (source.loaderMode === "powershell-byte-assembly" && !powershell) {
|
|
1112
|
+
const assemblyDeleted = materialized.cleanupAssembly();
|
|
1113
|
+
return {
|
|
1114
|
+
reason: assemblyDeleted
|
|
1115
|
+
? "windows_powershell_host_unavailable"
|
|
1116
|
+
: "windows_native_adapter_compile_cleanup_unverified",
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
let attestation;
|
|
443
1121
|
try {
|
|
444
|
-
|
|
1122
|
+
attestation = inspectWindowsAdapterAssembly(
|
|
1123
|
+
runtime,
|
|
1124
|
+
materialized.assemblyPath,
|
|
1125
|
+
);
|
|
1126
|
+
if (attestation.assemblyDigest !== source.sourceDigest) {
|
|
1127
|
+
throw new Error("Materialized Windows adapter digest mismatch");
|
|
1128
|
+
}
|
|
445
1129
|
} catch {
|
|
446
|
-
|
|
1130
|
+
const assemblyDeleted = materialized.cleanupAssembly();
|
|
1131
|
+
return {
|
|
1132
|
+
reason: assemblyDeleted
|
|
1133
|
+
? "windows_native_adapter_materialize_attestation_failed"
|
|
1134
|
+
: "windows_native_adapter_compile_cleanup_unverified",
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const entry = {
|
|
1139
|
+
runtime,
|
|
1140
|
+
runtimeFs: runtime.fs,
|
|
1141
|
+
tempDirectory: source.tempDirectory,
|
|
1142
|
+
sourceDigest: source.sourceDigest,
|
|
1143
|
+
sourceContractDigest: source.sourceContractDigest,
|
|
1144
|
+
loaderMode: source.loaderMode,
|
|
1145
|
+
powershellPath: powershell,
|
|
1146
|
+
adapterDirectory: materialized.adapterDirectory,
|
|
1147
|
+
assemblyPath: materialized.assemblyPath,
|
|
1148
|
+
assemblyDigest: attestation.assemblyDigest,
|
|
1149
|
+
fileIdentity: attestation.fileIdentity,
|
|
1150
|
+
refCount: 0,
|
|
1151
|
+
idleTimer: null,
|
|
1152
|
+
retired: false,
|
|
1153
|
+
cleaned: false,
|
|
1154
|
+
};
|
|
1155
|
+
const probePayload = materializeWindowsAdapterInvocation(runtime, [
|
|
1156
|
+
"--probe-helper",
|
|
1157
|
+
]);
|
|
1158
|
+
if (!probePayload.payloadPath) {
|
|
1159
|
+
const assemblyDeleted = materialized.cleanupAssembly();
|
|
1160
|
+
return assemblyDeleted
|
|
1161
|
+
? probePayload
|
|
1162
|
+
: { reason: "windows_native_adapter_compile_cleanup_unverified" };
|
|
1163
|
+
}
|
|
1164
|
+
const probeInvocation = buildWindowsAdapterInvocation(entry, probePayload);
|
|
1165
|
+
let probeResult;
|
|
1166
|
+
let probePayloadDeleted = false;
|
|
1167
|
+
try {
|
|
1168
|
+
probeResult = runtime.spawnSync(
|
|
1169
|
+
probeInvocation.command,
|
|
1170
|
+
probeInvocation.args,
|
|
1171
|
+
{
|
|
1172
|
+
shell: false,
|
|
1173
|
+
windowsHide: true,
|
|
1174
|
+
encoding: "utf8",
|
|
1175
|
+
timeout: WINDOWS_SANDBOX_HELPER_OPERATION_TIMEOUT_MS,
|
|
1176
|
+
cwd: runtime.joinPath(runtime.windowsDir(), "System32"),
|
|
1177
|
+
env: hostEnvironment,
|
|
1178
|
+
},
|
|
1179
|
+
);
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
probeResult = { error, status: null };
|
|
1182
|
+
} finally {
|
|
1183
|
+
probePayloadDeleted = probeInvocation.cleanup();
|
|
1184
|
+
}
|
|
1185
|
+
let probeReady = false;
|
|
1186
|
+
try {
|
|
1187
|
+
const probe = JSON.parse(String(probeResult?.stdout || "").trim());
|
|
1188
|
+
probeReady =
|
|
1189
|
+
probeResult?.status === 0 &&
|
|
1190
|
+
probe?.ready === true &&
|
|
1191
|
+
probe?.hostRuntime ===
|
|
1192
|
+
(source.loaderMode === "managed-executable"
|
|
1193
|
+
? "managed-executable-v1"
|
|
1194
|
+
: "powershell-byte-assembly-v1") &&
|
|
1195
|
+
(source.sourceContractDigest === null ||
|
|
1196
|
+
probe?.sourceSha256 === source.sourceContractDigest);
|
|
1197
|
+
} catch {
|
|
1198
|
+
probeReady = false;
|
|
1199
|
+
}
|
|
1200
|
+
if (
|
|
1201
|
+
!probePayloadDeleted ||
|
|
1202
|
+
!probeReady ||
|
|
1203
|
+
!verifyWindowsAdapterEntry(entry, runtime, source)
|
|
1204
|
+
) {
|
|
1205
|
+
const assemblyDeleted = materialized.cleanupAssembly();
|
|
1206
|
+
return {
|
|
1207
|
+
reason:
|
|
1208
|
+
!assemblyDeleted || !probePayloadDeleted
|
|
1209
|
+
? "windows_native_adapter_compile_cleanup_unverified"
|
|
1210
|
+
: "windows_in_memory_adapter_probe_failed",
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
windowsAdapterCacheEntries.add(entry);
|
|
1215
|
+
windowsAdapterCache = entry;
|
|
1216
|
+
registerWindowsAdapterExitCleanup();
|
|
1217
|
+
return { entry };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function acquireWindowsAdapterLease(runtime, source, hostEnvironment) {
|
|
1221
|
+
if (windowsAdapterCache) {
|
|
1222
|
+
if (verifyWindowsAdapterEntry(windowsAdapterCache, runtime, source)) {
|
|
1223
|
+
if (windowsAdapterCache.idleTimer) {
|
|
1224
|
+
clearTimeout(windowsAdapterCache.idleTimer);
|
|
1225
|
+
windowsAdapterCache.idleTimer = null;
|
|
1226
|
+
}
|
|
1227
|
+
windowsAdapterCache.refCount += 1;
|
|
1228
|
+
return {
|
|
1229
|
+
entry: windowsAdapterCache,
|
|
1230
|
+
released: false,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
retireWindowsAdapterEntry(windowsAdapterCache);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const compiled = createFreshWindowsAdapter(runtime, source, hostEnvironment);
|
|
1237
|
+
if (!compiled.entry) return compiled;
|
|
1238
|
+
compiled.entry.refCount += 1;
|
|
1239
|
+
return { entry: compiled.entry, released: false };
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function releaseWindowsAdapterLease(lease) {
|
|
1243
|
+
if (!lease || lease.released) return;
|
|
1244
|
+
lease.released = true;
|
|
1245
|
+
lease.entry.refCount = Math.max(0, lease.entry.refCount - 1);
|
|
1246
|
+
if (lease.entry.retired && lease.entry.refCount === 0) {
|
|
1247
|
+
cleanupWindowsAdapterEntry(lease.entry);
|
|
1248
|
+
} else if (lease.entry.refCount === 0) {
|
|
1249
|
+
scheduleWindowsAdapterIdleCleanup(lease.entry);
|
|
447
1250
|
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function createWindowsAdapterController(runtime, source, hostEnvironment) {
|
|
1254
|
+
const acquired = acquireWindowsAdapterLease(runtime, source, hostEnvironment);
|
|
1255
|
+
if (!acquired.entry) return acquired;
|
|
1256
|
+
let lease = acquired;
|
|
1257
|
+
let released = false;
|
|
1258
|
+
|
|
1259
|
+
const ensureEntry = () => {
|
|
1260
|
+
if (released) {
|
|
1261
|
+
const error = new Error("Windows native adapter lease was released");
|
|
1262
|
+
error.adapterReason = "windows_native_adapter_lease_released";
|
|
1263
|
+
throw error;
|
|
1264
|
+
}
|
|
1265
|
+
if (!verifyWindowsAdapterEntry(lease.entry, runtime, source)) {
|
|
1266
|
+
retireWindowsAdapterEntry(lease.entry);
|
|
1267
|
+
releaseWindowsAdapterLease(lease);
|
|
1268
|
+
const refreshed = acquireWindowsAdapterLease(
|
|
1269
|
+
runtime,
|
|
1270
|
+
source,
|
|
1271
|
+
hostEnvironment,
|
|
1272
|
+
);
|
|
1273
|
+
if (!refreshed.entry) {
|
|
1274
|
+
const error = new Error(
|
|
1275
|
+
`Windows native adapter refresh failed: ${refreshed.reason}`,
|
|
1276
|
+
);
|
|
1277
|
+
error.adapterReason = refreshed.reason;
|
|
1278
|
+
throw error;
|
|
1279
|
+
}
|
|
1280
|
+
lease = refreshed;
|
|
1281
|
+
}
|
|
1282
|
+
return lease.entry;
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
const createInvocation = (helperArgs) => {
|
|
1286
|
+
const entry = ensureEntry();
|
|
1287
|
+
const payload = materializeWindowsAdapterInvocation(runtime, helperArgs);
|
|
1288
|
+
if (!payload.payloadPath) {
|
|
1289
|
+
const error = new Error(
|
|
1290
|
+
`Windows in-memory adapter invocation failed: ${payload.reason}`,
|
|
1291
|
+
);
|
|
1292
|
+
error.adapterReason = payload.reason;
|
|
1293
|
+
throw error;
|
|
1294
|
+
}
|
|
1295
|
+
return buildWindowsAdapterInvocation(entry, payload);
|
|
1296
|
+
};
|
|
1297
|
+
|
|
448
1298
|
return {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1299
|
+
ensureExecutable: ensureEntry,
|
|
1300
|
+
createInvocation,
|
|
1301
|
+
spawnSync: (args, options) => {
|
|
1302
|
+
const invocation = createInvocation(args);
|
|
1303
|
+
let result;
|
|
1304
|
+
let failure;
|
|
453
1305
|
try {
|
|
454
|
-
runtime.
|
|
455
|
-
|
|
456
|
-
|
|
1306
|
+
result = runtime.spawnSync(invocation.command, invocation.args, {
|
|
1307
|
+
...options,
|
|
1308
|
+
cwd: runtime.joinPath(runtime.windowsDir(), "System32"),
|
|
1309
|
+
env: hostEnvironment,
|
|
1310
|
+
});
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
failure = error;
|
|
1313
|
+
}
|
|
1314
|
+
if (!invocation.cleanup()) {
|
|
1315
|
+
const cleanupError = new Error(
|
|
1316
|
+
"Windows sandbox adapter invocation cleanup could not be verified",
|
|
1317
|
+
failure ? { cause: failure } : undefined,
|
|
1318
|
+
);
|
|
1319
|
+
cleanupError.code = "ERR_WINDOWS_SANDBOX_INVOCATION_CLEANUP";
|
|
1320
|
+
throw cleanupError;
|
|
457
1321
|
}
|
|
1322
|
+
if (failure) throw failure;
|
|
1323
|
+
return result;
|
|
1324
|
+
},
|
|
1325
|
+
release: () => {
|
|
1326
|
+
if (released) return;
|
|
1327
|
+
released = true;
|
|
1328
|
+
releaseWindowsAdapterLease(lease);
|
|
458
1329
|
},
|
|
459
1330
|
};
|
|
460
1331
|
}
|
|
@@ -463,7 +1334,8 @@ function waitForWindowsTargetIdentity(
|
|
|
463
1334
|
proc,
|
|
464
1335
|
identityPath,
|
|
465
1336
|
runtime,
|
|
466
|
-
|
|
1337
|
+
expectedAppContainer = null,
|
|
1338
|
+
timeoutMs = 30_000,
|
|
467
1339
|
) {
|
|
468
1340
|
const deadline = runtime.now() + timeoutMs;
|
|
469
1341
|
let lastError;
|
|
@@ -496,15 +1368,50 @@ function waitForWindowsTargetIdentity(
|
|
|
496
1368
|
"Windows sandbox target PID unexpectedly equals its helper PID",
|
|
497
1369
|
);
|
|
498
1370
|
}
|
|
1371
|
+
if (expectedAppContainer) {
|
|
1372
|
+
if (identity?.appContainer !== true) {
|
|
1373
|
+
throw new Error(
|
|
1374
|
+
"Windows sandbox identity did not attest an AppContainer target",
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
if (
|
|
1378
|
+
identity?.appContainerSid !== expectedAppContainer.appContainerSid
|
|
1379
|
+
) {
|
|
1380
|
+
throw new Error(
|
|
1381
|
+
"Windows sandbox identity named an unexpected AppContainer SID",
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
if (identity?.capabilityCount !== 0) {
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
"Windows sandbox identity did not attest zero capabilities",
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
499
1390
|
|
|
500
1391
|
const wrapperPid = proc.pid;
|
|
501
1392
|
proc.sandboxWrapperPid = wrapperPid;
|
|
502
1393
|
proc.sandboxTargetPid = targetPid;
|
|
1394
|
+
if (expectedAppContainer) {
|
|
1395
|
+
proc.sandboxAppContainerProfile =
|
|
1396
|
+
expectedAppContainer.appContainerProfileName;
|
|
1397
|
+
proc.sandboxAppContainerSid = expectedAppContainer.appContainerSid;
|
|
1398
|
+
}
|
|
503
1399
|
// ChildProcess.pid is a writable data property. The native ChildProcess
|
|
504
1400
|
// handle still points at the helper, so kill()/ref()/unref() retain Job
|
|
505
1401
|
// lifetime semantics while callers observe and persist the real target.
|
|
506
1402
|
proc.pid = targetPid;
|
|
507
|
-
return {
|
|
1403
|
+
return {
|
|
1404
|
+
targetPid,
|
|
1405
|
+
wrapperPid,
|
|
1406
|
+
...(expectedAppContainer
|
|
1407
|
+
? {
|
|
1408
|
+
appContainerProfileName:
|
|
1409
|
+
expectedAppContainer.appContainerProfileName,
|
|
1410
|
+
appContainerSid: expectedAppContainer.appContainerSid,
|
|
1411
|
+
capabilityCount: 0,
|
|
1412
|
+
}
|
|
1413
|
+
: {}),
|
|
1414
|
+
};
|
|
508
1415
|
} catch (error) {
|
|
509
1416
|
lastError = error;
|
|
510
1417
|
if (
|
|
@@ -524,38 +1431,637 @@ function waitForWindowsTargetIdentity(
|
|
|
524
1431
|
);
|
|
525
1432
|
}
|
|
526
1433
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
1434
|
+
function windowsNodeSnapshotProbeResult(result) {
|
|
1435
|
+
const runtimeProbe = {
|
|
1436
|
+
kind: "windows-plugin-node-entry-snapshot-v1",
|
|
1437
|
+
attempted: true,
|
|
1438
|
+
runnable: false,
|
|
1439
|
+
reason: "probe_failed",
|
|
1440
|
+
probeRuntime: "node",
|
|
1441
|
+
targetRuntime: "node",
|
|
1442
|
+
};
|
|
1443
|
+
if (result?.error || result?.status !== 0) {
|
|
1444
|
+
return {
|
|
1445
|
+
...runtimeProbe,
|
|
1446
|
+
reason: windowsSandboxHelperFailureReason(result, "probe_failed"),
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
try {
|
|
1451
|
+
const attestation = JSON.parse(String(result.stdout || "").trim());
|
|
1452
|
+
if (
|
|
1453
|
+
attestation?.ready !== true ||
|
|
1454
|
+
attestation?.targetRuntime !== "node" ||
|
|
1455
|
+
attestation?.contentSnapshot !== true
|
|
1456
|
+
) {
|
|
1457
|
+
return { ...runtimeProbe, reason: "invalid_attestation" };
|
|
1458
|
+
}
|
|
1459
|
+
} catch {
|
|
1460
|
+
return { ...runtimeProbe, reason: "invalid_attestation" };
|
|
1461
|
+
}
|
|
1462
|
+
return { ...runtimeProbe, runnable: true, reason: null };
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function windowsSandboxHelperFailureReason(result, fallback) {
|
|
1466
|
+
if (result?.error) {
|
|
1467
|
+
const errorCode = String(result.error.code || "").toLowerCase();
|
|
1468
|
+
if (
|
|
1469
|
+
errorCode === "etimedout" ||
|
|
1470
|
+
result.error.killed === true ||
|
|
1471
|
+
result.error.signal
|
|
1472
|
+
) {
|
|
1473
|
+
return `${fallback}_timeout`;
|
|
1474
|
+
}
|
|
1475
|
+
if (/^[a-z0-9_]+$/.test(errorCode)) {
|
|
1476
|
+
return `${fallback}_spawn_${errorCode}`;
|
|
1477
|
+
}
|
|
1478
|
+
return `${fallback}_spawn_error`;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
if (result?.status === 0) return fallback;
|
|
1482
|
+
const status = Number.isSafeInteger(result?.status)
|
|
1483
|
+
? result.status
|
|
1484
|
+
: "unknown";
|
|
1485
|
+
const stderr = String(result?.stderr || "");
|
|
1486
|
+
const knownFailures = [
|
|
1487
|
+
[
|
|
1488
|
+
/CreateAppContainerProfile\(cleanup verification\)/i,
|
|
1489
|
+
"appcontainer_cleanup_verification_create",
|
|
1490
|
+
],
|
|
1491
|
+
[/CreateAppContainerProfile/i, "appcontainer_profile_create"],
|
|
1492
|
+
[/DeleteAppContainerProfile/i, "appcontainer_profile_delete"],
|
|
1493
|
+
[
|
|
1494
|
+
/CreateProcess(?:AsUser)?\(AppContainer\)/i,
|
|
1495
|
+
"appcontainer_process_create",
|
|
1496
|
+
],
|
|
1497
|
+
[/Target application contains a reparse component/i, "target_reparse"],
|
|
1498
|
+
[
|
|
1499
|
+
/Target working directory contains a reparse component/i,
|
|
1500
|
+
"working_directory_reparse",
|
|
1501
|
+
],
|
|
1502
|
+
[
|
|
1503
|
+
/Target PATH directory contains a reparse component/i,
|
|
1504
|
+
"path_directory_reparse",
|
|
1505
|
+
],
|
|
1506
|
+
[/Target application must use a local DOS drive/i, "target_nonlocal"],
|
|
1507
|
+
[
|
|
1508
|
+
/Target working directory must use a local DOS drive/i,
|
|
1509
|
+
"working_directory_nonlocal",
|
|
1510
|
+
],
|
|
1511
|
+
[
|
|
1512
|
+
/Node entry snapshot probe returned a non-zero exit code/i,
|
|
1513
|
+
"node_snapshot_nonzero",
|
|
1514
|
+
],
|
|
1515
|
+
[
|
|
1516
|
+
/Node entry snapshot probe did not execute its verified source/i,
|
|
1517
|
+
"node_snapshot_unverified",
|
|
1518
|
+
],
|
|
1519
|
+
[
|
|
1520
|
+
/AppContainer readiness target returned a non-zero exit code/i,
|
|
1521
|
+
"appcontainer_readiness_nonzero",
|
|
1522
|
+
],
|
|
1523
|
+
[
|
|
1524
|
+
/AppContainer SID changed during readiness attestation/i,
|
|
1525
|
+
"appcontainer_sid_changed",
|
|
1526
|
+
],
|
|
1527
|
+
];
|
|
1528
|
+
const classified = knownFailures.find(([pattern]) => pattern.test(stderr));
|
|
1529
|
+
const nativeCode = stderr.match(
|
|
1530
|
+
/\b(?:hresult|win32)=(0x[0-9a-f]+|\d+)\b/i,
|
|
1531
|
+
)?.[1];
|
|
1532
|
+
return [
|
|
1533
|
+
fallback,
|
|
1534
|
+
`helper_exit_${status}`,
|
|
1535
|
+
classified?.[1],
|
|
1536
|
+
nativeCode?.toLowerCase(),
|
|
1537
|
+
]
|
|
1538
|
+
.filter(Boolean)
|
|
1539
|
+
.join("_");
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function appContainerReadinessResult(
|
|
1543
|
+
result,
|
|
1544
|
+
profileName,
|
|
1545
|
+
targetRuntime = null,
|
|
1546
|
+
) {
|
|
1547
|
+
const runtimeProbe = {
|
|
1548
|
+
kind: "windows-appcontainer-launch-attestation-v1",
|
|
1549
|
+
attempted: true,
|
|
1550
|
+
runnable: false,
|
|
1551
|
+
reason: "probe_failed",
|
|
1552
|
+
};
|
|
1553
|
+
if (result?.error || result?.status !== 0) {
|
|
1554
|
+
return {
|
|
1555
|
+
runtimeProbe: {
|
|
1556
|
+
...runtimeProbe,
|
|
1557
|
+
reason: windowsSandboxHelperFailureReason(result, "probe_failed"),
|
|
1558
|
+
},
|
|
1559
|
+
readiness: null,
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
let readiness;
|
|
1564
|
+
try {
|
|
1565
|
+
readiness = JSON.parse(String(result.stdout || "").trim());
|
|
1566
|
+
} catch {
|
|
1567
|
+
return {
|
|
1568
|
+
runtimeProbe: {
|
|
1569
|
+
...runtimeProbe,
|
|
1570
|
+
reason: "invalid_attestation",
|
|
1571
|
+
},
|
|
1572
|
+
readiness: null,
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
const sidPattern = /^S-1-15-2(?:-\d+){7}$/;
|
|
1576
|
+
if (
|
|
1577
|
+
readiness?.ready !== true ||
|
|
1578
|
+
readiness?.profileName !== profileName ||
|
|
1579
|
+
typeof readiness?.appContainerSid !== "string" ||
|
|
1580
|
+
!sidPattern.test(readiness.appContainerSid) ||
|
|
1581
|
+
readiness?.capabilityCount !== 0 ||
|
|
1582
|
+
readiness?.tokenAttested !== true ||
|
|
1583
|
+
readiness?.restrictedTokenAttested !== true ||
|
|
1584
|
+
(targetRuntime !== null &&
|
|
1585
|
+
(readiness?.probeRuntime !== targetRuntime ||
|
|
1586
|
+
readiness?.targetRuntime !== targetRuntime))
|
|
1587
|
+
) {
|
|
1588
|
+
return {
|
|
1589
|
+
runtimeProbe: {
|
|
1590
|
+
...runtimeProbe,
|
|
1591
|
+
reason: "invalid_attestation",
|
|
1592
|
+
},
|
|
1593
|
+
readiness: null,
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
return {
|
|
1597
|
+
runtimeProbe: {
|
|
1598
|
+
...runtimeProbe,
|
|
1599
|
+
runnable: true,
|
|
1600
|
+
reason: null,
|
|
1601
|
+
...(targetRuntime
|
|
1602
|
+
? {
|
|
1603
|
+
probeRuntime: targetRuntime,
|
|
1604
|
+
targetRuntime,
|
|
1605
|
+
}
|
|
1606
|
+
: {}),
|
|
1607
|
+
},
|
|
1608
|
+
readiness,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function deleteWindowsAppContainerProfile(
|
|
1613
|
+
adapter,
|
|
1614
|
+
profileName,
|
|
1615
|
+
expectedSid = null,
|
|
1616
|
+
failure = null,
|
|
1617
|
+
) {
|
|
1618
|
+
const helperArgs = ["--delete-appcontainer", profileName];
|
|
1619
|
+
if (expectedSid) helperArgs.push(expectedSid);
|
|
1620
|
+
try {
|
|
1621
|
+
const result = adapter.spawnSync(helperArgs, {
|
|
1622
|
+
shell: false,
|
|
1623
|
+
windowsHide: true,
|
|
1624
|
+
encoding: "utf8",
|
|
1625
|
+
timeout: WINDOWS_SANDBOX_HELPER_OPERATION_TIMEOUT_MS,
|
|
1626
|
+
});
|
|
1627
|
+
if (result?.error || result?.status !== 0) {
|
|
1628
|
+
if (failure) {
|
|
1629
|
+
failure.reason = windowsSandboxHelperFailureReason(
|
|
1630
|
+
result,
|
|
1631
|
+
"cleanup_failed",
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
return false;
|
|
1635
|
+
}
|
|
1636
|
+
let deletion;
|
|
1637
|
+
try {
|
|
1638
|
+
deletion = JSON.parse(String(result.stdout || "").trim());
|
|
1639
|
+
} catch {
|
|
1640
|
+
if (failure) {
|
|
1641
|
+
failure.reason = "cleanup_failed_invalid_attestation";
|
|
1642
|
+
}
|
|
1643
|
+
return false;
|
|
1644
|
+
}
|
|
1645
|
+
const deleted =
|
|
1646
|
+
deletion?.deleted === true &&
|
|
1647
|
+
deletion?.absent === true &&
|
|
1648
|
+
deletion?.profileName === profileName;
|
|
1649
|
+
if (!deleted && failure) {
|
|
1650
|
+
failure.reason = "cleanup_failed_invalid_attestation";
|
|
1651
|
+
}
|
|
1652
|
+
return deleted;
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
if (failure) {
|
|
1655
|
+
failure.reason = windowsSandboxHelperFailureReason(
|
|
1656
|
+
{ error, status: null },
|
|
1657
|
+
"cleanup_failed",
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
return false;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function appContainerCleanupFailureReason(readinessReason, cleanupReason) {
|
|
1665
|
+
const readiness =
|
|
1666
|
+
typeof readinessReason === "string" && readinessReason
|
|
1667
|
+
? readinessReason
|
|
1668
|
+
: "unknown_readiness";
|
|
1669
|
+
const cleanup =
|
|
1670
|
+
typeof cleanupReason === "string" && cleanupReason
|
|
1671
|
+
? cleanupReason
|
|
1672
|
+
: "unknown_cleanup";
|
|
1673
|
+
return `cleanup_unverified_after_${readiness}_because_${cleanup}`;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function retryWindowsAppContainerCleanup(record) {
|
|
1677
|
+
if (record.cleaned) return true;
|
|
1678
|
+
let adapter;
|
|
1679
|
+
try {
|
|
1680
|
+
adapter = createWindowsAdapterController(
|
|
1681
|
+
record.runtime,
|
|
1682
|
+
record.source,
|
|
1683
|
+
record.hostEnvironment,
|
|
1684
|
+
);
|
|
1685
|
+
if (!adapter.ensureExecutable) return false;
|
|
1686
|
+
if (
|
|
1687
|
+
!deleteWindowsAppContainerProfile(
|
|
1688
|
+
adapter,
|
|
1689
|
+
record.profileName,
|
|
1690
|
+
record.expectedSid,
|
|
1691
|
+
)
|
|
1692
|
+
) {
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
record.cleaned = true;
|
|
1696
|
+
if (record.automaticTimer) {
|
|
1697
|
+
clearTimeout(record.automaticTimer);
|
|
1698
|
+
record.automaticTimer = null;
|
|
1699
|
+
}
|
|
1700
|
+
windowsAppContainerCleanupBacklog.delete(record);
|
|
1701
|
+
return true;
|
|
1702
|
+
} catch {
|
|
1703
|
+
return false;
|
|
1704
|
+
} finally {
|
|
1705
|
+
adapter?.release?.();
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function retryWindowsAppContainerCleanupBacklog() {
|
|
1710
|
+
let cleaned = true;
|
|
1711
|
+
for (const record of [...windowsAppContainerCleanupBacklog]) {
|
|
1712
|
+
if (!retryWindowsAppContainerCleanup(record)) cleaned = false;
|
|
1713
|
+
}
|
|
1714
|
+
return cleaned;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function scheduleWindowsAppContainerCleanupRetry(record) {
|
|
1718
|
+
if (
|
|
1719
|
+
record.cleaned ||
|
|
1720
|
+
record.automaticTimer ||
|
|
1721
|
+
record.automaticAttempts >=
|
|
1722
|
+
WINDOWS_APPCONTAINER_AUTOMATIC_CLEANUP_DELAYS_MS.length
|
|
1723
|
+
) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
record.automaticTimer = setTimeout(() => {
|
|
1727
|
+
record.automaticTimer = null;
|
|
1728
|
+
if (record.cleaned || !windowsAppContainerCleanupBacklog.has(record)) {
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
record.automaticAttempts += 1;
|
|
1732
|
+
if (!retryWindowsAppContainerCleanup(record)) {
|
|
1733
|
+
scheduleWindowsAppContainerCleanupRetry(record);
|
|
1734
|
+
}
|
|
1735
|
+
}, WINDOWS_APPCONTAINER_AUTOMATIC_CLEANUP_DELAYS_MS[record.automaticAttempts]);
|
|
1736
|
+
record.automaticTimer.unref?.();
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function trackWindowsAppContainerCleanup(
|
|
1740
|
+
runtime,
|
|
1741
|
+
source,
|
|
1742
|
+
hostEnvironment,
|
|
1743
|
+
profileName,
|
|
1744
|
+
expectedSid,
|
|
1745
|
+
) {
|
|
1746
|
+
let record = [...windowsAppContainerCleanupBacklog].find(
|
|
1747
|
+
(candidate) =>
|
|
1748
|
+
candidate.runtime === runtime && candidate.profileName === profileName,
|
|
1749
|
+
);
|
|
1750
|
+
if (!record) {
|
|
1751
|
+
record = {
|
|
1752
|
+
runtime,
|
|
1753
|
+
source,
|
|
1754
|
+
hostEnvironment,
|
|
1755
|
+
profileName,
|
|
1756
|
+
expectedSid,
|
|
1757
|
+
cleaned: false,
|
|
1758
|
+
automaticAttempts: 0,
|
|
1759
|
+
automaticTimer: null,
|
|
1760
|
+
};
|
|
1761
|
+
windowsAppContainerCleanupBacklog.add(record);
|
|
1762
|
+
} else if (!record.expectedSid && expectedSid) {
|
|
1763
|
+
record.expectedSid = expectedSid;
|
|
1764
|
+
}
|
|
1765
|
+
registerWindowsAdapterExitCleanup();
|
|
1766
|
+
scheduleWindowsAppContainerCleanupRetry(record);
|
|
1767
|
+
return record;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function windowsCanonicalPathKey(value) {
|
|
1771
|
+
if (typeof value !== "string" || !value || value.includes("\0")) return null;
|
|
1772
|
+
try {
|
|
1773
|
+
return path.win32.resolve(value).toLowerCase();
|
|
1774
|
+
} catch {
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function sanitizeWindowsSandboxEnvironment(environment, pluginNodeSnapshot) {
|
|
1780
|
+
const sanitized = {};
|
|
1781
|
+
const normalizedKeys = new Map();
|
|
1782
|
+
for (const [key, value] of Object.entries(environment || process.env)) {
|
|
1783
|
+
const normalized = key.toUpperCase();
|
|
1784
|
+
if (
|
|
1785
|
+
WINDOWS_SANDBOX_HOST_ENV_DENYLIST.has(normalized) ||
|
|
1786
|
+
(pluginNodeSnapshot && WINDOWS_PLUGIN_NODE_ENV_DENYLIST.has(normalized))
|
|
1787
|
+
) {
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
const previousKey = normalizedKeys.get(normalized);
|
|
1791
|
+
if (previousKey !== undefined) delete sanitized[previousKey];
|
|
1792
|
+
normalizedKeys.set(normalized, key);
|
|
1793
|
+
sanitized[key] = String(value);
|
|
1794
|
+
}
|
|
1795
|
+
return sanitized;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
function setWindowsEnvironmentValue(environment, name, value) {
|
|
1799
|
+
const normalized = name.toUpperCase();
|
|
1800
|
+
for (const key of Object.keys(environment)) {
|
|
1801
|
+
if (key.toUpperCase() === normalized) delete environment[key];
|
|
1802
|
+
}
|
|
1803
|
+
environment[name] = String(value);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function windowsSandboxHostEnvironment(runtime) {
|
|
1807
|
+
const windowsDirectory = path.win32.resolve(runtime.windowsDir());
|
|
1808
|
+
const systemDirectory = runtime.joinPath(windowsDirectory, "System32");
|
|
1809
|
+
const temporaryDirectory = runtime.joinPath(windowsDirectory, "Temp");
|
|
1810
|
+
return {
|
|
1811
|
+
SystemRoot: windowsDirectory,
|
|
1812
|
+
WINDIR: windowsDirectory,
|
|
1813
|
+
ComSpec: runtime.joinPath(systemDirectory, "cmd.exe"),
|
|
1814
|
+
PATH: `${systemDirectory};${windowsDirectory}`,
|
|
1815
|
+
PATHEXT: ".COM;.EXE;.BAT;.CMD",
|
|
1816
|
+
TEMP: temporaryDirectory,
|
|
1817
|
+
TMP: temporaryDirectory,
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function windowsPluginNodeEntrySnapshot(invocation, sandboxOpts, spawnOpts) {
|
|
1822
|
+
const contract = sandboxOpts.executionContract;
|
|
1823
|
+
if (!contract) return { locks: null, reason: null };
|
|
1824
|
+
if (contract.kind !== "strict-plugin-node-bin") {
|
|
1825
|
+
return {
|
|
1826
|
+
locks: null,
|
|
1827
|
+
reason: "windows_plugin_execution_contract_unsupported",
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
if (sandboxOpts.sync !== true || spawnOpts?.detached === true) {
|
|
1831
|
+
return {
|
|
1832
|
+
locks: null,
|
|
1833
|
+
reason: "windows_plugin_launch_path_lock_requires_sync_foreground",
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
const runtimeIdentity = contract.runtimeIdentity;
|
|
1838
|
+
const entryIdentity = contract.entryIdentity;
|
|
1839
|
+
const validIdentity = (identity, maxBytes) =>
|
|
1840
|
+
identity &&
|
|
1841
|
+
typeof identity === "object" &&
|
|
1842
|
+
typeof identity.realPath === "string" &&
|
|
1843
|
+
path.win32.isAbsolute(identity.realPath) &&
|
|
1844
|
+
typeof identity.sha256 === "string" &&
|
|
1845
|
+
/^[a-f0-9]{64}$/.test(identity.sha256) &&
|
|
1846
|
+
identity.fileId &&
|
|
1847
|
+
typeof identity.fileId === "object" &&
|
|
1848
|
+
typeof identity.fileId.dev === "string" &&
|
|
1849
|
+
typeof identity.fileId.ino === "string" &&
|
|
1850
|
+
/^\d+$/.test(identity.fileId.dev) &&
|
|
1851
|
+
/^\d+$/.test(identity.fileId.ino) &&
|
|
1852
|
+
Number.isSafeInteger(identity.bytes) &&
|
|
1853
|
+
identity.bytes >= 0 &&
|
|
1854
|
+
identity.bytes <= maxBytes;
|
|
1855
|
+
if (
|
|
1856
|
+
!validIdentity(runtimeIdentity, 256 * 1024 * 1024) ||
|
|
1857
|
+
!validIdentity(entryIdentity, 64 * 1024 * 1024)
|
|
1858
|
+
) {
|
|
1859
|
+
return {
|
|
1860
|
+
locks: null,
|
|
1861
|
+
reason: "windows_plugin_launch_path_identity_invalid",
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const runtimePath = windowsCanonicalPathKey(runtimeIdentity.realPath);
|
|
1866
|
+
const entryPath = windowsCanonicalPathKey(entryIdentity.realPath);
|
|
1867
|
+
if (
|
|
1868
|
+
!runtimePath ||
|
|
1869
|
+
!entryPath ||
|
|
1870
|
+
runtimePath !== windowsCanonicalPathKey(contract.runtimePath) ||
|
|
1871
|
+
runtimePath !== windowsCanonicalPathKey(invocation.command) ||
|
|
1872
|
+
entryPath !== windowsCanonicalPathKey(invocation.args?.[0])
|
|
1873
|
+
) {
|
|
1874
|
+
return {
|
|
1875
|
+
locks: null,
|
|
1876
|
+
reason: "windows_plugin_launch_path_identity_mismatch",
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
if (path.win32.extname(entryIdentity.realPath).toLowerCase() !== ".cjs") {
|
|
1880
|
+
return {
|
|
1881
|
+
locks: null,
|
|
1882
|
+
reason: "windows_plugin_entry_snapshot_format_unsupported",
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
return {
|
|
1887
|
+
locks: [
|
|
1888
|
+
{
|
|
1889
|
+
role: "runtime",
|
|
1890
|
+
path: runtimeIdentity.realPath,
|
|
1891
|
+
sha256: runtimeIdentity.sha256,
|
|
1892
|
+
bytes: runtimeIdentity.bytes,
|
|
1893
|
+
dev: runtimeIdentity.fileId.dev,
|
|
1894
|
+
ino: runtimeIdentity.fileId.ino,
|
|
1895
|
+
},
|
|
1896
|
+
{
|
|
1897
|
+
role: "entry",
|
|
1898
|
+
path: entryIdentity.realPath,
|
|
1899
|
+
sha256: entryIdentity.sha256,
|
|
1900
|
+
bytes: entryIdentity.bytes,
|
|
1901
|
+
dev: entryIdentity.fileId.dev,
|
|
1902
|
+
ino: entryIdentity.fileId.ino,
|
|
1903
|
+
},
|
|
1904
|
+
],
|
|
1905
|
+
reason: null,
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function windowsAppContainerPolicyDigest({
|
|
1910
|
+
profile,
|
|
1911
|
+
requiredBoundaries,
|
|
1912
|
+
guarantees,
|
|
1913
|
+
adapterSource,
|
|
1914
|
+
launchSpec,
|
|
1915
|
+
entrySnapshot,
|
|
1916
|
+
executionContract,
|
|
1917
|
+
}) {
|
|
1918
|
+
// Bind the effective enforcement policy and attested executable snapshot.
|
|
1919
|
+
// Transient profile names/SIDs and caller payload values are intentionally
|
|
1920
|
+
// excluded so identical policy identities remain stable across launches.
|
|
1921
|
+
const snapshotLocks = entrySnapshot.locks
|
|
1922
|
+
? entrySnapshot.locks.map(
|
|
1923
|
+
({ role, path: lockPath, sha256, bytes, dev, ino }) => ({
|
|
1924
|
+
role,
|
|
1925
|
+
path: lockPath,
|
|
1926
|
+
sha256,
|
|
1927
|
+
bytes,
|
|
1928
|
+
dev,
|
|
1929
|
+
ino,
|
|
1930
|
+
}),
|
|
1931
|
+
)
|
|
1932
|
+
: [];
|
|
1933
|
+
return sha256(
|
|
1934
|
+
JSON.stringify({
|
|
1935
|
+
version: 1,
|
|
1936
|
+
backend: WINDOWS_APPCONTAINER_BACKEND,
|
|
1937
|
+
profile,
|
|
1938
|
+
requiredBoundaries: [...new Set(requiredBoundaries)].sort(),
|
|
1939
|
+
guarantees: [...new Set(guarantees)].sort(),
|
|
1940
|
+
adapter: {
|
|
1941
|
+
loaderMode: adapterSource.loaderMode,
|
|
1942
|
+
sourceDigest: adapterSource.sourceDigest,
|
|
1943
|
+
sourceContractDigest: adapterSource.sourceContractDigest,
|
|
1944
|
+
},
|
|
1945
|
+
appContainer: {
|
|
1946
|
+
attestationKind: "windows-appcontainer-launch-attestation-v1",
|
|
1947
|
+
capabilities: [],
|
|
1948
|
+
token: "restricted-primary-lowbox",
|
|
1949
|
+
disableAdministratorSids: launchSpec.disableAdministratorSids,
|
|
1950
|
+
allowReparsePaths: launchSpec.allowReparsePaths,
|
|
1951
|
+
lifecycle: "ephemeral-delete-and-assert-absent",
|
|
1952
|
+
},
|
|
1953
|
+
job: {
|
|
1954
|
+
killOnClose: true,
|
|
1955
|
+
activeProcessLimit: launchSpec.activeProcessLimit,
|
|
1956
|
+
cpuSeconds: launchSpec.cpuSeconds,
|
|
1957
|
+
processMemoryBytes: launchSpec.processMemoryBytes,
|
|
1958
|
+
},
|
|
1959
|
+
execution: {
|
|
1960
|
+
contractKind: executionContract?.kind || null,
|
|
1961
|
+
contentSnapshot: snapshotLocks.length > 0,
|
|
1962
|
+
contentSnapshotScope:
|
|
1963
|
+
snapshotLocks.length > 0 ? "plugin-entry-source" : null,
|
|
1964
|
+
contentSnapshotMechanism:
|
|
1965
|
+
snapshotLocks.length > 0
|
|
1966
|
+
? "verified-handle-inherited-pipe-module-compile-v1"
|
|
1967
|
+
: null,
|
|
1968
|
+
handleAtomic: false,
|
|
1969
|
+
launchPathLocks: snapshotLocks,
|
|
1970
|
+
},
|
|
1971
|
+
}),
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
/**
|
|
1976
|
+
* Wrap a target with a protected Windows PowerShell host. PowerShell 7 is
|
|
1977
|
+
* preferred only when its canonical Program Files path is intact; otherwise
|
|
1978
|
+
* the in-box System32 host remains the fallback. The shipped byte-loaded helper
|
|
1979
|
+
* P/Invokes Win32 to create a restricted primary token, starts the target
|
|
1980
|
+
* suspended, assigns it to a kill-on-close Job Object, then resumes it.
|
|
1981
|
+
* Filesystem/network requests additionally require a synchronous real-launch
|
|
1982
|
+
* AppContainer probe and suspended-target token/SID attestation.
|
|
1983
|
+
*
|
|
1984
|
+
* @param {string} command
|
|
1985
|
+
* @param {string[]} args
|
|
1986
|
+
* @param {Object} spawnOpts
|
|
1987
|
+
* @param {Object} sandboxOpts
|
|
1988
|
+
*/
|
|
1989
|
+
export function applyWindowsSandbox(
|
|
1990
|
+
command,
|
|
1991
|
+
args,
|
|
1992
|
+
spawnOpts,
|
|
1993
|
+
sandboxOpts = {},
|
|
1994
|
+
runtimeOverrides = {},
|
|
1995
|
+
) {
|
|
1996
|
+
let runtime = resolveRuntime(runtimeOverrides);
|
|
1997
|
+
const requiredBoundaries = Array.isArray(sandboxOpts.requiredBoundaries)
|
|
1998
|
+
? sandboxOpts.requiredBoundaries
|
|
1999
|
+
: [];
|
|
2000
|
+
const requiresAppContainer = requiredBoundaries.some(
|
|
2001
|
+
(boundary) =>
|
|
2002
|
+
boundary === SANDBOX_BOUNDARIES.FILESYSTEM ||
|
|
2003
|
+
boundary === SANDBOX_BOUNDARIES.NETWORK,
|
|
2004
|
+
);
|
|
2005
|
+
const backend = requiresAppContainer
|
|
2006
|
+
? WINDOWS_APPCONTAINER_BACKEND
|
|
2007
|
+
: WINDOWS_RESTRICTED_TOKEN_BACKEND;
|
|
2008
|
+
const appContainerGuarantees = [
|
|
2009
|
+
SANDBOX_BOUNDARIES.FILESYSTEM,
|
|
2010
|
+
SANDBOX_BOUNDARIES.NETWORK,
|
|
2011
|
+
SANDBOX_BOUNDARIES.PROCESS_TREE,
|
|
2012
|
+
SANDBOX_BOUNDARIES.RESOURCE_LIMITS,
|
|
2013
|
+
SANDBOX_BOUNDARIES.PRIVILEGE_REDUCTION,
|
|
2014
|
+
];
|
|
2015
|
+
const base = {
|
|
2016
|
+
platform: runtime.platform,
|
|
2017
|
+
profile: sandboxOpts.profileName || "default",
|
|
2018
|
+
backend,
|
|
550
2019
|
command,
|
|
551
2020
|
args,
|
|
552
2021
|
options: spawnOpts,
|
|
553
2022
|
};
|
|
554
|
-
|
|
555
|
-
|
|
2023
|
+
let appContainerPolicyDigest = null;
|
|
2024
|
+
const unavailablePlan = (reason, extra = {}) =>
|
|
2025
|
+
createSandboxPlan({
|
|
556
2026
|
...base,
|
|
557
|
-
|
|
2027
|
+
...(requiresAppContainer
|
|
2028
|
+
? {
|
|
2029
|
+
backend: null,
|
|
2030
|
+
candidateBackend: WINDOWS_APPCONTAINER_BACKEND,
|
|
2031
|
+
policyAttested: false,
|
|
2032
|
+
policyDigest: appContainerPolicyDigest,
|
|
2033
|
+
}
|
|
2034
|
+
: {}),
|
|
2035
|
+
reason,
|
|
2036
|
+
...extra,
|
|
558
2037
|
});
|
|
2038
|
+
if (runtime.platform !== "win32") {
|
|
2039
|
+
return unavailablePlan("platform_mismatch");
|
|
2040
|
+
}
|
|
2041
|
+
let windowsDirectory;
|
|
2042
|
+
try {
|
|
2043
|
+
windowsDirectory = path.win32.resolve(runtime.windowsDir());
|
|
2044
|
+
} catch {
|
|
2045
|
+
windowsDirectory = null;
|
|
2046
|
+
}
|
|
2047
|
+
if (
|
|
2048
|
+
typeof windowsDirectory !== "string" ||
|
|
2049
|
+
!/^[A-Za-z]:\\/.test(windowsDirectory) ||
|
|
2050
|
+
windowsDirectory.includes("\0")
|
|
2051
|
+
) {
|
|
2052
|
+
return unavailablePlan("windows_trusted_system_root_unavailable");
|
|
2053
|
+
}
|
|
2054
|
+
// Resolve once per plan so even trusted embedding hooks cannot redirect the
|
|
2055
|
+
// host executable, cwd, and environment between individual launch steps.
|
|
2056
|
+
runtime = { ...runtime, windowsDir: () => windowsDirectory };
|
|
2057
|
+
|
|
2058
|
+
if (
|
|
2059
|
+
requiresAppContainer &&
|
|
2060
|
+
requiredBoundaries.some(
|
|
2061
|
+
(boundary) => !appContainerGuarantees.includes(boundary),
|
|
2062
|
+
)
|
|
2063
|
+
) {
|
|
2064
|
+
return unavailablePlan("windows_appcontainer_boundary_unsupported");
|
|
559
2065
|
}
|
|
560
2066
|
|
|
561
2067
|
// The hosted Windows restricted-token path cannot reliably preserve file
|
|
@@ -568,10 +2074,7 @@ export function applyWindowsSandbox(
|
|
|
568
2074
|
Array.isArray(spawnOpts?.stdio) &&
|
|
569
2075
|
spawnOpts.stdio.slice(0, 3).some((entry) => Number.isInteger(entry))
|
|
570
2076
|
) {
|
|
571
|
-
return
|
|
572
|
-
...base,
|
|
573
|
-
reason: "windows_detached_file_stdio_unsupported",
|
|
574
|
-
});
|
|
2077
|
+
return unavailablePlan("windows_detached_file_stdio_unsupported");
|
|
575
2078
|
}
|
|
576
2079
|
|
|
577
2080
|
const nodeIpcFd = Array.isArray(spawnOpts?.stdio)
|
|
@@ -589,18 +2092,7 @@ export function applyWindowsSandbox(
|
|
|
589
2092
|
entry !== undefined,
|
|
590
2093
|
)
|
|
591
2094
|
) {
|
|
592
|
-
return
|
|
593
|
-
...base,
|
|
594
|
-
reason: "windows_extra_descriptor_unsupported",
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const adapter = materializeWindowsAdapter(runtime);
|
|
599
|
-
if (!adapter.cacheExecutable) {
|
|
600
|
-
return createSandboxPlan({
|
|
601
|
-
...base,
|
|
602
|
-
reason: adapter.reason,
|
|
603
|
-
});
|
|
2095
|
+
return unavailablePlan("windows_extra_descriptor_unsupported");
|
|
604
2096
|
}
|
|
605
2097
|
|
|
606
2098
|
const invocation = normalizeWrappedInvocation(
|
|
@@ -609,10 +2101,86 @@ export function applyWindowsSandbox(
|
|
|
609
2101
|
spawnOpts,
|
|
610
2102
|
runtime.platform,
|
|
611
2103
|
);
|
|
2104
|
+
const entrySnapshot = windowsPluginNodeEntrySnapshot(
|
|
2105
|
+
invocation,
|
|
2106
|
+
sandboxOpts,
|
|
2107
|
+
spawnOpts,
|
|
2108
|
+
);
|
|
2109
|
+
if (entrySnapshot.reason) {
|
|
2110
|
+
return unavailablePlan(entrySnapshot.reason);
|
|
2111
|
+
}
|
|
2112
|
+
if (entrySnapshot.locks && nodeIpcFd >= 0) {
|
|
2113
|
+
return unavailablePlan("windows_plugin_entry_snapshot_ipc_unsupported");
|
|
2114
|
+
}
|
|
2115
|
+
const snapshotTestGate = entrySnapshot.locks
|
|
2116
|
+
? runtime.windowsSnapshotTestGate
|
|
2117
|
+
: null;
|
|
2118
|
+
if (
|
|
2119
|
+
snapshotTestGate !== null &&
|
|
2120
|
+
snapshotTestGate !== undefined &&
|
|
2121
|
+
(typeof snapshotTestGate !== "object" ||
|
|
2122
|
+
typeof snapshotTestGate.token !== "string" ||
|
|
2123
|
+
!/^[a-f0-9]{64}$/.test(snapshotTestGate.token) ||
|
|
2124
|
+
typeof snapshotTestGate.releasePath !== "string" ||
|
|
2125
|
+
!path.win32.isAbsolute(snapshotTestGate.releasePath) ||
|
|
2126
|
+
snapshotTestGate.releasePath.includes("\0"))
|
|
2127
|
+
) {
|
|
2128
|
+
return unavailablePlan("windows_plugin_entry_snapshot_test_gate_invalid");
|
|
2129
|
+
}
|
|
612
2130
|
const profile = sandboxOpts.profileName || "default";
|
|
2131
|
+
const executableName = path.win32
|
|
2132
|
+
.basename(String(invocation.command))
|
|
2133
|
+
.toLowerCase();
|
|
2134
|
+
if (
|
|
2135
|
+
profile === "default" &&
|
|
2136
|
+
requiredBoundaries.length === 0 &&
|
|
2137
|
+
!entrySnapshot.locks &&
|
|
2138
|
+
(executableName === "git" || executableName === "git.exe")
|
|
2139
|
+
) {
|
|
2140
|
+
return unavailablePlan("windows_git_nested_process_compatibility");
|
|
2141
|
+
}
|
|
2142
|
+
const loaderMode =
|
|
2143
|
+
profile !== "strict" && !requiresAppContainer && !entrySnapshot.locks
|
|
2144
|
+
? "managed-executable"
|
|
2145
|
+
: "powershell-byte-assembly";
|
|
2146
|
+
const adapterSource = loadWindowsAdapterSource(runtime, loaderMode);
|
|
2147
|
+
if (!adapterSource.content) {
|
|
2148
|
+
return unavailablePlan(adapterSource.reason);
|
|
2149
|
+
}
|
|
2150
|
+
const targetEnvironment = sanitizeWindowsSandboxEnvironment(
|
|
2151
|
+
invocation.options.env,
|
|
2152
|
+
Boolean(entrySnapshot.locks),
|
|
2153
|
+
);
|
|
2154
|
+
setWindowsEnvironmentValue(targetEnvironment, "CC_WINDOWS_SANDBOXED", "1");
|
|
2155
|
+
setWindowsEnvironmentValue(
|
|
2156
|
+
targetEnvironment,
|
|
2157
|
+
"CC_WINDOWS_SANDBOX_PROFILE",
|
|
2158
|
+
profile,
|
|
2159
|
+
);
|
|
2160
|
+
if (nodeIpcFd >= 0) {
|
|
2161
|
+
setWindowsEnvironmentValue(
|
|
2162
|
+
targetEnvironment,
|
|
2163
|
+
"NODE_CHANNEL_FD",
|
|
2164
|
+
String(nodeIpcFd),
|
|
2165
|
+
);
|
|
2166
|
+
setWindowsEnvironmentValue(
|
|
2167
|
+
targetEnvironment,
|
|
2168
|
+
"NODE_CHANNEL_SERIALIZATION_MODE",
|
|
2169
|
+
invocation.options.serialization === "advanced" ? "advanced" : "json",
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2172
|
+
const hostEnvironment = windowsSandboxHostEnvironment(runtime);
|
|
2173
|
+
const helperWorkingDirectory = runtime.joinPath(
|
|
2174
|
+
runtime.windowsDir(),
|
|
2175
|
+
"System32",
|
|
2176
|
+
);
|
|
613
2177
|
const limits = sandboxOpts.limits || {};
|
|
2178
|
+
const targetWorkingDirectory = runtime.resolvePath(
|
|
2179
|
+
invocation.options.cwd || process.cwd(),
|
|
2180
|
+
);
|
|
614
2181
|
const identityPath =
|
|
615
|
-
spawnOpts?.detached === true
|
|
2182
|
+
spawnOpts?.detached === true ||
|
|
2183
|
+
(requiresAppContainer && sandboxOpts.sync !== true)
|
|
616
2184
|
? runtime.joinPath(
|
|
617
2185
|
runtime.tmpdir(),
|
|
618
2186
|
`chainless-win-sandbox-identity-${runtime
|
|
@@ -626,186 +2194,1811 @@ export function applyWindowsSandbox(
|
|
|
626
2194
|
limits.as || (profile === "strict" ? 256 * 1024 * 1024 : 0),
|
|
627
2195
|
),
|
|
628
2196
|
activeProcessLimit: profile === "strict" ? 16 : 64,
|
|
2197
|
+
allowReparsePaths: loaderMode === "managed-executable",
|
|
2198
|
+
disableAdministratorSids:
|
|
2199
|
+
profile === "strict" ||
|
|
2200
|
+
requiresAppContainer ||
|
|
2201
|
+
Boolean(entrySnapshot.locks),
|
|
629
2202
|
command: invocation.command,
|
|
630
2203
|
args: invocation.args,
|
|
631
2204
|
nodeIpcFd,
|
|
632
2205
|
detached: invocation.options.detached === true,
|
|
633
2206
|
windowsHide: invocation.options.windowsHide === true,
|
|
2207
|
+
workingDirectory: targetWorkingDirectory,
|
|
2208
|
+
environment: targetEnvironment,
|
|
634
2209
|
};
|
|
2210
|
+
if (entrySnapshot.locks) {
|
|
2211
|
+
launchSpec.launchPathLocks = entrySnapshot.locks;
|
|
2212
|
+
if (snapshotTestGate) {
|
|
2213
|
+
launchSpec.snapshotTestGateToken = snapshotTestGate.token;
|
|
2214
|
+
launchSpec.snapshotTestGateReleasePath = snapshotTestGate.releasePath;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
if (requiresAppContainer) {
|
|
2218
|
+
appContainerPolicyDigest = windowsAppContainerPolicyDigest({
|
|
2219
|
+
profile,
|
|
2220
|
+
requiredBoundaries,
|
|
2221
|
+
guarantees: appContainerGuarantees,
|
|
2222
|
+
adapterSource,
|
|
2223
|
+
launchSpec,
|
|
2224
|
+
entrySnapshot,
|
|
2225
|
+
executionContract: sandboxOpts.executionContract,
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
635
2228
|
if (identityPath) launchSpec.identityPath = identityPath;
|
|
636
|
-
const
|
|
637
|
-
|
|
2229
|
+
const cleanupIdentity = () => {
|
|
2230
|
+
if (!identityPath) return true;
|
|
2231
|
+
return cleanupOrTrackWindowsTemporaryPath(runtime, identityPath);
|
|
2232
|
+
};
|
|
2233
|
+
const appContainerProfileName = requiresAppContainer
|
|
2234
|
+
? `ChainlessChain.CliSandbox.${runtime.randomBytes(12).toString("hex")}`
|
|
2235
|
+
: null;
|
|
2236
|
+
const adapter = createWindowsAdapterController(
|
|
2237
|
+
runtime,
|
|
2238
|
+
adapterSource,
|
|
2239
|
+
hostEnvironment,
|
|
638
2240
|
);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
2241
|
+
if (!adapter.ensureExecutable) {
|
|
2242
|
+
cleanupIdentity();
|
|
2243
|
+
return unavailablePlan(adapter.reason);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
let appContainer = null;
|
|
2247
|
+
let appContainerRuntimeProbe = null;
|
|
2248
|
+
let nodeSnapshotRuntimeProbe = null;
|
|
2249
|
+
if (entrySnapshot.locks && !requiresAppContainer) {
|
|
2250
|
+
let probeResult;
|
|
2251
|
+
try {
|
|
2252
|
+
probeResult = adapter.spawnSync(
|
|
2253
|
+
["--probe-node-snapshot", entrySnapshot.locks[0].path],
|
|
2254
|
+
{
|
|
2255
|
+
shell: false,
|
|
2256
|
+
windowsHide: true,
|
|
2257
|
+
encoding: "utf8",
|
|
2258
|
+
timeout: WINDOWS_SANDBOX_HELPER_OPERATION_TIMEOUT_MS,
|
|
2259
|
+
},
|
|
2260
|
+
);
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
probeResult = { error, status: null };
|
|
2263
|
+
}
|
|
2264
|
+
nodeSnapshotRuntimeProbe = windowsNodeSnapshotProbeResult(probeResult);
|
|
2265
|
+
if (!nodeSnapshotRuntimeProbe.runnable) {
|
|
2266
|
+
adapter.release();
|
|
2267
|
+
cleanupIdentity();
|
|
2268
|
+
return unavailablePlan("windows_plugin_entry_snapshot_probe_failed", {
|
|
2269
|
+
runtimeProbe: nodeSnapshotRuntimeProbe,
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
if (requiresAppContainer) {
|
|
2274
|
+
let readinessResult;
|
|
2275
|
+
try {
|
|
2276
|
+
const readinessArgs = ["--prepare-appcontainer", appContainerProfileName];
|
|
2277
|
+
if (entrySnapshot.locks) {
|
|
2278
|
+
readinessArgs.push(entrySnapshot.locks[0].path);
|
|
2279
|
+
}
|
|
2280
|
+
readinessResult = adapter.spawnSync(readinessArgs, {
|
|
2281
|
+
shell: false,
|
|
2282
|
+
windowsHide: true,
|
|
2283
|
+
encoding: "utf8",
|
|
2284
|
+
timeout: WINDOWS_SANDBOX_HELPER_OPERATION_TIMEOUT_MS,
|
|
2285
|
+
});
|
|
2286
|
+
} catch (error) {
|
|
2287
|
+
readinessResult = { error, status: null };
|
|
2288
|
+
}
|
|
2289
|
+
const readiness = appContainerReadinessResult(
|
|
2290
|
+
readinessResult,
|
|
2291
|
+
appContainerProfileName,
|
|
2292
|
+
entrySnapshot.locks ? "node" : null,
|
|
2293
|
+
);
|
|
2294
|
+
appContainerRuntimeProbe = readiness.runtimeProbe;
|
|
2295
|
+
if (!readiness.readiness) {
|
|
2296
|
+
const cleanupFailure = {};
|
|
2297
|
+
const deleted = deleteWindowsAppContainerProfile(
|
|
2298
|
+
adapter,
|
|
2299
|
+
appContainerProfileName,
|
|
2300
|
+
null,
|
|
2301
|
+
cleanupFailure,
|
|
2302
|
+
);
|
|
2303
|
+
if (!deleted) {
|
|
2304
|
+
trackWindowsAppContainerCleanup(
|
|
2305
|
+
runtime,
|
|
2306
|
+
adapterSource,
|
|
2307
|
+
hostEnvironment,
|
|
2308
|
+
appContainerProfileName,
|
|
2309
|
+
null,
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
adapter.release();
|
|
2313
|
+
if (!deleted) {
|
|
2314
|
+
appContainerRuntimeProbe = {
|
|
2315
|
+
...appContainerRuntimeProbe,
|
|
2316
|
+
reason: appContainerCleanupFailureReason(
|
|
2317
|
+
appContainerRuntimeProbe.reason,
|
|
2318
|
+
cleanupFailure.reason,
|
|
2319
|
+
),
|
|
2320
|
+
};
|
|
2321
|
+
cleanupIdentity();
|
|
2322
|
+
return unavailablePlan(
|
|
2323
|
+
"windows_appcontainer_readiness_cleanup_unverified",
|
|
2324
|
+
{
|
|
2325
|
+
runtimeProbe: appContainerRuntimeProbe,
|
|
2326
|
+
},
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
2329
|
+
cleanupIdentity();
|
|
2330
|
+
return unavailablePlan("windows_appcontainer_readiness_failed", {
|
|
2331
|
+
runtimeProbe: appContainerRuntimeProbe,
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
appContainer = {
|
|
2335
|
+
appContainerProfileName,
|
|
2336
|
+
appContainerSid: readiness.readiness.appContainerSid,
|
|
2337
|
+
};
|
|
2338
|
+
launchSpec.appContainerProfileName = appContainer.appContainerProfileName;
|
|
2339
|
+
launchSpec.appContainerSid = appContainer.appContainerSid;
|
|
2340
|
+
setWindowsEnvironmentValue(
|
|
2341
|
+
targetEnvironment,
|
|
2342
|
+
"CC_WINDOWS_APPCONTAINER",
|
|
2343
|
+
"1",
|
|
2344
|
+
);
|
|
2345
|
+
setWindowsEnvironmentValue(
|
|
2346
|
+
targetEnvironment,
|
|
2347
|
+
"CC_WINDOWS_APPCONTAINER_PROFILE",
|
|
2348
|
+
appContainer.appContainerProfileName,
|
|
2349
|
+
);
|
|
2350
|
+
setWindowsEnvironmentValue(
|
|
2351
|
+
targetEnvironment,
|
|
2352
|
+
"CC_WINDOWS_APPCONTAINER_SID",
|
|
2353
|
+
appContainer.appContainerSid,
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
let helperInvocation;
|
|
2358
|
+
try {
|
|
2359
|
+
const payload = Buffer.from(JSON.stringify(launchSpec), "utf8").toString(
|
|
2360
|
+
"base64",
|
|
2361
|
+
);
|
|
2362
|
+
// This is the final synchronous operation before the broker consumes the
|
|
2363
|
+
// plan and spawns the helper. Internal readiness/deletion invocations also
|
|
2364
|
+
// pass through the same digest + file-identity attestation.
|
|
2365
|
+
helperInvocation = adapter.createInvocation([payload]);
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
const cleanupFailure = {};
|
|
2368
|
+
const deleted = appContainer
|
|
2369
|
+
? deleteWindowsAppContainerProfile(
|
|
2370
|
+
adapter,
|
|
2371
|
+
appContainer.appContainerProfileName,
|
|
2372
|
+
appContainer.appContainerSid,
|
|
2373
|
+
cleanupFailure,
|
|
2374
|
+
)
|
|
2375
|
+
: true;
|
|
2376
|
+
if (!deleted) {
|
|
2377
|
+
trackWindowsAppContainerCleanup(
|
|
2378
|
+
runtime,
|
|
2379
|
+
adapterSource,
|
|
2380
|
+
hostEnvironment,
|
|
2381
|
+
appContainer.appContainerProfileName,
|
|
2382
|
+
appContainer.appContainerSid,
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
adapter.release();
|
|
2386
|
+
cleanupIdentity();
|
|
2387
|
+
if (!deleted) {
|
|
2388
|
+
return unavailablePlan(
|
|
2389
|
+
"windows_appcontainer_readiness_cleanup_unverified",
|
|
2390
|
+
{
|
|
2391
|
+
runtimeProbe: appContainerRuntimeProbe
|
|
2392
|
+
? {
|
|
2393
|
+
...appContainerRuntimeProbe,
|
|
2394
|
+
runnable: false,
|
|
2395
|
+
reason: appContainerCleanupFailureReason(
|
|
2396
|
+
appContainerRuntimeProbe.reason || "ready",
|
|
2397
|
+
cleanupFailure.reason,
|
|
2398
|
+
),
|
|
2399
|
+
}
|
|
2400
|
+
: null,
|
|
2401
|
+
},
|
|
2402
|
+
);
|
|
2403
|
+
}
|
|
2404
|
+
return unavailablePlan(
|
|
2405
|
+
error?.adapterReason || "windows_native_adapter_attestation_failed",
|
|
2406
|
+
{ runtimeProbe: appContainerRuntimeProbe },
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
const options = {
|
|
2411
|
+
...invocation.options,
|
|
2412
|
+
// The managed helper is the lifetime supervisor and must stay attached to
|
|
2413
|
+
// the broker so its bootstrap can reliably publish the target identity.
|
|
2414
|
+
// The payload still carries `detached: true`, so the helper creates the
|
|
2415
|
+
// actual target with DETACHED_PROCESS + CREATE_NEW_PROCESS_GROUP. Detaching
|
|
2416
|
+
// the outer PowerShell host as well caused it to stall before writing the
|
|
2417
|
+
// identity file on Windows.
|
|
2418
|
+
detached: false,
|
|
2419
|
+
shell: false,
|
|
2420
|
+
cwd: helperWorkingDirectory,
|
|
2421
|
+
env: hostEnvironment,
|
|
2422
|
+
};
|
|
2423
|
+
let appContainerCleanupVerified = false;
|
|
2424
|
+
let appContainerCleanupRecord = null;
|
|
2425
|
+
const cleanupAppContainer = () => {
|
|
2426
|
+
if (!appContainer || appContainerCleanupVerified) return true;
|
|
2427
|
+
if (appContainerCleanupRecord) {
|
|
2428
|
+
if (!retryWindowsAppContainerCleanup(appContainerCleanupRecord)) {
|
|
2429
|
+
return false;
|
|
2430
|
+
}
|
|
2431
|
+
appContainerCleanupRecord = null;
|
|
2432
|
+
appContainerCleanupVerified = true;
|
|
2433
|
+
return true;
|
|
2434
|
+
}
|
|
2435
|
+
// Native.Run deletes the profile on every ordinary target exit. This
|
|
2436
|
+
// second, intentionally idempotent path covers a failed spawn or a helper
|
|
2437
|
+
// that was forcibly terminated before its finally block ran. The helper
|
|
2438
|
+
// re-derives and compares the expected SID, then proves absence by
|
|
2439
|
+
// creating and deleting a fresh verification profile with the same name.
|
|
2440
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
2441
|
+
const deleted = deleteWindowsAppContainerProfile(
|
|
2442
|
+
adapter,
|
|
2443
|
+
appContainer.appContainerProfileName,
|
|
2444
|
+
appContainer.appContainerSid,
|
|
2445
|
+
);
|
|
2446
|
+
if (deleted) {
|
|
2447
|
+
appContainerCleanupVerified = true;
|
|
2448
|
+
return true;
|
|
2449
|
+
}
|
|
2450
|
+
if (attempt < 3) runtime.sleepSync(25);
|
|
2451
|
+
}
|
|
2452
|
+
appContainerCleanupRecord = trackWindowsAppContainerCleanup(
|
|
2453
|
+
runtime,
|
|
2454
|
+
adapterSource,
|
|
2455
|
+
hostEnvironment,
|
|
2456
|
+
appContainer.appContainerProfileName,
|
|
2457
|
+
appContainer.appContainerSid,
|
|
2458
|
+
);
|
|
2459
|
+
return false;
|
|
2460
|
+
};
|
|
2461
|
+
const cleanup = () => {
|
|
2462
|
+
const failures = [];
|
|
2463
|
+
const attempt = (label, action) => {
|
|
2464
|
+
try {
|
|
2465
|
+
if (action() === false) failures.push(label);
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
failures.push(`${label}: ${error?.message || error}`);
|
|
2468
|
+
}
|
|
2469
|
+
};
|
|
2470
|
+
try {
|
|
2471
|
+
attempt("target identity", cleanupIdentity);
|
|
2472
|
+
attempt("invocation payload", helperInvocation.cleanup);
|
|
2473
|
+
attempt("AppContainer profile", cleanupAppContainer);
|
|
2474
|
+
} finally {
|
|
2475
|
+
adapter.release();
|
|
2476
|
+
}
|
|
2477
|
+
if (failures.length > 0) {
|
|
2478
|
+
const message =
|
|
2479
|
+
"Windows sandbox cleanup could not be verified for " +
|
|
2480
|
+
failures.join(", ");
|
|
2481
|
+
if (sandboxOpts.sync === true) throw new Error(message);
|
|
2482
|
+
runtime.warn(message);
|
|
2483
|
+
return false;
|
|
2484
|
+
}
|
|
2485
|
+
return true;
|
|
2486
|
+
};
|
|
2487
|
+
const identityContract = identityPath
|
|
2488
|
+
? {
|
|
2489
|
+
postSpawn: { required: true, mode: "sync" },
|
|
2490
|
+
postSpawnWindows: (proc) => {
|
|
2491
|
+
try {
|
|
2492
|
+
const identity = waitForWindowsTargetIdentity(
|
|
2493
|
+
proc,
|
|
2494
|
+
identityPath,
|
|
2495
|
+
runtime,
|
|
2496
|
+
appContainer,
|
|
2497
|
+
);
|
|
2498
|
+
if (!cleanupIdentity()) {
|
|
2499
|
+
throw new Error(
|
|
2500
|
+
"Windows sandbox target identity cleanup could not be verified",
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
return identity;
|
|
2504
|
+
} catch (error) {
|
|
2505
|
+
try {
|
|
2506
|
+
proc.kill?.();
|
|
2507
|
+
} catch {
|
|
2508
|
+
// Closing the helper is best-effort; the thrown failure remains
|
|
2509
|
+
// authoritative and strict mode will fail closed.
|
|
2510
|
+
}
|
|
2511
|
+
if (!cleanupIdentity()) {
|
|
2512
|
+
throw new Error(
|
|
2513
|
+
"Windows sandbox target identity cleanup could not be verified",
|
|
2514
|
+
{ cause: error },
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
throw error;
|
|
2518
|
+
}
|
|
2519
|
+
},
|
|
2520
|
+
}
|
|
2521
|
+
: {};
|
|
2522
|
+
const entrySnapshotRuntimeProbe = entrySnapshot.locks
|
|
2523
|
+
? {
|
|
2524
|
+
...(appContainerRuntimeProbe || nodeSnapshotRuntimeProbe),
|
|
2525
|
+
contentSnapshot: true,
|
|
2526
|
+
contentSnapshotScope: "plugin-entry-source",
|
|
2527
|
+
contentSnapshotMechanism:
|
|
2528
|
+
"verified-handle-inherited-pipe-module-compile-v1",
|
|
2529
|
+
handleAtomic: false,
|
|
2530
|
+
}
|
|
2531
|
+
: appContainerRuntimeProbe;
|
|
2532
|
+
return createSandboxPlan({
|
|
2533
|
+
...base,
|
|
2534
|
+
applied: true,
|
|
2535
|
+
enforcement: backend,
|
|
2536
|
+
policyAttested: requiresAppContainer ? true : null,
|
|
2537
|
+
policyDigest: appContainerPolicyDigest,
|
|
2538
|
+
runtimeProbe: entrySnapshotRuntimeProbe,
|
|
2539
|
+
guarantees: requiresAppContainer
|
|
2540
|
+
? appContainerGuarantees
|
|
2541
|
+
: [
|
|
2542
|
+
SANDBOX_BOUNDARIES.PROCESS_TREE,
|
|
2543
|
+
SANDBOX_BOUNDARIES.RESOURCE_LIMITS,
|
|
2544
|
+
SANDBOX_BOUNDARIES.PRIVILEGE_REDUCTION,
|
|
2545
|
+
],
|
|
2546
|
+
command: helperInvocation.command,
|
|
2547
|
+
args: helperInvocation.args,
|
|
2548
|
+
options,
|
|
2549
|
+
...identityContract,
|
|
2550
|
+
cleanup,
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
/**
|
|
2555
|
+
* Retained for injected adapters that elect to perform synchronous post-spawn
|
|
2556
|
+
* enforcement. The built-in wrapper completes enforcement before target resume
|
|
2557
|
+
* and therefore does not require this path.
|
|
2558
|
+
*
|
|
2559
|
+
* @param {ChildProcess} proc - The spawned child process
|
|
2560
|
+
* @param {Object} sandboxResult - Result from applyWindowsSandbox
|
|
2561
|
+
*/
|
|
2562
|
+
export function postSpawnWindowsSandbox(
|
|
2563
|
+
proc,
|
|
2564
|
+
sandboxResult,
|
|
2565
|
+
runtimeOverrides = {},
|
|
2566
|
+
) {
|
|
2567
|
+
const runtime = resolveRuntime(runtimeOverrides);
|
|
2568
|
+
if (runtime.platform !== "win32" || !sandboxResult?.postSpawn?.required) {
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
if (typeof sandboxResult.postSpawnWindows === "function") {
|
|
2572
|
+
return sandboxResult.postSpawnWindows(proc);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// ---------------------------------------------------------------------------
|
|
2577
|
+
// Linux resource-limit enforcement
|
|
2578
|
+
// ---------------------------------------------------------------------------
|
|
2579
|
+
|
|
2580
|
+
function linuxBubblewrapProbe(
|
|
2581
|
+
attempted,
|
|
2582
|
+
runnable,
|
|
2583
|
+
reason,
|
|
2584
|
+
targetRuntime = "node",
|
|
2585
|
+
) {
|
|
2586
|
+
const native = targetRuntime === "native-static-elf";
|
|
2587
|
+
return {
|
|
2588
|
+
kind: native
|
|
2589
|
+
? "linux-bwrap-plugin-native-static-elf-policy-v1"
|
|
2590
|
+
: "linux-bwrap-plugin-node-policy-v1",
|
|
2591
|
+
attempted,
|
|
2592
|
+
runnable,
|
|
2593
|
+
reason,
|
|
2594
|
+
probeRuntime: "node",
|
|
2595
|
+
targetRuntime,
|
|
2596
|
+
contentSnapshot: false,
|
|
2597
|
+
handleAtomic: false,
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
function linuxRealpath(runtime, value) {
|
|
2602
|
+
const realpathSync = runtime.fs?.realpathSync;
|
|
2603
|
+
if (typeof realpathSync !== "function") {
|
|
2604
|
+
throw new Error("realpath_unavailable");
|
|
2605
|
+
}
|
|
2606
|
+
return typeof realpathSync.native === "function"
|
|
2607
|
+
? realpathSync.native(value)
|
|
2608
|
+
: realpathSync(value);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
function linuxPathWithin(root, target) {
|
|
2612
|
+
const relative = path.posix.relative(root, target);
|
|
2613
|
+
return (
|
|
2614
|
+
relative !== "" &&
|
|
2615
|
+
relative !== ".." &&
|
|
2616
|
+
!relative.startsWith("../") &&
|
|
2617
|
+
!path.posix.isAbsolute(relative)
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
function linuxIdentityMatches(runtime, identity, { executable = false } = {}) {
|
|
2622
|
+
if (
|
|
2623
|
+
!identity ||
|
|
2624
|
+
typeof identity.realPath !== "string" ||
|
|
2625
|
+
!path.posix.isAbsolute(identity.realPath) ||
|
|
2626
|
+
typeof identity.sha256 !== "string" ||
|
|
2627
|
+
!/^[a-f0-9]{64}$/.test(identity.sha256) ||
|
|
2628
|
+
!identity.fileId ||
|
|
2629
|
+
!Number.isSafeInteger(identity.bytes) ||
|
|
2630
|
+
identity.bytes < 0 ||
|
|
2631
|
+
identity.bytes > LINUX_ATTESTED_FILE_MAX_BYTES ||
|
|
2632
|
+
identity.mtimeMs === null
|
|
2633
|
+
) {
|
|
2634
|
+
return false;
|
|
2635
|
+
}
|
|
2636
|
+
let fd;
|
|
2637
|
+
try {
|
|
2638
|
+
if (linuxRealpath(runtime, identity.realPath) !== identity.realPath) {
|
|
2639
|
+
return false;
|
|
2640
|
+
}
|
|
2641
|
+
const lst = runtime.fs.lstatSync(identity.realPath);
|
|
2642
|
+
if (lst.isSymbolicLink() || !lst.isFile()) return false;
|
|
2643
|
+
if (executable && (Number(lst.mode) & 0o111) === 0) return false;
|
|
2644
|
+
const constants = runtime.fs.constants || fs.constants;
|
|
2645
|
+
const flags =
|
|
2646
|
+
Number(constants.O_RDONLY) |
|
|
2647
|
+
Number(constants.O_NOFOLLOW || 0) |
|
|
2648
|
+
Number(constants.O_NONBLOCK || 0);
|
|
2649
|
+
fd = runtime.fs.openSync(identity.realPath, flags);
|
|
2650
|
+
const before = runtime.fs.fstatSync(fd);
|
|
2651
|
+
if (
|
|
2652
|
+
!before.isFile() ||
|
|
2653
|
+
String(before.dev) !== String(identity.fileId.dev) ||
|
|
2654
|
+
String(before.ino) !== String(identity.fileId.ino) ||
|
|
2655
|
+
Number(before.size) !== Number(identity.bytes) ||
|
|
2656
|
+
Number(before.mtimeMs) !== Number(identity.mtimeMs)
|
|
2657
|
+
) {
|
|
2658
|
+
return false;
|
|
2659
|
+
}
|
|
2660
|
+
const digest = crypto.createHash("sha256");
|
|
2661
|
+
const chunk = Buffer.allocUnsafe(
|
|
2662
|
+
Math.max(1, Math.min(LINUX_ATTESTATION_HASH_CHUNK_BYTES, identity.bytes)),
|
|
2663
|
+
);
|
|
2664
|
+
let offset = 0;
|
|
2665
|
+
while (offset < identity.bytes) {
|
|
2666
|
+
const read = runtime.fs.readSync(
|
|
2667
|
+
fd,
|
|
2668
|
+
chunk,
|
|
2669
|
+
0,
|
|
2670
|
+
Math.min(chunk.length, identity.bytes - offset),
|
|
2671
|
+
offset,
|
|
2672
|
+
);
|
|
2673
|
+
if (read <= 0) return false;
|
|
2674
|
+
digest.update(chunk.subarray(0, read));
|
|
2675
|
+
offset += read;
|
|
2676
|
+
}
|
|
2677
|
+
const after = runtime.fs.fstatSync(fd);
|
|
2678
|
+
const stable =
|
|
2679
|
+
String(before.dev) === String(after.dev) &&
|
|
2680
|
+
String(before.ino) === String(after.ino) &&
|
|
2681
|
+
Number(before.size) === Number(after.size) &&
|
|
2682
|
+
Number(before.mtimeMs) === Number(after.mtimeMs);
|
|
2683
|
+
if (!stable) return false;
|
|
2684
|
+
return digest.digest("hex") === identity.sha256;
|
|
2685
|
+
} catch {
|
|
2686
|
+
return false;
|
|
2687
|
+
} finally {
|
|
2688
|
+
if (fd !== undefined) {
|
|
2689
|
+
try {
|
|
2690
|
+
runtime.fs.closeSync(fd);
|
|
2691
|
+
} catch {
|
|
2692
|
+
// best effort
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
function linuxDirectoryIdentityMatches(runtime, identity) {
|
|
2699
|
+
if (
|
|
2700
|
+
!identity ||
|
|
2701
|
+
typeof identity.realPath !== "string" ||
|
|
2702
|
+
!path.posix.isAbsolute(identity.realPath) ||
|
|
2703
|
+
!identity.fileId
|
|
2704
|
+
) {
|
|
2705
|
+
return false;
|
|
2706
|
+
}
|
|
2707
|
+
try {
|
|
2708
|
+
const stat = runtime.fs.lstatSync(identity.realPath);
|
|
2709
|
+
return (
|
|
2710
|
+
!stat.isSymbolicLink() &&
|
|
2711
|
+
stat.isDirectory() &&
|
|
2712
|
+
String(stat.dev) === String(identity.fileId.dev) &&
|
|
2713
|
+
String(stat.ino) === String(identity.fileId.ino)
|
|
2714
|
+
);
|
|
2715
|
+
} catch {
|
|
2716
|
+
return false;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
function validateLinuxPluginTree(runtime, pluginRoot) {
|
|
2721
|
+
try {
|
|
2722
|
+
const rootStat = runtime.fs.lstatSync(pluginRoot);
|
|
2723
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return false;
|
|
2724
|
+
const rootDevice = String(rootStat.dev);
|
|
2725
|
+
const pending = [pluginRoot];
|
|
2726
|
+
let entries = 0;
|
|
2727
|
+
while (pending.length > 0) {
|
|
2728
|
+
const current = pending.pop();
|
|
2729
|
+
const children = runtime.fs.readdirSync(current, {
|
|
2730
|
+
withFileTypes: true,
|
|
2731
|
+
});
|
|
2732
|
+
for (const child of children) {
|
|
2733
|
+
entries += 1;
|
|
2734
|
+
if (entries > LINUX_BWRAP_MAX_PLUGIN_ENTRIES) return false;
|
|
2735
|
+
const childPath = path.posix.join(current, child.name);
|
|
2736
|
+
const stat = runtime.fs.lstatSync(childPath);
|
|
2737
|
+
if (
|
|
2738
|
+
stat.isSymbolicLink() ||
|
|
2739
|
+
String(stat.dev) !== rootDevice ||
|
|
2740
|
+
stat.isSocket?.() ||
|
|
2741
|
+
stat.isFIFO?.() ||
|
|
2742
|
+
stat.isBlockDevice?.() ||
|
|
2743
|
+
stat.isCharacterDevice?.()
|
|
2744
|
+
) {
|
|
2745
|
+
return false;
|
|
2746
|
+
}
|
|
2747
|
+
if (stat.isDirectory()) {
|
|
2748
|
+
pending.push(childPath);
|
|
2749
|
+
} else if (!stat.isFile() || Number(stat.nlink) !== 1) {
|
|
2750
|
+
return false;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
return true;
|
|
2755
|
+
} catch {
|
|
2756
|
+
return false;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
function decodeLinuxMountInfoPath(value) {
|
|
2761
|
+
return value.replace(/\\(040|011|012|134)/g, (_match, escaped) => {
|
|
2762
|
+
const decoded = {
|
|
2763
|
+
"040": " ",
|
|
2764
|
+
"011": "\t",
|
|
2765
|
+
"012": "\n",
|
|
2766
|
+
134: "\\",
|
|
2767
|
+
};
|
|
2768
|
+
return decoded[escaped];
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
function linuxPluginMountTreeIsNarrow(runtime, pluginRoot) {
|
|
2773
|
+
try {
|
|
2774
|
+
const contents = String(
|
|
2775
|
+
runtime.fs.readFileSync("/proc/self/mountinfo", "utf8"),
|
|
2776
|
+
);
|
|
2777
|
+
let parsed = 0;
|
|
2778
|
+
let coveringMount = null;
|
|
2779
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
2780
|
+
if (!line) continue;
|
|
2781
|
+
const fields = line.split(" ");
|
|
2782
|
+
const separator = fields.indexOf("-");
|
|
2783
|
+
if (
|
|
2784
|
+
fields.length < 10 ||
|
|
2785
|
+
separator < 6 ||
|
|
2786
|
+
separator + 2 >= fields.length
|
|
2787
|
+
) {
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
parsed += 1;
|
|
2791
|
+
const mountPoint = decodeLinuxMountInfoPath(fields[4]);
|
|
2792
|
+
const filesystemType = fields[separator + 1];
|
|
2793
|
+
if (mountPoint === pluginRoot) return false;
|
|
2794
|
+
if (linuxPathWithin(pluginRoot, mountPoint)) {
|
|
2795
|
+
return false;
|
|
2796
|
+
}
|
|
2797
|
+
if (linuxPathWithin(mountPoint, pluginRoot)) {
|
|
2798
|
+
if (
|
|
2799
|
+
!coveringMount ||
|
|
2800
|
+
mountPoint.length > coveringMount.mountPoint.length
|
|
2801
|
+
) {
|
|
2802
|
+
coveringMount = { mountPoint, filesystemType };
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
return (
|
|
2807
|
+
parsed > 0 &&
|
|
2808
|
+
coveringMount !== null &&
|
|
2809
|
+
LINUX_LOCAL_FILESYSTEM_TYPES.has(coveringMount.filesystemType)
|
|
2810
|
+
);
|
|
2811
|
+
} catch {
|
|
2812
|
+
return false;
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
function linuxStdioIsNarrow(stdio) {
|
|
2817
|
+
if (stdio === undefined || stdio === null) return true;
|
|
2818
|
+
if (stdio === "pipe" || stdio === "ignore") return true;
|
|
2819
|
+
if (!Array.isArray(stdio) || stdio.length > 3) return false;
|
|
2820
|
+
return stdio.every(
|
|
2821
|
+
(entry) =>
|
|
2822
|
+
entry === undefined ||
|
|
2823
|
+
entry === null ||
|
|
2824
|
+
entry === "pipe" ||
|
|
2825
|
+
entry === "ignore",
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
function assignLinuxMountChildFds(mounts) {
|
|
2830
|
+
return mounts.map((mount, index) => ({
|
|
2831
|
+
...mount,
|
|
2832
|
+
childFd: LINUX_BWRAP_FIRST_MOUNT_FD + index,
|
|
2833
|
+
}));
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
function linuxStdioWithPinnedMounts(
|
|
2837
|
+
stdio,
|
|
2838
|
+
descriptors,
|
|
2839
|
+
{ probe = false } = {},
|
|
2840
|
+
) {
|
|
2841
|
+
let standard;
|
|
2842
|
+
if (probe) {
|
|
2843
|
+
standard = ["ignore", "pipe", "pipe"];
|
|
2844
|
+
} else if (Array.isArray(stdio)) {
|
|
2845
|
+
standard = [...stdio];
|
|
2846
|
+
} else if (stdio === "ignore") {
|
|
2847
|
+
standard = ["ignore", "ignore", "ignore"];
|
|
2848
|
+
} else {
|
|
2849
|
+
standard = ["pipe", "pipe", "pipe"];
|
|
2850
|
+
}
|
|
2851
|
+
while (standard.length < 3) standard.push(undefined);
|
|
2852
|
+
return [
|
|
2853
|
+
...standard.slice(0, 3),
|
|
2854
|
+
...descriptors.map((descriptor) => descriptor.fd),
|
|
2855
|
+
];
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
function validateLinuxPluginContract(
|
|
2859
|
+
command,
|
|
2860
|
+
args,
|
|
2861
|
+
spawnOpts,
|
|
2862
|
+
contract,
|
|
2863
|
+
runtime,
|
|
2864
|
+
sync,
|
|
2865
|
+
) {
|
|
2866
|
+
const nodeContract = contract?.kind === "strict-plugin-node-bin";
|
|
2867
|
+
const nativeContract =
|
|
2868
|
+
contract?.kind === "strict-plugin-native-static-elf-bin";
|
|
2869
|
+
if (
|
|
2870
|
+
!contract ||
|
|
2871
|
+
contract.contractVersion !== 1 ||
|
|
2872
|
+
(!nodeContract && !nativeContract)
|
|
2873
|
+
) {
|
|
2874
|
+
return { ok: false, reason: "execution_contract_missing" };
|
|
2875
|
+
}
|
|
2876
|
+
if (
|
|
2877
|
+
sync !== true ||
|
|
2878
|
+
spawnOpts?.shell !== false ||
|
|
2879
|
+
spawnOpts?.detached === true ||
|
|
2880
|
+
!linuxStdioIsNarrow(spawnOpts?.stdio) ||
|
|
2881
|
+
spawnOpts?.serialization !== undefined ||
|
|
2882
|
+
spawnOpts?.argv0 !== undefined ||
|
|
2883
|
+
spawnOpts?.uid !== undefined ||
|
|
2884
|
+
spawnOpts?.gid !== undefined
|
|
2885
|
+
) {
|
|
2886
|
+
return { ok: false, reason: "unsupported_launch_options" };
|
|
2887
|
+
}
|
|
2888
|
+
const invalidArgs =
|
|
2889
|
+
!Array.isArray(args) ||
|
|
2890
|
+
args.some((arg) => typeof arg !== "string" || arg.includes("\0"));
|
|
2891
|
+
const invalidNodeLaunch =
|
|
2892
|
+
nodeContract &&
|
|
2893
|
+
(!Array.isArray(args) ||
|
|
2894
|
+
command !== contract.runtimePath ||
|
|
2895
|
+
command !== contract.runtimeIdentity?.realPath ||
|
|
2896
|
+
args.length < 1 ||
|
|
2897
|
+
args[0] !== contract.entryIdentity?.realPath);
|
|
2898
|
+
const invalidNativeLaunch =
|
|
2899
|
+
nativeContract && command !== contract.entryIdentity?.realPath;
|
|
2900
|
+
if (
|
|
2901
|
+
typeof command !== "string" ||
|
|
2902
|
+
!path.posix.isAbsolute(command) ||
|
|
2903
|
+
invalidArgs ||
|
|
2904
|
+
invalidNodeLaunch ||
|
|
2905
|
+
invalidNativeLaunch
|
|
2906
|
+
) {
|
|
2907
|
+
return { ok: false, reason: "launch_identity_mismatch" };
|
|
2908
|
+
}
|
|
2909
|
+
if (
|
|
2910
|
+
nodeContract &&
|
|
2911
|
+
![".js", ".cjs", ".mjs"].includes(
|
|
2912
|
+
path.posix.extname(contract.entryIdentity.realPath).toLowerCase(),
|
|
2913
|
+
)
|
|
2914
|
+
) {
|
|
2915
|
+
return { ok: false, reason: "unsupported_plugin_entry" };
|
|
2916
|
+
}
|
|
2917
|
+
for (const value of [
|
|
2918
|
+
contract.pluginRoot,
|
|
2919
|
+
contract.workingDirectory,
|
|
2920
|
+
contract.rootIdentity?.realPath,
|
|
2921
|
+
contract.entryIdentity.realPath,
|
|
2922
|
+
contract.runtimeIdentity.realPath,
|
|
2923
|
+
]) {
|
|
2924
|
+
if (
|
|
2925
|
+
typeof value !== "string" ||
|
|
2926
|
+
!path.posix.isAbsolute(value) ||
|
|
2927
|
+
value.includes("\0")
|
|
2928
|
+
) {
|
|
2929
|
+
return { ok: false, reason: "noncanonical_contract_path" };
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
try {
|
|
2933
|
+
if (
|
|
2934
|
+
linuxRealpath(runtime, contract.pluginRoot) !== contract.pluginRoot ||
|
|
2935
|
+
linuxRealpath(runtime, contract.workingDirectory) !==
|
|
2936
|
+
contract.workingDirectory ||
|
|
2937
|
+
contract.workingDirectory !== contract.pluginRoot ||
|
|
2938
|
+
linuxRealpath(runtime, spawnOpts.cwd) !== contract.pluginRoot ||
|
|
2939
|
+
contract.rootIdentity?.realPath !== contract.pluginRoot ||
|
|
2940
|
+
!linuxPathWithin(contract.pluginRoot, contract.entryIdentity.realPath)
|
|
2941
|
+
) {
|
|
2942
|
+
return { ok: false, reason: "noncanonical_contract_path" };
|
|
2943
|
+
}
|
|
2944
|
+
} catch {
|
|
2945
|
+
return { ok: false, reason: "contract_path_unavailable" };
|
|
2946
|
+
}
|
|
2947
|
+
const forbiddenRoots = new Set([
|
|
2948
|
+
"/",
|
|
2949
|
+
"/bin",
|
|
2950
|
+
"/dev",
|
|
2951
|
+
"/etc",
|
|
2952
|
+
"/home",
|
|
2953
|
+
"/lib",
|
|
2954
|
+
"/lib64",
|
|
2955
|
+
"/proc",
|
|
2956
|
+
"/root",
|
|
2957
|
+
"/run",
|
|
2958
|
+
"/sys",
|
|
2959
|
+
"/tmp",
|
|
2960
|
+
"/usr",
|
|
2961
|
+
"/var",
|
|
2962
|
+
path.posix.resolve(runtime.homedir()),
|
|
2963
|
+
]);
|
|
2964
|
+
if (
|
|
2965
|
+
forbiddenRoots.has(contract.pluginRoot) ||
|
|
2966
|
+
["/dev", "/proc", "/run", "/sys"].some(
|
|
2967
|
+
(root) =>
|
|
2968
|
+
contract.pluginRoot === root ||
|
|
2969
|
+
contract.pluginRoot.startsWith(`${root}/`),
|
|
2970
|
+
)
|
|
2971
|
+
) {
|
|
2972
|
+
return { ok: false, reason: "broad_plugin_root_disallowed" };
|
|
2973
|
+
}
|
|
2974
|
+
if (
|
|
2975
|
+
!linuxDirectoryIdentityMatches(runtime, contract.rootIdentity) ||
|
|
2976
|
+
!linuxIdentityMatches(runtime, contract.entryIdentity, {
|
|
2977
|
+
executable: nativeContract,
|
|
2978
|
+
}) ||
|
|
2979
|
+
!linuxIdentityMatches(runtime, contract.runtimeIdentity, {
|
|
2980
|
+
executable: true,
|
|
2981
|
+
})
|
|
2982
|
+
) {
|
|
2983
|
+
return { ok: false, reason: "execution_identity_changed" };
|
|
2984
|
+
}
|
|
2985
|
+
let entryFormat = null;
|
|
2986
|
+
if (nativeContract) {
|
|
2987
|
+
try {
|
|
2988
|
+
entryFormat = inspectLinuxStaticNativePath(
|
|
2989
|
+
runtime,
|
|
2990
|
+
contract.entryIdentity,
|
|
2991
|
+
);
|
|
2992
|
+
} catch (error) {
|
|
2993
|
+
return {
|
|
2994
|
+
ok: false,
|
|
2995
|
+
reason: error.message || "native_entry_unattested",
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
if (
|
|
3000
|
+
!validateLinuxPluginTree(runtime, contract.pluginRoot) ||
|
|
3001
|
+
!linuxPluginMountTreeIsNarrow(runtime, contract.pluginRoot)
|
|
3002
|
+
) {
|
|
3003
|
+
return { ok: false, reason: "plugin_tree_unattested" };
|
|
3004
|
+
}
|
|
3005
|
+
return {
|
|
3006
|
+
ok: true,
|
|
3007
|
+
contract,
|
|
3008
|
+
entryRuntime: nativeContract ? "native-static-elf" : "node",
|
|
3009
|
+
entryFormat,
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
function linuxStatMatches(left, right) {
|
|
3014
|
+
return (
|
|
3015
|
+
String(left.dev) === String(right.dev) &&
|
|
3016
|
+
String(left.ino) === String(right.ino) &&
|
|
3017
|
+
Number(left.size) === Number(right.size) &&
|
|
3018
|
+
Number(left.mtimeMs) === Number(right.mtimeMs)
|
|
3019
|
+
);
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
function linuxOpenStatMatches(left, right) {
|
|
3023
|
+
const timestamp = (stat, nanosecondsKey, millisecondsKey) =>
|
|
3024
|
+
stat[nanosecondsKey] !== undefined
|
|
3025
|
+
? String(stat[nanosecondsKey])
|
|
3026
|
+
: String(Math.trunc(Number(stat[millisecondsKey] || 0) * 1_000_000));
|
|
3027
|
+
return (
|
|
3028
|
+
linuxStatMatches(left, right) &&
|
|
3029
|
+
String(left.mode) === String(right.mode) &&
|
|
3030
|
+
String(left.nlink) === String(right.nlink) &&
|
|
3031
|
+
timestamp(left, "ctimeNs", "ctimeMs") ===
|
|
3032
|
+
timestamp(right, "ctimeNs", "ctimeMs") &&
|
|
3033
|
+
timestamp(left, "mtimeNs", "mtimeMs") ===
|
|
3034
|
+
timestamp(right, "mtimeNs", "mtimeMs")
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
function readLinuxFdExactly(runtime, fd, bytes, position) {
|
|
3039
|
+
const result = Buffer.allocUnsafe(bytes);
|
|
3040
|
+
let offset = 0;
|
|
3041
|
+
while (offset < bytes) {
|
|
3042
|
+
const read = runtime.fs.readSync(
|
|
3043
|
+
fd,
|
|
3044
|
+
result,
|
|
3045
|
+
offset,
|
|
3046
|
+
bytes - offset,
|
|
3047
|
+
position + offset,
|
|
3048
|
+
);
|
|
3049
|
+
if (read <= 0) throw new Error("elf_file_ended_early");
|
|
3050
|
+
offset += read;
|
|
3051
|
+
}
|
|
3052
|
+
return result;
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
function hashLinuxOpenFile(runtime, fd, bytes) {
|
|
3056
|
+
const digest = crypto.createHash("sha256");
|
|
3057
|
+
const chunk = Buffer.allocUnsafe(
|
|
3058
|
+
Math.max(1, Math.min(LINUX_ATTESTATION_HASH_CHUNK_BYTES, bytes)),
|
|
3059
|
+
);
|
|
3060
|
+
let offset = 0;
|
|
3061
|
+
while (offset < bytes) {
|
|
3062
|
+
const read = runtime.fs.readSync(
|
|
3063
|
+
fd,
|
|
3064
|
+
chunk,
|
|
3065
|
+
0,
|
|
3066
|
+
Math.min(chunk.length, bytes - offset),
|
|
3067
|
+
offset,
|
|
3068
|
+
);
|
|
3069
|
+
if (read <= 0) throw new Error("elf_file_ended_early");
|
|
3070
|
+
digest.update(chunk.subarray(0, read));
|
|
3071
|
+
offset += read;
|
|
3072
|
+
}
|
|
3073
|
+
return digest.digest("hex");
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
/**
|
|
3077
|
+
* Classify one already-pinned plugin entry without invoking it or any host
|
|
3078
|
+
* inspection utility. The native slice accepts conventional fully static
|
|
3079
|
+
* ELF64 ET_EXEC images and a narrow static-PIE-shaped ELF:
|
|
3080
|
+
* ET_DYN without PT_INTERP, with one bounded PT_DYNAMIC table that declares
|
|
3081
|
+
* PIE but no external DT_NEEDED dependency.
|
|
3082
|
+
*/
|
|
3083
|
+
function inspectLinuxStaticNativeElf(runtime, fd, identity) {
|
|
3084
|
+
const expectedMachine = LINUX_ELF_MACHINES[runtime.arch];
|
|
3085
|
+
if (!expectedMachine) {
|
|
3086
|
+
throw new Error(`unsupported_elf_architecture:${String(runtime.arch)}`);
|
|
3087
|
+
}
|
|
3088
|
+
const before = runtime.fs.fstatSync(fd);
|
|
3089
|
+
if (
|
|
3090
|
+
!before.isFile() ||
|
|
3091
|
+
Number(before.nlink) !== 1 ||
|
|
3092
|
+
(Number(before.mode) & 0o111) === 0 ||
|
|
3093
|
+
(Number(before.mode) & 0o6000) !== 0 ||
|
|
3094
|
+
String(before.dev) !== String(identity?.fileId?.dev) ||
|
|
3095
|
+
String(before.ino) !== String(identity?.fileId?.ino) ||
|
|
3096
|
+
Number(before.size) !== Number(identity?.bytes) ||
|
|
3097
|
+
Number(before.mtimeMs) !== Number(identity?.mtimeMs) ||
|
|
3098
|
+
Number(before.size) < LINUX_ELF64_HEADER_BYTES
|
|
3099
|
+
) {
|
|
3100
|
+
throw new Error("native_entry_identity_changed");
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
const header = readLinuxFdExactly(runtime, fd, LINUX_ELF64_HEADER_BYTES, 0);
|
|
3104
|
+
if (
|
|
3105
|
+
header[0] !== 0x7f ||
|
|
3106
|
+
header[1] !== 0x45 ||
|
|
3107
|
+
header[2] !== 0x4c ||
|
|
3108
|
+
header[3] !== 0x46
|
|
3109
|
+
) {
|
|
3110
|
+
throw new Error("native_entry_not_elf");
|
|
3111
|
+
}
|
|
3112
|
+
if (header[4] !== 2) throw new Error("native_entry_not_elf64");
|
|
3113
|
+
if (header[5] !== 1) throw new Error("native_entry_not_little_endian");
|
|
3114
|
+
if (header[6] !== 1 || header.readUInt32LE(20) !== 1) {
|
|
3115
|
+
throw new Error("native_entry_invalid_elf_version");
|
|
3116
|
+
}
|
|
3117
|
+
const elfType = header.readUInt16LE(16);
|
|
3118
|
+
if (elfType !== LINUX_ELF_TYPE_EXEC && elfType !== LINUX_ELF_TYPE_DYN) {
|
|
3119
|
+
throw new Error("native_entry_unsupported_elf_type");
|
|
3120
|
+
}
|
|
3121
|
+
if (header.readUInt16LE(18) !== expectedMachine) {
|
|
3122
|
+
throw new Error("native_entry_architecture_mismatch");
|
|
3123
|
+
}
|
|
3124
|
+
if (header.readUInt16LE(52) !== LINUX_ELF64_HEADER_BYTES) {
|
|
3125
|
+
throw new Error("native_entry_invalid_elf_header");
|
|
3126
|
+
}
|
|
3127
|
+
const programHeaderBytes = header.readUInt16LE(54);
|
|
3128
|
+
const programHeaderCount = header.readUInt16LE(56);
|
|
3129
|
+
if (
|
|
3130
|
+
programHeaderBytes !== LINUX_ELF64_PROGRAM_HEADER_BYTES ||
|
|
3131
|
+
programHeaderCount < 1 ||
|
|
3132
|
+
programHeaderCount > LINUX_ELF_MAX_PROGRAM_HEADERS ||
|
|
3133
|
+
programHeaderCount === 0xffff
|
|
3134
|
+
) {
|
|
3135
|
+
throw new Error("native_entry_invalid_program_headers");
|
|
3136
|
+
}
|
|
3137
|
+
const programHeaderOffset = header.readBigUInt64LE(32);
|
|
3138
|
+
const tableBytes = BigInt(programHeaderBytes) * BigInt(programHeaderCount);
|
|
3139
|
+
const fileBytes = BigInt(before.size);
|
|
3140
|
+
if (
|
|
3141
|
+
programHeaderOffset > fileBytes ||
|
|
3142
|
+
tableBytes > fileBytes - programHeaderOffset ||
|
|
3143
|
+
programHeaderOffset > BigInt(Number.MAX_SAFE_INTEGER)
|
|
3144
|
+
) {
|
|
3145
|
+
throw new Error("native_entry_program_headers_out_of_bounds");
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
const entryAddress = header.readBigUInt64LE(24);
|
|
3149
|
+
let executableLoad = false;
|
|
3150
|
+
let entryInExecutableLoad = false;
|
|
3151
|
+
let nonExecutableStack = false;
|
|
3152
|
+
const loadSegments = [];
|
|
3153
|
+
const dynamicSegments = [];
|
|
3154
|
+
for (let index = 0; index < programHeaderCount; index += 1) {
|
|
3155
|
+
const offset =
|
|
3156
|
+
Number(programHeaderOffset) + index * LINUX_ELF64_PROGRAM_HEADER_BYTES;
|
|
3157
|
+
const program = readLinuxFdExactly(
|
|
3158
|
+
runtime,
|
|
3159
|
+
fd,
|
|
3160
|
+
LINUX_ELF64_PROGRAM_HEADER_BYTES,
|
|
3161
|
+
offset,
|
|
3162
|
+
);
|
|
3163
|
+
const type = program.readUInt32LE(0);
|
|
3164
|
+
const flags = program.readUInt32LE(4);
|
|
3165
|
+
const fileOffset = program.readBigUInt64LE(8);
|
|
3166
|
+
const virtualAddress = program.readBigUInt64LE(16);
|
|
3167
|
+
const fileSize = program.readBigUInt64LE(32);
|
|
3168
|
+
const memorySize = program.readBigUInt64LE(40);
|
|
3169
|
+
if (fileOffset > fileBytes || fileSize > fileBytes - fileOffset) {
|
|
3170
|
+
throw new Error("native_entry_segment_out_of_bounds");
|
|
3171
|
+
}
|
|
3172
|
+
if (type === LINUX_ELF_PROGRAM_LOAD && fileSize > memorySize) {
|
|
3173
|
+
throw new Error("native_entry_segment_out_of_bounds");
|
|
3174
|
+
}
|
|
3175
|
+
if (type === LINUX_ELF_PROGRAM_INTERP) {
|
|
3176
|
+
throw new Error("native_entry_interpreter_unsupported");
|
|
3177
|
+
}
|
|
3178
|
+
if (type === LINUX_ELF_PROGRAM_DYNAMIC) {
|
|
3179
|
+
if (elfType === LINUX_ELF_TYPE_EXEC) {
|
|
3180
|
+
throw new Error("native_entry_dynamic_elf_unsupported");
|
|
3181
|
+
}
|
|
3182
|
+
dynamicSegments.push({
|
|
3183
|
+
flags,
|
|
3184
|
+
fileOffset,
|
|
3185
|
+
virtualAddress,
|
|
3186
|
+
fileSize,
|
|
3187
|
+
memorySize,
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
if (type === LINUX_ELF_PROGRAM_GNU_STACK) {
|
|
3191
|
+
if ((flags & 0x1) !== 0) {
|
|
3192
|
+
throw new Error("native_entry_executable_stack_unsupported");
|
|
3193
|
+
}
|
|
3194
|
+
nonExecutableStack = true;
|
|
3195
|
+
}
|
|
3196
|
+
if (type === LINUX_ELF_PROGRAM_LOAD) {
|
|
3197
|
+
loadSegments.push({
|
|
3198
|
+
flags,
|
|
3199
|
+
fileOffset,
|
|
3200
|
+
virtualAddress,
|
|
3201
|
+
fileSize,
|
|
3202
|
+
memorySize,
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
if (type === LINUX_ELF_PROGRAM_LOAD && (flags & 0x1) !== 0) {
|
|
3206
|
+
if ((flags & 0x2) !== 0) {
|
|
3207
|
+
throw new Error("native_entry_writable_executable_segment");
|
|
3208
|
+
}
|
|
3209
|
+
executableLoad = true;
|
|
3210
|
+
if (
|
|
3211
|
+
entryAddress >= virtualAddress &&
|
|
3212
|
+
entryAddress - virtualAddress < memorySize
|
|
3213
|
+
) {
|
|
3214
|
+
entryInExecutableLoad = true;
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
if (!executableLoad || !entryInExecutableLoad) {
|
|
3219
|
+
throw new Error("native_entry_has_no_executable_entry_segment");
|
|
3220
|
+
}
|
|
3221
|
+
if (!nonExecutableStack) {
|
|
3222
|
+
throw new Error("native_entry_nonexecutable_stack_unattested");
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
if (elfType === LINUX_ELF_TYPE_DYN) {
|
|
3226
|
+
if (dynamicSegments.length === 0) {
|
|
3227
|
+
throw new Error("native_entry_static_pie_dynamic_segment_missing");
|
|
3228
|
+
}
|
|
3229
|
+
if (dynamicSegments.length !== 1) {
|
|
3230
|
+
throw new Error("native_entry_static_pie_dynamic_segment_ambiguous");
|
|
3231
|
+
}
|
|
3232
|
+
const dynamic = dynamicSegments[0];
|
|
3233
|
+
if ((dynamic.flags & 0x1) !== 0) {
|
|
3234
|
+
throw new Error("native_entry_static_pie_dynamic_segment_executable");
|
|
3235
|
+
}
|
|
3236
|
+
if (
|
|
3237
|
+
dynamic.fileSize < BigInt(LINUX_ELF64_DYNAMIC_ENTRY_BYTES) ||
|
|
3238
|
+
dynamic.fileSize % BigInt(LINUX_ELF64_DYNAMIC_ENTRY_BYTES) !== 0n ||
|
|
3239
|
+
dynamic.fileSize > dynamic.memorySize
|
|
3240
|
+
) {
|
|
3241
|
+
throw new Error("native_entry_static_pie_dynamic_segment_invalid");
|
|
3242
|
+
}
|
|
3243
|
+
const dynamicEntries =
|
|
3244
|
+
dynamic.fileSize / BigInt(LINUX_ELF64_DYNAMIC_ENTRY_BYTES);
|
|
3245
|
+
if (
|
|
3246
|
+
dynamicEntries < 1n ||
|
|
3247
|
+
dynamicEntries > BigInt(LINUX_ELF_MAX_DYNAMIC_ENTRIES)
|
|
3248
|
+
) {
|
|
3249
|
+
throw new Error("native_entry_static_pie_dynamic_table_too_large");
|
|
3250
|
+
}
|
|
3251
|
+
const containingLoad = loadSegments.find((load) => {
|
|
3252
|
+
if (
|
|
3253
|
+
dynamic.fileOffset < load.fileOffset ||
|
|
3254
|
+
dynamic.virtualAddress < load.virtualAddress
|
|
3255
|
+
) {
|
|
3256
|
+
return false;
|
|
3257
|
+
}
|
|
3258
|
+
const fileDelta = dynamic.fileOffset - load.fileOffset;
|
|
3259
|
+
const memoryDelta = dynamic.virtualAddress - load.virtualAddress;
|
|
3260
|
+
return (
|
|
3261
|
+
fileDelta === memoryDelta &&
|
|
3262
|
+
fileDelta <= load.fileSize &&
|
|
3263
|
+
dynamic.fileSize <= load.fileSize - fileDelta &&
|
|
3264
|
+
memoryDelta <= load.memorySize &&
|
|
3265
|
+
dynamic.memorySize <= load.memorySize - memoryDelta
|
|
3266
|
+
);
|
|
3267
|
+
});
|
|
3268
|
+
if (!containingLoad) {
|
|
3269
|
+
throw new Error("native_entry_static_pie_dynamic_segment_unmapped");
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
const dynamicTable = readLinuxFdExactly(
|
|
3273
|
+
runtime,
|
|
3274
|
+
fd,
|
|
3275
|
+
Number(dynamic.fileSize),
|
|
3276
|
+
Number(dynamic.fileOffset),
|
|
3277
|
+
);
|
|
3278
|
+
let terminated = false;
|
|
3279
|
+
let flags1 = null;
|
|
3280
|
+
for (
|
|
3281
|
+
let offset = 0;
|
|
3282
|
+
offset < dynamicTable.length;
|
|
3283
|
+
offset += LINUX_ELF64_DYNAMIC_ENTRY_BYTES
|
|
3284
|
+
) {
|
|
3285
|
+
const tag = dynamicTable.readBigUInt64LE(offset);
|
|
3286
|
+
const value = dynamicTable.readBigUInt64LE(offset + 8);
|
|
3287
|
+
if (tag === LINUX_ELF_DYNAMIC_NULL) {
|
|
3288
|
+
terminated = true;
|
|
3289
|
+
break;
|
|
3290
|
+
}
|
|
3291
|
+
if (tag === LINUX_ELF_DYNAMIC_NEEDED) {
|
|
3292
|
+
throw new Error("native_entry_static_pie_dependency_unsupported");
|
|
3293
|
+
}
|
|
3294
|
+
if (tag === LINUX_ELF_DYNAMIC_FLAGS_1) {
|
|
3295
|
+
if (flags1 !== null) {
|
|
3296
|
+
throw new Error("native_entry_static_pie_flags_ambiguous");
|
|
3297
|
+
}
|
|
3298
|
+
flags1 = value;
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
if (!terminated) {
|
|
3302
|
+
throw new Error("native_entry_static_pie_dynamic_table_unterminated");
|
|
3303
|
+
}
|
|
3304
|
+
if (flags1 === null || (flags1 & LINUX_ELF_DYNAMIC_FLAG_PIE) === 0n) {
|
|
3305
|
+
throw new Error("native_entry_static_pie_flag_missing");
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
const sha256 = hashLinuxOpenFile(runtime, fd, Number(before.size));
|
|
3310
|
+
const after = runtime.fs.fstatSync(fd);
|
|
3311
|
+
if (!linuxOpenStatMatches(before, after) || sha256 !== identity.sha256) {
|
|
3312
|
+
throw new Error("native_entry_changed_during_elf_attestation");
|
|
3313
|
+
}
|
|
3314
|
+
return Object.freeze({
|
|
3315
|
+
format:
|
|
3316
|
+
elfType === LINUX_ELF_TYPE_DYN
|
|
3317
|
+
? "elf64-static-pie-shaped-et-dyn"
|
|
3318
|
+
: "elf64-static-et-exec",
|
|
3319
|
+
architecture: runtime.arch,
|
|
3320
|
+
machine: expectedMachine,
|
|
3321
|
+
programHeaders: programHeaderCount,
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
function inspectLinuxStaticNativePath(runtime, identity) {
|
|
3326
|
+
let fd;
|
|
3327
|
+
try {
|
|
3328
|
+
const constants = runtime.fs.constants || fs.constants;
|
|
3329
|
+
const flags =
|
|
3330
|
+
Number(constants.O_RDONLY) |
|
|
3331
|
+
Number(constants.O_NOFOLLOW || 0) |
|
|
3332
|
+
Number(constants.O_NONBLOCK || 0);
|
|
3333
|
+
fd = runtime.fs.openSync(identity.realPath, flags);
|
|
3334
|
+
return inspectLinuxStaticNativeElf(runtime, fd, identity);
|
|
3335
|
+
} finally {
|
|
3336
|
+
if (fd !== undefined) {
|
|
3337
|
+
try {
|
|
3338
|
+
runtime.fs.closeSync(fd);
|
|
3339
|
+
} catch {
|
|
3340
|
+
// best effort
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
function linuxFdMountId(runtime, fd) {
|
|
3347
|
+
const contents = String(
|
|
3348
|
+
runtime.fs.readFileSync(`/proc/self/fdinfo/${fd}`, "utf8"),
|
|
3349
|
+
);
|
|
3350
|
+
const match = contents.match(/^mnt_id:\s*(\d+)\s*$/m);
|
|
3351
|
+
if (!match) throw new Error("fd_mount_identity_unavailable");
|
|
3352
|
+
return match[1];
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
function closeLinuxPinnedMounts(runtime, mounts) {
|
|
3356
|
+
for (const mount of mounts || []) {
|
|
3357
|
+
if (!Number.isInteger(mount?.fd)) continue;
|
|
3358
|
+
try {
|
|
3359
|
+
runtime.fs.closeSync(mount.fd);
|
|
3360
|
+
} catch {
|
|
3361
|
+
// Best effort. The process is already fail-closed if a pin cannot be
|
|
3362
|
+
// created; cleanup must not replace the original boundary error.
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
function pinLinuxRegularFile(
|
|
3368
|
+
runtime,
|
|
3369
|
+
source,
|
|
3370
|
+
destination,
|
|
3371
|
+
expectedStat,
|
|
3372
|
+
{
|
|
3373
|
+
requireSingleLink = false,
|
|
3374
|
+
openPath = source,
|
|
3375
|
+
verifyCurrentPath = true,
|
|
3376
|
+
expectedMountId = null,
|
|
3377
|
+
} = {},
|
|
3378
|
+
) {
|
|
3379
|
+
let fd;
|
|
3380
|
+
try {
|
|
3381
|
+
const constants = runtime.fs.constants || fs.constants;
|
|
3382
|
+
const flags =
|
|
3383
|
+
Number(constants.O_RDONLY) |
|
|
3384
|
+
Number(constants.O_NOFOLLOW || 0) |
|
|
3385
|
+
Number(constants.O_NONBLOCK || 0);
|
|
3386
|
+
fd = runtime.fs.openSync(openPath, flags);
|
|
3387
|
+
const opened = runtime.fs.fstatSync(fd);
|
|
3388
|
+
const current = verifyCurrentPath ? runtime.fs.statSync(source) : opened;
|
|
3389
|
+
if (
|
|
3390
|
+
!opened.isFile() ||
|
|
3391
|
+
!linuxStatMatches(opened, current) ||
|
|
3392
|
+
(expectedStat && !linuxStatMatches(opened, expectedStat)) ||
|
|
3393
|
+
(requireSingleLink && Number(opened.nlink) !== 1) ||
|
|
3394
|
+
(expectedMountId !== null &&
|
|
3395
|
+
linuxFdMountId(runtime, fd) !== expectedMountId)
|
|
3396
|
+
) {
|
|
3397
|
+
throw new Error("pinned_file_identity_changed");
|
|
3398
|
+
}
|
|
3399
|
+
return {
|
|
3400
|
+
source,
|
|
3401
|
+
destination,
|
|
3402
|
+
fd,
|
|
3403
|
+
fileId: {
|
|
3404
|
+
dev: String(opened.dev),
|
|
3405
|
+
ino: String(opened.ino),
|
|
3406
|
+
},
|
|
3407
|
+
bytes: Number(opened.size),
|
|
3408
|
+
mtimeMs: Number(opened.mtimeMs),
|
|
3409
|
+
};
|
|
3410
|
+
} catch (error) {
|
|
3411
|
+
if (fd !== undefined) {
|
|
3412
|
+
try {
|
|
3413
|
+
runtime.fs.closeSync(fd);
|
|
3414
|
+
} catch {
|
|
3415
|
+
// best effort
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
throw error;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
/**
|
|
3423
|
+
* Build a synthetic plugin tree from individually pinned regular files.
|
|
3424
|
+
* Never bind the mutable source directory itself: files added, hard-linked,
|
|
3425
|
+
* symlinked, or mount-injected after this scan cannot appear in the sandbox.
|
|
3426
|
+
*/
|
|
3427
|
+
function pinLinuxPluginTree(runtime, pluginRoot, rootIdentity) {
|
|
3428
|
+
const mounts = [];
|
|
3429
|
+
const directoryPins = [];
|
|
3430
|
+
try {
|
|
3431
|
+
const constants = runtime.fs.constants || fs.constants;
|
|
3432
|
+
const directoryFlags =
|
|
3433
|
+
Number(constants.O_RDONLY) |
|
|
3434
|
+
Number(constants.O_DIRECTORY || 0) |
|
|
3435
|
+
Number(constants.O_NOFOLLOW || 0) |
|
|
3436
|
+
Number(constants.O_NONBLOCK || 0);
|
|
3437
|
+
const rootFd = runtime.fs.openSync(pluginRoot, directoryFlags);
|
|
3438
|
+
directoryPins.push({ fd: rootFd });
|
|
3439
|
+
const rootStat = runtime.fs.fstatSync(rootFd);
|
|
3440
|
+
const rootPathStat = runtime.fs.lstatSync(pluginRoot);
|
|
3441
|
+
if (
|
|
3442
|
+
rootPathStat.isSymbolicLink() ||
|
|
3443
|
+
!rootStat.isDirectory() ||
|
|
3444
|
+
!linuxStatMatches(rootStat, rootPathStat) ||
|
|
3445
|
+
String(rootStat.dev) !== String(rootIdentity?.fileId?.dev) ||
|
|
3446
|
+
String(rootStat.ino) !== String(rootIdentity?.fileId?.ino)
|
|
3447
|
+
) {
|
|
3448
|
+
throw new Error("plugin_root_not_directory");
|
|
3449
|
+
}
|
|
3450
|
+
const rootMountId = linuxFdMountId(runtime, rootFd);
|
|
3451
|
+
const rootDevice = String(rootStat.dev);
|
|
3452
|
+
const directories = new Set(["/opt/chainless/plugin"]);
|
|
3453
|
+
const pending = [{ fd: rootFd, source: pluginRoot, relative: "" }];
|
|
3454
|
+
let entries = 0;
|
|
3455
|
+
while (pending.length > 0) {
|
|
3456
|
+
const current = pending.pop();
|
|
3457
|
+
const currentHandlePath = `/proc/self/fd/${current.fd}`;
|
|
3458
|
+
const children = runtime.fs
|
|
3459
|
+
.readdirSync(currentHandlePath, { withFileTypes: true })
|
|
3460
|
+
.map((child) => child.name)
|
|
3461
|
+
.sort((left, right) => left.localeCompare(right));
|
|
3462
|
+
for (const name of children) {
|
|
3463
|
+
if (
|
|
3464
|
+
typeof name !== "string" ||
|
|
3465
|
+
!name ||
|
|
3466
|
+
name === "." ||
|
|
3467
|
+
name === ".." ||
|
|
3468
|
+
name.includes("/") ||
|
|
3469
|
+
name.includes("\0")
|
|
3470
|
+
) {
|
|
3471
|
+
throw new Error("plugin_tree_entry_invalid");
|
|
3472
|
+
}
|
|
3473
|
+
entries += 1;
|
|
3474
|
+
if (entries > LINUX_BWRAP_MAX_PLUGIN_ENTRIES) {
|
|
3475
|
+
throw new Error("plugin_tree_too_large");
|
|
3476
|
+
}
|
|
3477
|
+
const source = path.posix.join(current.source, name);
|
|
3478
|
+
const sourceHandlePath = `${currentHandlePath}/${name}`;
|
|
3479
|
+
const relative = current.relative
|
|
3480
|
+
? path.posix.join(current.relative, name)
|
|
3481
|
+
: name;
|
|
3482
|
+
const destination = path.posix.join("/opt/chainless/plugin", relative);
|
|
3483
|
+
const stat = runtime.fs.lstatSync(sourceHandlePath);
|
|
3484
|
+
if (
|
|
3485
|
+
stat.isSymbolicLink() ||
|
|
3486
|
+
String(stat.dev) !== rootDevice ||
|
|
3487
|
+
stat.isSocket?.() ||
|
|
3488
|
+
stat.isFIFO?.() ||
|
|
3489
|
+
stat.isBlockDevice?.() ||
|
|
3490
|
+
stat.isCharacterDevice?.()
|
|
3491
|
+
) {
|
|
3492
|
+
throw new Error("plugin_tree_entry_unattested");
|
|
3493
|
+
}
|
|
3494
|
+
if (stat.isDirectory()) {
|
|
3495
|
+
const fd = runtime.fs.openSync(sourceHandlePath, directoryFlags);
|
|
3496
|
+
const opened = runtime.fs.fstatSync(fd);
|
|
3497
|
+
directoryPins.push({ fd });
|
|
3498
|
+
if (
|
|
3499
|
+
!opened.isDirectory() ||
|
|
3500
|
+
!linuxStatMatches(opened, stat) ||
|
|
3501
|
+
linuxFdMountId(runtime, fd) !== rootMountId
|
|
3502
|
+
) {
|
|
3503
|
+
throw new Error("plugin_tree_directory_changed");
|
|
3504
|
+
}
|
|
3505
|
+
directories.add(destination);
|
|
3506
|
+
pending.push({ fd, source, relative });
|
|
3507
|
+
} else if (stat.isFile() && Number(stat.nlink) === 1) {
|
|
3508
|
+
mounts.push(
|
|
3509
|
+
pinLinuxRegularFile(runtime, source, destination, stat, {
|
|
3510
|
+
openPath: sourceHandlePath,
|
|
3511
|
+
verifyCurrentPath: false,
|
|
3512
|
+
requireSingleLink: true,
|
|
3513
|
+
expectedMountId: rootMountId,
|
|
3514
|
+
}),
|
|
3515
|
+
);
|
|
3516
|
+
} else {
|
|
3517
|
+
throw new Error("plugin_tree_entry_unattested");
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
closeLinuxPinnedMounts(runtime, directoryPins);
|
|
3522
|
+
return {
|
|
3523
|
+
directories: [...directories].sort((left, right) => {
|
|
3524
|
+
const depth =
|
|
3525
|
+
left.split("/").filter(Boolean).length -
|
|
3526
|
+
right.split("/").filter(Boolean).length;
|
|
3527
|
+
return depth || left.localeCompare(right);
|
|
3528
|
+
}),
|
|
3529
|
+
mounts,
|
|
3530
|
+
};
|
|
3531
|
+
} catch (error) {
|
|
3532
|
+
closeLinuxPinnedMounts(runtime, directoryPins);
|
|
3533
|
+
closeLinuxPinnedMounts(runtime, mounts);
|
|
3534
|
+
throw error;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
|
|
3538
|
+
function buildLinuxNetworkSeccompFilter(arch) {
|
|
3539
|
+
const architecture = LINUX_SECCOMP_FILTERS[arch];
|
|
3540
|
+
if (!architecture) {
|
|
3541
|
+
throw new Error(`unsupported_seccomp_architecture:${String(arch)}`);
|
|
3542
|
+
}
|
|
3543
|
+
const instructions = [
|
|
3544
|
+
// Reject a different syscall ABI instead of interpreting overlapping
|
|
3545
|
+
// syscall numbers under the wrong architecture.
|
|
3546
|
+
[0x20, 0, 0, 4],
|
|
3547
|
+
[0x15, 1, 0, architecture.auditArch],
|
|
3548
|
+
[0x06, 0, 0, 0x80000000],
|
|
3549
|
+
[0x20, 0, 0, 0],
|
|
3550
|
+
];
|
|
3551
|
+
if (architecture.x32SyscallBit) {
|
|
3552
|
+
instructions.push([0x54, 0, 0, ~architecture.x32SyscallBit]);
|
|
3553
|
+
}
|
|
3554
|
+
instructions.push(
|
|
3555
|
+
// No socket family is needed by this direct foreground Plugin Node route.
|
|
3556
|
+
// Blocking creation rather than relying only on CLONE_NEWNET also closes
|
|
3557
|
+
// non-network-namespaced transports such as AF_VSOCK.
|
|
3558
|
+
[0x15, 0, 1, architecture.socketSyscall],
|
|
3559
|
+
[0x06, 0, 0, 0x00050001],
|
|
3560
|
+
[0x15, 0, 1, architecture.socketpairSyscall],
|
|
3561
|
+
[0x06, 0, 0, 0x00050001],
|
|
3562
|
+
// A new io_uring can create and connect sockets without the socket(2)
|
|
3563
|
+
// syscall. No ring descriptor is inherited by this narrow launch route.
|
|
3564
|
+
[0x15, 0, 1, architecture.ioUringSetupSyscall],
|
|
3565
|
+
[0x06, 0, 0, 0x00050001],
|
|
3566
|
+
[0x06, 0, 0, 0x7fff0000],
|
|
3567
|
+
);
|
|
3568
|
+
const buffer = Buffer.alloc(instructions.length * 8);
|
|
3569
|
+
instructions.forEach(([code, jumpTrue, jumpFalse, value], index) => {
|
|
3570
|
+
const offset = index * 8;
|
|
3571
|
+
buffer.writeUInt16LE(code, offset);
|
|
3572
|
+
buffer.writeUInt8(jumpTrue, offset + 2);
|
|
3573
|
+
buffer.writeUInt8(jumpFalse, offset + 3);
|
|
3574
|
+
buffer.writeUInt32LE(value >>> 0, offset + 4);
|
|
3575
|
+
});
|
|
3576
|
+
return {
|
|
3577
|
+
arch,
|
|
3578
|
+
buffer,
|
|
3579
|
+
sha256: crypto.createHash("sha256").update(buffer).digest("hex"),
|
|
648
3580
|
};
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
function pinLinuxNetworkSeccompFilter(runtime) {
|
|
3584
|
+
const filter = buildLinuxNetworkSeccompFilter(runtime.arch);
|
|
3585
|
+
let fd;
|
|
3586
|
+
try {
|
|
3587
|
+
for (const method of [
|
|
3588
|
+
"openSync",
|
|
3589
|
+
"writeSync",
|
|
3590
|
+
"readSync",
|
|
3591
|
+
"fchmodSync",
|
|
3592
|
+
"fsyncSync",
|
|
3593
|
+
"fstatSync",
|
|
3594
|
+
]) {
|
|
3595
|
+
if (typeof runtime.fs[method] !== "function") {
|
|
3596
|
+
throw new Error(`seccomp_filter_${method}_unavailable`);
|
|
3597
|
+
}
|
|
655
3598
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
3599
|
+
const constants = runtime.fs.constants || fs.constants;
|
|
3600
|
+
const flags =
|
|
3601
|
+
Number(constants.O_RDWR) |
|
|
3602
|
+
Number(constants.O_EXCL) |
|
|
3603
|
+
Number(constants.O_TMPFILE ?? LINUX_O_TMPFILE) |
|
|
3604
|
+
Number(constants.O_NOFOLLOW || 0) |
|
|
3605
|
+
Number(constants.O_NONBLOCK || 0);
|
|
3606
|
+
// An anonymous inode avoids a same-UID process opening the filter by path.
|
|
3607
|
+
// Mode 0400 applies from creation; this process can still write through
|
|
3608
|
+
// the already-open O_RDWR descriptor, but no new writable open is allowed.
|
|
3609
|
+
fd = runtime.fs.openSync("/tmp", flags, 0o400);
|
|
3610
|
+
runtime.fs.fchmodSync(fd, 0o400);
|
|
3611
|
+
let offset = 0;
|
|
3612
|
+
while (offset < filter.buffer.length) {
|
|
3613
|
+
const written = runtime.fs.writeSync(
|
|
3614
|
+
fd,
|
|
3615
|
+
filter.buffer,
|
|
3616
|
+
offset,
|
|
3617
|
+
filter.buffer.length - offset,
|
|
3618
|
+
offset,
|
|
3619
|
+
);
|
|
3620
|
+
if (written <= 0) throw new Error("seccomp_filter_write_failed");
|
|
3621
|
+
offset += written;
|
|
3622
|
+
}
|
|
3623
|
+
runtime.fs.fsyncSync(fd);
|
|
3624
|
+
const before = runtime.fs.fstatSync(fd);
|
|
3625
|
+
if (!before.isFile() || Number(before.size) !== filter.buffer.length) {
|
|
3626
|
+
throw new Error("seccomp_filter_identity_changed");
|
|
3627
|
+
}
|
|
3628
|
+
const observed = Buffer.allocUnsafe(filter.buffer.length);
|
|
3629
|
+
offset = 0;
|
|
3630
|
+
while (offset < observed.length) {
|
|
3631
|
+
const read = runtime.fs.readSync(
|
|
3632
|
+
fd,
|
|
3633
|
+
observed,
|
|
3634
|
+
offset,
|
|
3635
|
+
observed.length - offset,
|
|
3636
|
+
offset,
|
|
3637
|
+
);
|
|
3638
|
+
if (read <= 0) throw new Error("seccomp_filter_read_failed");
|
|
3639
|
+
offset += read;
|
|
3640
|
+
}
|
|
3641
|
+
const after = runtime.fs.fstatSync(fd);
|
|
3642
|
+
const observedSha256 = crypto
|
|
3643
|
+
.createHash("sha256")
|
|
3644
|
+
.update(observed)
|
|
3645
|
+
.digest("hex");
|
|
3646
|
+
if (!linuxStatMatches(before, after) || observedSha256 !== filter.sha256) {
|
|
3647
|
+
throw new Error("seccomp_filter_identity_changed");
|
|
3648
|
+
}
|
|
3649
|
+
return {
|
|
3650
|
+
fd,
|
|
3651
|
+
childFd: null,
|
|
3652
|
+
arch: filter.arch,
|
|
3653
|
+
sha256: filter.sha256,
|
|
3654
|
+
policy: "deny-network-creation",
|
|
3655
|
+
};
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
if (fd !== undefined) {
|
|
3658
|
+
try {
|
|
3659
|
+
runtime.fs.closeSync(fd);
|
|
3660
|
+
} catch {
|
|
3661
|
+
// best effort
|
|
675
3662
|
}
|
|
676
|
-
|
|
3663
|
+
}
|
|
3664
|
+
throw error;
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
677
3667
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
);
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
3668
|
+
function attestLinuxRootOwnedFile(
|
|
3669
|
+
runtime,
|
|
3670
|
+
filePath,
|
|
3671
|
+
{ executable = false } = {},
|
|
3672
|
+
) {
|
|
3673
|
+
try {
|
|
3674
|
+
if (!runtime.fs.existsSync(filePath)) return null;
|
|
3675
|
+
const source = linuxRealpath(runtime, filePath);
|
|
3676
|
+
const before = runtime.fs.lstatSync(source);
|
|
3677
|
+
if (
|
|
3678
|
+
!before.isFile() ||
|
|
3679
|
+
before.isSymbolicLink() ||
|
|
3680
|
+
Number(before.uid) !== 0 ||
|
|
3681
|
+
(Number(before.mode) & 0o022) !== 0 ||
|
|
3682
|
+
(executable && (Number(before.mode) & 0o111) === 0)
|
|
3683
|
+
) {
|
|
3684
|
+
return null;
|
|
693
3685
|
}
|
|
694
|
-
const
|
|
695
|
-
powershell,
|
|
696
|
-
[
|
|
697
|
-
"-NoLogo",
|
|
698
|
-
"-NoProfile",
|
|
699
|
-
"-NonInteractive",
|
|
700
|
-
"-ExecutionPolicy",
|
|
701
|
-
"Bypass",
|
|
702
|
-
"-File",
|
|
703
|
-
adapter.scriptPath,
|
|
704
|
-
"-CacheExecutable",
|
|
705
|
-
adapter.cacheExecutable,
|
|
706
|
-
"-CompileOnly",
|
|
707
|
-
],
|
|
708
|
-
{
|
|
709
|
-
shell: false,
|
|
710
|
-
windowsHide: true,
|
|
711
|
-
encoding: "utf8",
|
|
712
|
-
timeout: 30_000,
|
|
713
|
-
env: invocation.options.env || process.env,
|
|
714
|
-
},
|
|
715
|
-
);
|
|
716
|
-
adapter.cleanup?.();
|
|
3686
|
+
const after = runtime.fs.statSync(source);
|
|
717
3687
|
if (
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
3688
|
+
String(before.dev) !== String(after.dev) ||
|
|
3689
|
+
String(before.ino) !== String(after.ino) ||
|
|
3690
|
+
Number(before.size) !== Number(after.size) ||
|
|
3691
|
+
Number(before.mtimeMs) !== Number(after.mtimeMs)
|
|
721
3692
|
) {
|
|
722
|
-
|
|
723
|
-
return createSandboxPlan({
|
|
724
|
-
...base,
|
|
725
|
-
reason: "windows_native_adapter_compile_failed",
|
|
726
|
-
});
|
|
3693
|
+
return null;
|
|
727
3694
|
}
|
|
3695
|
+
return source;
|
|
3696
|
+
} catch {
|
|
3697
|
+
return null;
|
|
728
3698
|
}
|
|
3699
|
+
}
|
|
729
3700
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
SANDBOX_BOUNDARIES.RESOURCE_LIMITS,
|
|
737
|
-
SANDBOX_BOUNDARIES.PRIVILEGE_REDUCTION,
|
|
738
|
-
],
|
|
739
|
-
command: adapter.cacheExecutable,
|
|
740
|
-
args: helperArgs,
|
|
741
|
-
options,
|
|
742
|
-
...identityContract,
|
|
743
|
-
cleanup: cleanupIdentity,
|
|
744
|
-
});
|
|
3701
|
+
function attestLinuxBubblewrapBinary(runtime) {
|
|
3702
|
+
return (
|
|
3703
|
+
attestLinuxRootOwnedFile(runtime, LINUX_BWRAP_PATH, {
|
|
3704
|
+
executable: true,
|
|
3705
|
+
}) === LINUX_BWRAP_PATH
|
|
3706
|
+
);
|
|
745
3707
|
}
|
|
746
3708
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
return;
|
|
3709
|
+
function attestLinuxBubblewrapCapabilities(runtime) {
|
|
3710
|
+
let result;
|
|
3711
|
+
try {
|
|
3712
|
+
result = runtime.spawnSync(LINUX_BWRAP_PATH, ["--help"], {
|
|
3713
|
+
shell: false,
|
|
3714
|
+
encoding: "utf8",
|
|
3715
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3716
|
+
timeout: 10_000,
|
|
3717
|
+
env: {
|
|
3718
|
+
PATH: "/usr/bin:/bin",
|
|
3719
|
+
LANG: "C",
|
|
3720
|
+
LC_ALL: "C",
|
|
3721
|
+
},
|
|
3722
|
+
});
|
|
3723
|
+
} catch {
|
|
3724
|
+
return { ok: false, reason: "capability_probe_spawn_failed" };
|
|
763
3725
|
}
|
|
764
|
-
if (
|
|
765
|
-
return
|
|
3726
|
+
if (result?.error || result?.status !== 0) {
|
|
3727
|
+
return { ok: false, reason: "capability_probe_failed" };
|
|
3728
|
+
}
|
|
3729
|
+
const help = `${String(result.stdout || "")}\n${String(result.stderr || "")}`;
|
|
3730
|
+
for (const option of [
|
|
3731
|
+
"--ro-bind-fd",
|
|
3732
|
+
"--disable-userns",
|
|
3733
|
+
"--assert-userns-disabled",
|
|
3734
|
+
"--seccomp",
|
|
3735
|
+
]) {
|
|
3736
|
+
if (!help.includes(option)) {
|
|
3737
|
+
return {
|
|
3738
|
+
ok: false,
|
|
3739
|
+
reason: `required_option_missing:${option.slice(2)}`,
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
766
3742
|
}
|
|
3743
|
+
return { ok: true, reason: null };
|
|
767
3744
|
}
|
|
768
3745
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
3746
|
+
function parseLinuxLddPaths(output) {
|
|
3747
|
+
const paths = new Set();
|
|
3748
|
+
for (const line of String(output || "").split(/\r?\n/)) {
|
|
3749
|
+
if (line.includes("=> not found")) {
|
|
3750
|
+
throw new Error("runtime_dependency_missing");
|
|
3751
|
+
}
|
|
3752
|
+
const resolved = line.match(/=>\s+(\/[^\s(]+)\s+\(/)?.[1];
|
|
3753
|
+
const loader = line.match(/^\s*(\/[^\s(]+)\s+\(/)?.[1];
|
|
3754
|
+
if (resolved) paths.add(resolved);
|
|
3755
|
+
if (loader) paths.add(loader);
|
|
3756
|
+
}
|
|
3757
|
+
return [...paths];
|
|
3758
|
+
}
|
|
772
3759
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
3760
|
+
function collectLinuxNodeRuntimeMounts(runtime, runtimeIdentity) {
|
|
3761
|
+
if (
|
|
3762
|
+
attestLinuxRootOwnedFile(runtime, LINUX_LDD_PATH, {
|
|
3763
|
+
executable: true,
|
|
3764
|
+
}) !== LINUX_LDD_PATH
|
|
3765
|
+
) {
|
|
3766
|
+
throw new Error("ldd_unavailable");
|
|
3767
|
+
}
|
|
3768
|
+
const runtimeExpected = {
|
|
3769
|
+
dev: runtimeIdentity.fileId.dev,
|
|
3770
|
+
ino: runtimeIdentity.fileId.ino,
|
|
3771
|
+
size: runtimeIdentity.bytes,
|
|
3772
|
+
mtimeMs: runtimeIdentity.mtimeMs,
|
|
3773
|
+
};
|
|
3774
|
+
const inspectionPin = pinLinuxRegularFile(
|
|
3775
|
+
runtime,
|
|
3776
|
+
runtimeIdentity.realPath,
|
|
3777
|
+
"/opt/chainless/runtime/node",
|
|
3778
|
+
runtimeExpected,
|
|
3779
|
+
);
|
|
3780
|
+
let result;
|
|
781
3781
|
try {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
3782
|
+
result = runtime.spawnSync(LINUX_LDD_PATH, ["/proc/self/fd/3"], {
|
|
3783
|
+
shell: false,
|
|
3784
|
+
encoding: "utf8",
|
|
3785
|
+
stdio: ["ignore", "pipe", "pipe", inspectionPin.fd],
|
|
3786
|
+
timeout: 10_000,
|
|
3787
|
+
env: {
|
|
3788
|
+
PATH: "/usr/bin:/bin",
|
|
3789
|
+
LANG: "C",
|
|
3790
|
+
LC_ALL: "C",
|
|
3791
|
+
},
|
|
3792
|
+
});
|
|
3793
|
+
} finally {
|
|
3794
|
+
closeLinuxPinnedMounts(runtime, [inspectionPin]);
|
|
790
3795
|
}
|
|
791
|
-
if (
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
3796
|
+
if (result?.error || result?.status !== 0) {
|
|
3797
|
+
throw new Error("runtime_dependency_probe_failed");
|
|
3798
|
+
}
|
|
3799
|
+
const mounts = [
|
|
3800
|
+
{
|
|
3801
|
+
source: runtimeIdentity.realPath,
|
|
3802
|
+
destination: "/opt/chainless/runtime/node",
|
|
3803
|
+
},
|
|
3804
|
+
];
|
|
3805
|
+
for (const destination of parseLinuxLddPaths(result.stdout)) {
|
|
3806
|
+
if (
|
|
3807
|
+
path.posix.normalize(destination) !== destination ||
|
|
3808
|
+
!["/lib/", "/lib64/", "/usr/lib/"].some((prefix) =>
|
|
3809
|
+
destination.startsWith(prefix),
|
|
3810
|
+
)
|
|
3811
|
+
) {
|
|
3812
|
+
throw new Error("runtime_dependency_outside_system_library_roots");
|
|
3813
|
+
}
|
|
3814
|
+
const source = attestLinuxRootOwnedFile(runtime, destination);
|
|
3815
|
+
if (!source) {
|
|
3816
|
+
throw new Error("runtime_dependency_unattested");
|
|
3817
|
+
}
|
|
3818
|
+
mounts.push({ source, destination });
|
|
3819
|
+
}
|
|
3820
|
+
if (runtime.fs.existsSync("/etc/ld.so.cache")) {
|
|
3821
|
+
const source = attestLinuxRootOwnedFile(runtime, "/etc/ld.so.cache");
|
|
3822
|
+
if (!source) {
|
|
3823
|
+
throw new Error("loader_cache_unattested");
|
|
3824
|
+
}
|
|
3825
|
+
mounts.push({ source, destination: "/etc/ld.so.cache" });
|
|
3826
|
+
}
|
|
3827
|
+
const byDestination = new Map();
|
|
3828
|
+
for (const mount of mounts) {
|
|
3829
|
+
const existing = byDestination.get(mount.destination);
|
|
3830
|
+
if (existing && existing.source !== mount.source) {
|
|
3831
|
+
throw new Error("runtime_mount_collision");
|
|
3832
|
+
}
|
|
3833
|
+
byDestination.set(mount.destination, mount);
|
|
3834
|
+
}
|
|
3835
|
+
return [...byDestination.values()].sort((left, right) =>
|
|
3836
|
+
left.destination.localeCompare(right.destination),
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
function pinLinuxRuntimeMounts(runtime, runtimeMounts, runtimeIdentity) {
|
|
3841
|
+
const mounts = [];
|
|
3842
|
+
try {
|
|
3843
|
+
for (const mount of runtimeMounts) {
|
|
3844
|
+
const expected =
|
|
3845
|
+
mount.source === runtimeIdentity.realPath
|
|
3846
|
+
? {
|
|
3847
|
+
dev: runtimeIdentity.fileId.dev,
|
|
3848
|
+
ino: runtimeIdentity.fileId.ino,
|
|
3849
|
+
size: runtimeIdentity.bytes,
|
|
3850
|
+
mtimeMs: runtimeIdentity.mtimeMs,
|
|
3851
|
+
}
|
|
3852
|
+
: runtime.fs.statSync(mount.source);
|
|
3853
|
+
mounts.push(
|
|
3854
|
+
pinLinuxRegularFile(runtime, mount.source, mount.destination, expected),
|
|
3855
|
+
);
|
|
3856
|
+
}
|
|
3857
|
+
return mounts;
|
|
3858
|
+
} catch (error) {
|
|
3859
|
+
closeLinuxPinnedMounts(runtime, mounts);
|
|
3860
|
+
throw error;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
function linuxSandboxEnvironment() {
|
|
3865
|
+
return {
|
|
3866
|
+
HOME: "/home/sandbox",
|
|
3867
|
+
TMPDIR: "/tmp",
|
|
3868
|
+
PATH: "/opt/chainless/runtime",
|
|
3869
|
+
LANG: "C.UTF-8",
|
|
3870
|
+
LC_ALL: "C.UTF-8",
|
|
3871
|
+
TZ: "UTC",
|
|
3872
|
+
OPENSSL_CONF: "/dev/null",
|
|
3873
|
+
CHAINLESS_SANDBOXED: "1",
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
function buildLinuxBubblewrapPolicyArgs(
|
|
3878
|
+
pluginDirectories,
|
|
3879
|
+
pinnedMounts,
|
|
3880
|
+
environment,
|
|
3881
|
+
seccompFilter,
|
|
3882
|
+
) {
|
|
3883
|
+
const directories = new Set([
|
|
3884
|
+
"/opt",
|
|
3885
|
+
"/opt/chainless",
|
|
3886
|
+
"/opt/chainless/runtime",
|
|
3887
|
+
"/opt/chainless/plugin",
|
|
3888
|
+
"/home",
|
|
3889
|
+
"/home/sandbox",
|
|
3890
|
+
"/tmp",
|
|
3891
|
+
"/run",
|
|
3892
|
+
"/var",
|
|
3893
|
+
"/var/tmp",
|
|
3894
|
+
"/proc",
|
|
3895
|
+
"/dev",
|
|
3896
|
+
...(pluginDirectories || []),
|
|
3897
|
+
]);
|
|
3898
|
+
for (const mount of pinnedMounts) {
|
|
3899
|
+
let parent = path.posix.dirname(mount.destination);
|
|
3900
|
+
while (parent !== "/") {
|
|
3901
|
+
directories.add(parent);
|
|
3902
|
+
parent = path.posix.dirname(parent);
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
const orderedDirectories = [...directories].sort((left, right) => {
|
|
3906
|
+
const depth =
|
|
3907
|
+
left.split("/").filter(Boolean).length -
|
|
3908
|
+
right.split("/").filter(Boolean).length;
|
|
3909
|
+
return depth || left.localeCompare(right);
|
|
3910
|
+
});
|
|
3911
|
+
const args = [
|
|
3912
|
+
"--die-with-parent",
|
|
3913
|
+
"--new-session",
|
|
3914
|
+
"--unshare-user",
|
|
3915
|
+
"--disable-userns",
|
|
3916
|
+
"--assert-userns-disabled",
|
|
3917
|
+
"--unshare-pid",
|
|
3918
|
+
"--unshare-ipc",
|
|
3919
|
+
"--unshare-net",
|
|
3920
|
+
"--unshare-uts",
|
|
3921
|
+
"--unshare-cgroup-try",
|
|
3922
|
+
"--cap-drop",
|
|
3923
|
+
"ALL",
|
|
3924
|
+
"--hostname",
|
|
3925
|
+
"chainless-sandbox",
|
|
3926
|
+
"--clearenv",
|
|
3927
|
+
];
|
|
3928
|
+
for (const directory of orderedDirectories) {
|
|
3929
|
+
args.push("--dir", directory);
|
|
3930
|
+
}
|
|
3931
|
+
for (const mount of pinnedMounts) {
|
|
3932
|
+
args.push("--ro-bind-fd", String(mount.childFd), mount.destination);
|
|
3933
|
+
}
|
|
3934
|
+
args.push(
|
|
3935
|
+
"--seccomp",
|
|
3936
|
+
String(seccompFilter.childFd),
|
|
3937
|
+
"--remount-ro",
|
|
3938
|
+
"/",
|
|
3939
|
+
"--perms",
|
|
3940
|
+
"1777",
|
|
3941
|
+
"--size",
|
|
3942
|
+
String(64 * 1024 * 1024),
|
|
3943
|
+
"--tmpfs",
|
|
3944
|
+
"/tmp",
|
|
3945
|
+
"--perms",
|
|
3946
|
+
"0755",
|
|
3947
|
+
"--size",
|
|
3948
|
+
String(16 * 1024 * 1024),
|
|
3949
|
+
"--tmpfs",
|
|
3950
|
+
"/run",
|
|
3951
|
+
"--perms",
|
|
3952
|
+
"1777",
|
|
3953
|
+
"--size",
|
|
3954
|
+
String(32 * 1024 * 1024),
|
|
3955
|
+
"--tmpfs",
|
|
3956
|
+
"/var/tmp",
|
|
3957
|
+
"--perms",
|
|
3958
|
+
"0700",
|
|
3959
|
+
"--size",
|
|
3960
|
+
String(16 * 1024 * 1024),
|
|
3961
|
+
"--tmpfs",
|
|
3962
|
+
"/home/sandbox",
|
|
3963
|
+
"--proc",
|
|
3964
|
+
"/proc",
|
|
3965
|
+
"--dev",
|
|
3966
|
+
"/dev",
|
|
3967
|
+
);
|
|
3968
|
+
for (const key of Object.keys(environment).sort()) {
|
|
3969
|
+
args.push("--setenv", key, environment[key]);
|
|
798
3970
|
}
|
|
3971
|
+
args.push("--chdir", "/opt/chainless/plugin");
|
|
3972
|
+
return args;
|
|
3973
|
+
}
|
|
799
3974
|
|
|
3975
|
+
function probeLinuxBubblewrapPolicy(
|
|
3976
|
+
runtime,
|
|
3977
|
+
policyArgs,
|
|
3978
|
+
pinnedDescriptors,
|
|
3979
|
+
targetRuntime,
|
|
3980
|
+
) {
|
|
800
3981
|
let result;
|
|
801
3982
|
try {
|
|
802
3983
|
result = runtime.spawnSync(
|
|
803
3984
|
LINUX_BWRAP_PATH,
|
|
804
|
-
[
|
|
3985
|
+
[
|
|
3986
|
+
...policyArgs,
|
|
3987
|
+
"--",
|
|
3988
|
+
"/opt/chainless/runtime/node",
|
|
3989
|
+
"-e",
|
|
3990
|
+
`process.stdout.write(${JSON.stringify(
|
|
3991
|
+
LINUX_BWRAP_NODE_PROBE_SENTINEL,
|
|
3992
|
+
)})`,
|
|
3993
|
+
],
|
|
805
3994
|
{
|
|
3995
|
+
cwd: "/",
|
|
806
3996
|
shell: false,
|
|
807
|
-
|
|
808
|
-
|
|
3997
|
+
encoding: "utf8",
|
|
3998
|
+
stdio: linuxStdioWithPinnedMounts(null, pinnedDescriptors, {
|
|
3999
|
+
probe: true,
|
|
4000
|
+
}),
|
|
4001
|
+
timeout: 15_000,
|
|
809
4002
|
env: {
|
|
810
4003
|
PATH: "/usr/bin:/bin",
|
|
811
4004
|
LANG: "C",
|
|
@@ -814,28 +4007,30 @@ function probeLinuxBubblewrapRuntime(runtime) {
|
|
|
814
4007
|
},
|
|
815
4008
|
);
|
|
816
4009
|
} catch {
|
|
817
|
-
return
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
4010
|
+
return linuxBubblewrapProbe(
|
|
4011
|
+
true,
|
|
4012
|
+
false,
|
|
4013
|
+
"probe_spawn_failed",
|
|
4014
|
+
targetRuntime,
|
|
4015
|
+
);
|
|
823
4016
|
}
|
|
824
|
-
|
|
825
4017
|
if (result?.error || result?.status !== 0) {
|
|
826
|
-
return
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
4018
|
+
return linuxBubblewrapProbe(
|
|
4019
|
+
true,
|
|
4020
|
+
false,
|
|
4021
|
+
result?.error ? "probe_spawn_failed" : "probe_failed",
|
|
4022
|
+
targetRuntime,
|
|
4023
|
+
);
|
|
832
4024
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
4025
|
+
if (String(result.stdout) !== LINUX_BWRAP_NODE_PROBE_SENTINEL) {
|
|
4026
|
+
return linuxBubblewrapProbe(
|
|
4027
|
+
true,
|
|
4028
|
+
false,
|
|
4029
|
+
"node_runtime_probe_failed",
|
|
4030
|
+
targetRuntime,
|
|
4031
|
+
);
|
|
4032
|
+
}
|
|
4033
|
+
return linuxBubblewrapProbe(true, true, null, targetRuntime);
|
|
839
4034
|
}
|
|
840
4035
|
|
|
841
4036
|
/**
|
|
@@ -886,17 +4081,370 @@ export function applyLinuxSandbox(
|
|
|
886
4081
|
);
|
|
887
4082
|
|
|
888
4083
|
if (requiresStrongLinuxBoundary) {
|
|
889
|
-
const
|
|
4084
|
+
const requestedEntryRuntime =
|
|
4085
|
+
sandboxOpts.executionContract?.kind ===
|
|
4086
|
+
"strict-plugin-native-static-elf-bin"
|
|
4087
|
+
? "native-static-elf"
|
|
4088
|
+
: "node";
|
|
4089
|
+
const validation = validateLinuxPluginContract(
|
|
4090
|
+
command,
|
|
4091
|
+
args,
|
|
4092
|
+
spawnOpts,
|
|
4093
|
+
sandboxOpts.executionContract,
|
|
4094
|
+
runtime,
|
|
4095
|
+
sandboxOpts.sync,
|
|
4096
|
+
);
|
|
4097
|
+
if (!validation.ok) {
|
|
4098
|
+
const missing = validation.reason === "execution_contract_missing";
|
|
4099
|
+
return createSandboxPlan({
|
|
4100
|
+
...base,
|
|
4101
|
+
backend: null,
|
|
4102
|
+
candidateBackend: LINUX_BWRAP_BACKEND,
|
|
4103
|
+
policyAttested: false,
|
|
4104
|
+
runtimeProbe: linuxBubblewrapProbe(
|
|
4105
|
+
false,
|
|
4106
|
+
false,
|
|
4107
|
+
validation.reason,
|
|
4108
|
+
requestedEntryRuntime,
|
|
4109
|
+
),
|
|
4110
|
+
reason: missing
|
|
4111
|
+
? "linux_bwrap_execution_contract_missing"
|
|
4112
|
+
: "linux_bwrap_execution_contract_invalid",
|
|
4113
|
+
guarantees: [],
|
|
4114
|
+
});
|
|
4115
|
+
}
|
|
4116
|
+
if (!attestLinuxBubblewrapBinary(runtime)) {
|
|
4117
|
+
return createSandboxPlan({
|
|
4118
|
+
...base,
|
|
4119
|
+
backend: null,
|
|
4120
|
+
candidateBackend: LINUX_BWRAP_BACKEND,
|
|
4121
|
+
policyAttested: false,
|
|
4122
|
+
runtimeProbe: linuxBubblewrapProbe(
|
|
4123
|
+
false,
|
|
4124
|
+
false,
|
|
4125
|
+
"binary_missing_or_unattested",
|
|
4126
|
+
validation.entryRuntime,
|
|
4127
|
+
),
|
|
4128
|
+
reason: "linux_bwrap_unavailable",
|
|
4129
|
+
guarantees: [],
|
|
4130
|
+
});
|
|
4131
|
+
}
|
|
4132
|
+
const capabilities = attestLinuxBubblewrapCapabilities(runtime);
|
|
4133
|
+
if (!capabilities.ok) {
|
|
4134
|
+
return createSandboxPlan({
|
|
4135
|
+
...base,
|
|
4136
|
+
backend: null,
|
|
4137
|
+
candidateBackend: LINUX_BWRAP_BACKEND,
|
|
4138
|
+
policyAttested: false,
|
|
4139
|
+
runtimeProbe: linuxBubblewrapProbe(
|
|
4140
|
+
true,
|
|
4141
|
+
false,
|
|
4142
|
+
capabilities.reason,
|
|
4143
|
+
validation.entryRuntime,
|
|
4144
|
+
),
|
|
4145
|
+
reason: "linux_bwrap_unavailable",
|
|
4146
|
+
guarantees: [],
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
let runtimeMounts;
|
|
4151
|
+
let pinnedRuntimeMounts = [];
|
|
4152
|
+
try {
|
|
4153
|
+
runtimeMounts = collectLinuxNodeRuntimeMounts(
|
|
4154
|
+
runtime,
|
|
4155
|
+
validation.contract.runtimeIdentity,
|
|
4156
|
+
);
|
|
4157
|
+
pinnedRuntimeMounts = pinLinuxRuntimeMounts(
|
|
4158
|
+
runtime,
|
|
4159
|
+
runtimeMounts,
|
|
4160
|
+
validation.contract.runtimeIdentity,
|
|
4161
|
+
);
|
|
4162
|
+
} catch (error) {
|
|
4163
|
+
closeLinuxPinnedMounts(runtime, pinnedRuntimeMounts);
|
|
4164
|
+
return createSandboxPlan({
|
|
4165
|
+
...base,
|
|
4166
|
+
backend: null,
|
|
4167
|
+
candidateBackend: LINUX_BWRAP_BACKEND,
|
|
4168
|
+
policyAttested: false,
|
|
4169
|
+
runtimeProbe: linuxBubblewrapProbe(
|
|
4170
|
+
false,
|
|
4171
|
+
false,
|
|
4172
|
+
error.message || "runtime_mounts_unattested",
|
|
4173
|
+
validation.entryRuntime,
|
|
4174
|
+
),
|
|
4175
|
+
reason: "linux_bwrap_runtime_unattested",
|
|
4176
|
+
guarantees: [],
|
|
4177
|
+
});
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
let pinnedMounts = [];
|
|
4181
|
+
let pluginTree;
|
|
4182
|
+
let entryFormat = null;
|
|
4183
|
+
let entryMount;
|
|
4184
|
+
try {
|
|
4185
|
+
pluginTree = pinLinuxPluginTree(
|
|
4186
|
+
runtime,
|
|
4187
|
+
validation.contract.pluginRoot,
|
|
4188
|
+
validation.contract.rootIdentity,
|
|
4189
|
+
);
|
|
4190
|
+
pinnedMounts = assignLinuxMountChildFds([
|
|
4191
|
+
...pinnedRuntimeMounts,
|
|
4192
|
+
...pluginTree.mounts,
|
|
4193
|
+
]);
|
|
4194
|
+
entryMount = pinnedMounts.find(
|
|
4195
|
+
(mount) => mount.source === validation.contract.entryIdentity.realPath,
|
|
4196
|
+
);
|
|
4197
|
+
if (
|
|
4198
|
+
!entryMount ||
|
|
4199
|
+
String(entryMount.fileId.dev) !==
|
|
4200
|
+
String(validation.contract.entryIdentity.fileId.dev) ||
|
|
4201
|
+
String(entryMount.fileId.ino) !==
|
|
4202
|
+
String(validation.contract.entryIdentity.fileId.ino) ||
|
|
4203
|
+
entryMount.bytes !== Number(validation.contract.entryIdentity.bytes) ||
|
|
4204
|
+
entryMount.mtimeMs !== Number(validation.contract.entryIdentity.mtimeMs)
|
|
4205
|
+
) {
|
|
4206
|
+
throw new Error("plugin_entry_pin_mismatch");
|
|
4207
|
+
}
|
|
4208
|
+
if (validation.entryRuntime === "native-static-elf") {
|
|
4209
|
+
entryFormat = inspectLinuxStaticNativeElf(
|
|
4210
|
+
runtime,
|
|
4211
|
+
entryMount.fd,
|
|
4212
|
+
validation.contract.entryIdentity,
|
|
4213
|
+
);
|
|
4214
|
+
if (
|
|
4215
|
+
JSON.stringify(entryFormat) !== JSON.stringify(validation.entryFormat)
|
|
4216
|
+
) {
|
|
4217
|
+
throw new Error("native_entry_format_changed_before_pin");
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
} catch (error) {
|
|
4221
|
+
if (pinnedMounts.length > 0) {
|
|
4222
|
+
closeLinuxPinnedMounts(runtime, pinnedMounts);
|
|
4223
|
+
} else {
|
|
4224
|
+
closeLinuxPinnedMounts(runtime, pinnedRuntimeMounts);
|
|
4225
|
+
closeLinuxPinnedMounts(runtime, pluginTree?.mounts);
|
|
4226
|
+
}
|
|
4227
|
+
return createSandboxPlan({
|
|
4228
|
+
...base,
|
|
4229
|
+
backend: null,
|
|
4230
|
+
candidateBackend: LINUX_BWRAP_BACKEND,
|
|
4231
|
+
policyAttested: false,
|
|
4232
|
+
runtimeProbe: linuxBubblewrapProbe(
|
|
4233
|
+
false,
|
|
4234
|
+
false,
|
|
4235
|
+
error.message || "plugin_tree_unattested",
|
|
4236
|
+
validation.entryRuntime,
|
|
4237
|
+
),
|
|
4238
|
+
reason: "linux_bwrap_plugin_tree_unattested",
|
|
4239
|
+
guarantees: [],
|
|
4240
|
+
});
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
let probeSeccompFilter;
|
|
4244
|
+
let seccompFilter;
|
|
4245
|
+
try {
|
|
4246
|
+
probeSeccompFilter = pinLinuxNetworkSeccompFilter(runtime);
|
|
4247
|
+
seccompFilter = pinLinuxNetworkSeccompFilter(runtime);
|
|
4248
|
+
const seccompChildFd = LINUX_BWRAP_FIRST_MOUNT_FD + pinnedMounts.length;
|
|
4249
|
+
probeSeccompFilter.childFd = seccompChildFd;
|
|
4250
|
+
seccompFilter.childFd = seccompChildFd;
|
|
4251
|
+
if (probeSeccompFilter.sha256 !== seccompFilter.sha256) {
|
|
4252
|
+
throw new Error("seccomp_filter_digest_mismatch");
|
|
4253
|
+
}
|
|
4254
|
+
} catch (error) {
|
|
4255
|
+
closeLinuxPinnedMounts(runtime, pinnedMounts);
|
|
4256
|
+
closeLinuxPinnedMounts(
|
|
4257
|
+
runtime,
|
|
4258
|
+
[probeSeccompFilter, seccompFilter].filter(Boolean),
|
|
4259
|
+
);
|
|
4260
|
+
return createSandboxPlan({
|
|
4261
|
+
...base,
|
|
4262
|
+
backend: null,
|
|
4263
|
+
candidateBackend: LINUX_BWRAP_BACKEND,
|
|
4264
|
+
policyAttested: false,
|
|
4265
|
+
runtimeProbe: linuxBubblewrapProbe(
|
|
4266
|
+
false,
|
|
4267
|
+
false,
|
|
4268
|
+
error.message || "seccomp_filter_unattested",
|
|
4269
|
+
validation.entryRuntime,
|
|
4270
|
+
),
|
|
4271
|
+
reason: "linux_bwrap_seccomp_unattested",
|
|
4272
|
+
guarantees: [],
|
|
4273
|
+
});
|
|
4274
|
+
}
|
|
4275
|
+
const pinnedDescriptors = [...pinnedMounts, seccompFilter];
|
|
4276
|
+
const probeDescriptors = [...pinnedMounts, probeSeccompFilter];
|
|
4277
|
+
const environment = linuxSandboxEnvironment();
|
|
4278
|
+
const policyArgs = buildLinuxBubblewrapPolicyArgs(
|
|
4279
|
+
pluginTree.directories,
|
|
4280
|
+
pinnedMounts,
|
|
4281
|
+
environment,
|
|
4282
|
+
seccompFilter,
|
|
4283
|
+
);
|
|
4284
|
+
const entryRelative = path.posix.relative(
|
|
4285
|
+
validation.contract.pluginRoot,
|
|
4286
|
+
validation.contract.entryIdentity.realPath,
|
|
4287
|
+
);
|
|
4288
|
+
const sandboxEntry = path.posix.join(
|
|
4289
|
+
"/opt/chainless/plugin",
|
|
4290
|
+
entryRelative,
|
|
4291
|
+
);
|
|
4292
|
+
const targetArgs =
|
|
4293
|
+
validation.entryRuntime === "native-static-elf"
|
|
4294
|
+
? [sandboxEntry, ...args]
|
|
4295
|
+
: ["/opt/chainless/runtime/node", sandboxEntry, ...args.slice(1)];
|
|
4296
|
+
const policyDigest = crypto
|
|
4297
|
+
.createHash("sha256")
|
|
4298
|
+
.update(
|
|
4299
|
+
JSON.stringify({
|
|
4300
|
+
version: 1,
|
|
4301
|
+
backend: LINUX_BWRAP_BACKEND,
|
|
4302
|
+
contract: {
|
|
4303
|
+
kind: validation.contract.kind,
|
|
4304
|
+
pluginRoot: validation.contract.pluginRoot,
|
|
4305
|
+
root: validation.contract.rootIdentity,
|
|
4306
|
+
entry: validation.contract.entryIdentity,
|
|
4307
|
+
runtime: validation.contract.runtimeIdentity,
|
|
4308
|
+
entryRuntime: validation.entryRuntime,
|
|
4309
|
+
entryFormat,
|
|
4310
|
+
},
|
|
4311
|
+
mounts: pinnedMounts.map((mount) => ({
|
|
4312
|
+
destination: mount.destination,
|
|
4313
|
+
fileId: mount.fileId,
|
|
4314
|
+
bytes: mount.bytes,
|
|
4315
|
+
mtimeMs: mount.mtimeMs,
|
|
4316
|
+
})),
|
|
4317
|
+
seccomp: {
|
|
4318
|
+
arch: seccompFilter.arch,
|
|
4319
|
+
policy: seccompFilter.policy,
|
|
4320
|
+
sha256: seccompFilter.sha256,
|
|
4321
|
+
},
|
|
4322
|
+
policyArgs,
|
|
4323
|
+
target:
|
|
4324
|
+
validation.entryRuntime === "native-static-elf"
|
|
4325
|
+
? targetArgs.slice(0, 1)
|
|
4326
|
+
: targetArgs.slice(0, 2),
|
|
4327
|
+
}),
|
|
4328
|
+
)
|
|
4329
|
+
.digest("hex");
|
|
4330
|
+
const runtimeProbe = Object.freeze(
|
|
4331
|
+
probeLinuxBubblewrapPolicy(
|
|
4332
|
+
runtime,
|
|
4333
|
+
policyArgs,
|
|
4334
|
+
probeDescriptors,
|
|
4335
|
+
validation.entryRuntime,
|
|
4336
|
+
),
|
|
4337
|
+
);
|
|
4338
|
+
closeLinuxPinnedMounts(runtime, [probeSeccompFilter]);
|
|
4339
|
+
if (!runtimeProbe.runnable) {
|
|
4340
|
+
closeLinuxPinnedMounts(runtime, pinnedDescriptors);
|
|
4341
|
+
return createSandboxPlan({
|
|
4342
|
+
...base,
|
|
4343
|
+
backend: null,
|
|
4344
|
+
candidateBackend: LINUX_BWRAP_BACKEND,
|
|
4345
|
+
policyAttested: false,
|
|
4346
|
+
policyDigest,
|
|
4347
|
+
runtimeProbe,
|
|
4348
|
+
reason: "linux_bwrap_policy_probe_failed",
|
|
4349
|
+
guarantees: [],
|
|
4350
|
+
});
|
|
4351
|
+
}
|
|
4352
|
+
const finalValidation = validateLinuxPluginContract(
|
|
4353
|
+
command,
|
|
4354
|
+
args,
|
|
4355
|
+
spawnOpts,
|
|
4356
|
+
validation.contract,
|
|
4357
|
+
runtime,
|
|
4358
|
+
sandboxOpts.sync,
|
|
4359
|
+
);
|
|
4360
|
+
if (!finalValidation.ok) {
|
|
4361
|
+
closeLinuxPinnedMounts(runtime, pinnedDescriptors);
|
|
4362
|
+
return createSandboxPlan({
|
|
4363
|
+
...base,
|
|
4364
|
+
backend: null,
|
|
4365
|
+
candidateBackend: LINUX_BWRAP_BACKEND,
|
|
4366
|
+
policyAttested: false,
|
|
4367
|
+
policyDigest,
|
|
4368
|
+
runtimeProbe: linuxBubblewrapProbe(
|
|
4369
|
+
true,
|
|
4370
|
+
false,
|
|
4371
|
+
`post_probe_${finalValidation.reason}`,
|
|
4372
|
+
validation.entryRuntime,
|
|
4373
|
+
),
|
|
4374
|
+
reason: "linux_bwrap_execution_contract_changed",
|
|
4375
|
+
guarantees: [],
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
if (validation.entryRuntime === "native-static-elf") {
|
|
4379
|
+
try {
|
|
4380
|
+
const finalEntryFormat = inspectLinuxStaticNativeElf(
|
|
4381
|
+
runtime,
|
|
4382
|
+
entryMount.fd,
|
|
4383
|
+
validation.contract.entryIdentity,
|
|
4384
|
+
);
|
|
4385
|
+
if (JSON.stringify(finalEntryFormat) !== JSON.stringify(entryFormat)) {
|
|
4386
|
+
throw new Error("native_entry_format_changed");
|
|
4387
|
+
}
|
|
4388
|
+
if (
|
|
4389
|
+
JSON.stringify(finalValidation.entryFormat) !==
|
|
4390
|
+
JSON.stringify(entryFormat)
|
|
4391
|
+
) {
|
|
4392
|
+
throw new Error("native_entry_path_format_changed");
|
|
4393
|
+
}
|
|
4394
|
+
} catch (error) {
|
|
4395
|
+
closeLinuxPinnedMounts(runtime, pinnedDescriptors);
|
|
4396
|
+
return createSandboxPlan({
|
|
4397
|
+
...base,
|
|
4398
|
+
backend: null,
|
|
4399
|
+
candidateBackend: LINUX_BWRAP_BACKEND,
|
|
4400
|
+
policyAttested: false,
|
|
4401
|
+
policyDigest,
|
|
4402
|
+
runtimeProbe: linuxBubblewrapProbe(
|
|
4403
|
+
true,
|
|
4404
|
+
false,
|
|
4405
|
+
`post_probe_${error.message || "native_entry_changed"}`,
|
|
4406
|
+
validation.entryRuntime,
|
|
4407
|
+
),
|
|
4408
|
+
reason: "linux_bwrap_execution_contract_changed",
|
|
4409
|
+
guarantees: [],
|
|
4410
|
+
});
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
const options = {
|
|
4414
|
+
...spawnOpts,
|
|
4415
|
+
cwd: "/",
|
|
4416
|
+
shell: false,
|
|
4417
|
+
detached: false,
|
|
4418
|
+
env: {
|
|
4419
|
+
PATH: "/usr/bin:/bin",
|
|
4420
|
+
LANG: "C",
|
|
4421
|
+
LC_ALL: "C",
|
|
4422
|
+
},
|
|
4423
|
+
stdio: Object.freeze(
|
|
4424
|
+
linuxStdioWithPinnedMounts(spawnOpts?.stdio, pinnedDescriptors),
|
|
4425
|
+
),
|
|
4426
|
+
};
|
|
4427
|
+
let pinsClosed = false;
|
|
4428
|
+
const cleanup = () => {
|
|
4429
|
+
if (pinsClosed) return;
|
|
4430
|
+
pinsClosed = true;
|
|
4431
|
+
closeLinuxPinnedMounts(runtime, pinnedDescriptors);
|
|
4432
|
+
};
|
|
890
4433
|
return createSandboxPlan({
|
|
891
4434
|
...base,
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
4435
|
+
applied: true,
|
|
4436
|
+
enforcement: LINUX_BWRAP_BACKEND,
|
|
4437
|
+
backend: LINUX_BWRAP_BACKEND,
|
|
4438
|
+
candidateBackend: null,
|
|
4439
|
+
policyAttested: true,
|
|
4440
|
+
policyDigest,
|
|
895
4441
|
runtimeProbe,
|
|
896
|
-
reason:
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
4442
|
+
reason: null,
|
|
4443
|
+
guarantees: [SANDBOX_BOUNDARIES.FILESYSTEM, SANDBOX_BOUNDARIES.NETWORK],
|
|
4444
|
+
command: LINUX_BWRAP_PATH,
|
|
4445
|
+
args: [...policyArgs, "--", ...targetArgs],
|
|
4446
|
+
options,
|
|
4447
|
+
cleanup,
|
|
900
4448
|
});
|
|
901
4449
|
}
|
|
902
4450
|
|
|
@@ -953,7 +4501,12 @@ export function applyLinuxSandbox(
|
|
|
953
4501
|
* requiredBoundaries?: string[]
|
|
954
4502
|
* }} profileOrRequest
|
|
955
4503
|
* @param {Object} runtimeOverrides
|
|
956
|
-
* @param {{
|
|
4504
|
+
* @param {{
|
|
4505
|
+
* profile?: string,
|
|
4506
|
+
* requiredBoundaries?: string[],
|
|
4507
|
+
* sync?: boolean,
|
|
4508
|
+
* executionContract?: Readonly<Object>|null
|
|
4509
|
+
* }|null} explicitRequest
|
|
957
4510
|
* @returns {ReturnType<typeof createSandboxPlan>}
|
|
958
4511
|
*/
|
|
959
4512
|
export function applySandbox(
|
|
@@ -1011,6 +4564,8 @@ export function applySandbox(
|
|
|
1011
4564
|
...(profiles[profileName] || profiles.default),
|
|
1012
4565
|
profileName: profiles[profileName] ? profileName : "default",
|
|
1013
4566
|
requiredBoundaries: sandboxRequest.requiredBoundaries,
|
|
4567
|
+
sync: sandboxRequest.sync,
|
|
4568
|
+
executionContract: sandboxRequest.executionContract,
|
|
1014
4569
|
};
|
|
1015
4570
|
|
|
1016
4571
|
// Dispatch to platform handler
|