chainlesschain 0.162.180 → 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 +7 -3
- package/scripts/build-windows-sandbox-helper.ps1 +163 -0
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-D_TIL54O.js → AIOps-DzZ5_xoj.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-eQwy9r-f.js → ActionButton-DeLXddIg.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D2eNuDgT.js → Analytics-rvDCmet1.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CFhwOFsf.js → AppLayout-Yt6QVDnp.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CJUMw6PB.js → Artifacts-BS112K5L.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CDB9V58o.js → Audit-0wJuQrNu.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-TBppYFjN.js → BackgroundAgents-B90PMgbn.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CYEOUpzJ.js → Backup-TkgMglQQ.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-C88fFT2u.js → BaseInput-CQVS9p1l.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-mkr1vC3N.js → Chat-YTjQHoC7.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-PR074hUN.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-CVU4xqti.js → Checkbox-5bwaOabq.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-rrNcByz3.js → Codegen-BFmNQ_Xe.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Cqu-zCvh.js → Col-BT_u4Dzp.js} +1 -1
- package/src/assets/web-panel/assets/{Community-RGRlSgXV.js → Community-CJP52u5b.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DBm6epOK.js → Compact-BpzhJE9i.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BgKw5iaW.js → Compliance-Cg7rfslp.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BqQcNO3Z.js → Cowork-JisxL6Yy.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DO5gTFNb.js → Cron-Bu7Ug_MC.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-B8hbJNuN.js → Crosschain-CvuqoLdj.js} +1 -1
- package/src/assets/web-panel/assets/{DID-B08bFJ2C.js → DID-C2u9G8Pu.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-xNdy_ifo.js → Dashboard-BFt5Yv3d.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-D-KIZgWL.js → Dropdown-DIaT9Wxq.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DKCvsaGo.js → EmailListRenderer-D_A601Y4.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-D_cfqama.js → FamilyGuardDashboard--IyQ8I8w.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-B-UqQD68.js → Federation-BihWHxuY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BWWvWuJW.js → FormItemContext-BB8dOXc_.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CPchx6N7.js → GenericCardRenderer-DQYhj1HH.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Vx4co9Ff.js → Git-B-2QZxGf.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-D4x0pp_m.js → Governance-CusSb-nG.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-T3KvwPYR.js → Inference-BAjmvPPL.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Ddv3KUv3.js → KnowledgeGraph-DoNYWKYq.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DfH-Dkae.js → Logs-B8fKWD-n.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DHW3PbCq.js → MarkdownRenderer-BlJm1J7E.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-tjgb0R3D.js → Marketplace-CyOscbSL.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DHAk5nYX.js → McpTools-Ex6Om5da.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-A2Yx7thM.js → Memory-CDcD8l8r.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BEmTVfN5.js → MobileBridge-xfBqoFDg.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-DuFHUgE4.js → MobileProjects-BwObMMA2.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BDSll0OZ.js → Mtc-CIWd36OV.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-CuBvluaS.js → MtcAudit-CPX0ouuA.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-BmXAA-Sb.js → Multisig-BDJsIUuW.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-ci2stZAe.js → NLProgramming-DEkbuD3N.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-D-1RpqnS.js → Notes-DJpHRhH1.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C0GyitIT.js → NotificationSettings-C1UZ-Qd5.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-U2bN-hj5.js → OrderTableRenderer-GlJtqLjT.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BNZgpPci.js → Organization-Bbhj0fac.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CDidKmUN.js → Overflow-1H04x3kx.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BnreBaXq.js → P2P-CDpPe3et.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DwJUxO9j.js → PdhVaultBrowser-MUX0Bpvw.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-BQGgJPH0.js → Permissions-A07-0Ci1.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-k_fSkCwL.js → PersonalDataHub-DhhDWxAZ.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-WGqKMqau.js → Pipeline-CyfbJfe-.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DtHj9Ymq.js → Privacy-CNFps3ED.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-B3wH2SFC.js → ProjectInit--T-u3Dr5.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-Cd0cgcA1.js → ProjectSettings-DJmrK2fC.js} +2 -2
- package/src/assets/web-panel/assets/Projects-Qx_MdX9k.js +1 -0
- package/src/assets/web-panel/assets/{Providers-DTpxEXur.js → Providers-DLzzbOZd.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-B2v6gR-C.js → QrScannerModal-CexOomKS.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-D3Nr9xXR.js → QuickAsk-QklGi19_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-C9inoMky.js → Recommend-Bb9kwSw8.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-DZuVNNN5.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-41KFG646.js → Reputation-Dr5cLP0N.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DqAaqqCv.js → Row-oCQHYv5t.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CMDpCDKc.js → RssFeed-IFDEykIR.js} +3 -3
- package/src/assets/web-panel/assets/{Search-B6mtegNL.js → Search-CspVIxDc.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BaePFVZ8.js → Security-Bm66o_DH.js} +3 -3
- package/src/assets/web-panel/assets/{Services-DeG3Qscl.js → Services-DvanNqXl.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-D_QP7F90.js → Skeleton-CHjEe9_O.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DLGyUgIA.js → Skills-Cb8jWjMX.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BN2pxW1Q.js → Sla-DxZm1491.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BWsZPd8B.js → SpeechSettings-DWxEEclC.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-VOZRPODW.js → SyncSettings-FIvOFw5p.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-CyuLnDFR.js +1 -0
- package/src/assets/web-panel/assets/{Templates-eJSob-q0.js → Templates-CaNjzV_t.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-Bdwk8Rwy.js → Tenant-DjlP6TmH.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-DwzGMniO.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-D5qkbPKT.js → TimelineRenderer-DoZ8y7y8.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DKgwn8hI.js → Tokens-390cg2TC.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-HlEHniGM.js → Trigger-CLrtCj5_.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CdPcsBpN.js → Trust-DpESfSPn.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-vAdXd-vG.js → UkeySign-CN6279O0.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-9hHBYFnH.js → VideoEditing-DPtAUlSe.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Dxh5d1yR.js → Wallet-nKOMV_1n.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-BsFmLHkp.js → WebAuthn-HuXlSOc4.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-Cn0OIwLn.js → WorkflowEditor-CUfFjARi.js} +1 -1
- package/src/assets/web-panel/assets/chat-6Eq0PcZJ.js +1 -0
- package/src/assets/web-panel/assets/{colors-Du9jzLN0.js → colors-BP_Sf6Iy.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DhEscfzw.js → compact-item-BufMKIh5.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CwzEfF3i.js → createContext-BBMXxWSW.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DgVKduH2.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DT_GSE6J.js → hasIn-fUK_tRO2.js} +1 -1
- package/src/assets/web-panel/assets/{index-tND8-TDU.js → index-2A5ph8b2.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8s5FxJY.js → index-3FuwBewR.js} +1 -1
- package/src/assets/web-panel/assets/{index-53z1YUxX.js → index-6GmBa4Kg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFD0cEna.js → index-B2vS66eI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQWxDOvM.js → index-B7Etk0Te.js} +1 -1
- package/src/assets/web-panel/assets/{index-VeHs6qH0.js → index-B9WYSn4m.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGvgnfl9.js → index-BHy6yNiF.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3llLkYM.js → index-BMLHv389.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dny_ZnAO.js → index-BWKoBU8R.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzjzrOli.js → index-Bkg4ZePE.js} +1 -1
- package/src/assets/web-panel/assets/{index-DN60VBn5.js → index-C44atv4k.js} +1 -1
- package/src/assets/web-panel/assets/{index-v5HLYCVa.js → index-CE2DdTP2.js} +1 -1
- package/src/assets/web-panel/assets/{index-BF-ZCVGE.js → index-CGjmILDm.js} +1 -1
- package/src/assets/web-panel/assets/{index-gFjYdstW.js → index-CW_G-MXw.js} +1 -1
- package/src/assets/web-panel/assets/{index-NA2oBLSB.js → index-CZ6Yt17L.js} +1 -1
- package/src/assets/web-panel/assets/{index-Mobw3PHs.js → index-CeASi4fr.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWL3SYN3.js → index-CgiECHu2.js} +1 -1
- package/src/assets/web-panel/assets/{index-1LoutPJd.js → index-Cp8vhyZy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnHhFnwt.js → index-CwSf5pnf.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6QDWRLk.js → index-D4_MuGkd.js} +1 -1
- package/src/assets/web-panel/assets/{index-D-QkW1fp.js → index-D9vU25ij.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4SN3by5.js → index-DASluSqY.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyEhUJcu.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-BrkJ7JPm.js → index-DPdUNk1i.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cnok8mhZ.js → index-DVsqvI4M.js} +1 -1
- package/src/assets/web-panel/assets/index-DhpvjpqI.js +1 -0
- package/src/assets/web-panel/assets/{index-CAVW0cWM.js → index-DmkMXwnR.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cc9aPkv-.js → index-DocdRcbd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnVVYKCE.js → index-Dyngbd29.js} +20 -20
- package/src/assets/web-panel/assets/{index-D8gt0j7r.js → index-GwdRUZ-x.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIYTjbv_.js → index-LvCBfTLo.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhXay17G.js → index-cpEli44X.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQWENaZf.js → index-fzaMtSJ8.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfRDyY1q.js → index-pum7HYBA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgSR2WMG.js → index-t8N1tSeX.js} +1 -1
- package/src/assets/web-panel/assets/{index-D5IytTND.js → index-uCbEN7cY.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtRwLhee.js → index-wKTLbKyc.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4kZoXNr.js → index-wV6iGdpY.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Cvt59c10.js → initDefaultProps-V5Fo95FM.js} +1 -1
- package/src/assets/web-panel/assets/{motion-D15pPEzl.js → motion-BYWU0hUT.js} +1 -1
- package/src/assets/web-panel/assets/{move-N55KeXJ7.js → move-Cr1M-J2h.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CyFO1bYP.js → mtc-parser-BysQKi2N.js} +1 -1
- package/src/assets/web-panel/assets/{omit-B5IXfxU2.js → omit-D1zHU8GC.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CpVqsulV.js → pickAttrs-CUArxtfi.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-ME36sMF3.js → placementArrow-ChoYEys0.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BXAIR1r-.js → responsiveObserve-BQRs3Mo7.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BkOX3-Dp.js → slide-B8zbsCkg.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-Cy4Aklpt.js → statusUtils-C2U8Gs6h.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-gUd2T206.js → styleChecker-Dd8MgL_V.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-CaVD4o99.js → useFlexGapSupport-hknqY5NH.js} +1 -1
- package/src/assets/web-panel/assets/{useFs--pFGl0KD.js → useFs-CLTAmcfw.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-D45iP86i.js → usePersonalDataHub-ByegHbiB.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BayoChH_.js → vnode-DqlAy5DP.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CpP38poi.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 +10 -1
- package/src/commands/background-session.js +2 -1
- package/src/commands/context.js +69 -2
- package/src/data/changelog.json +26 -0
- package/src/gateways/terminal/PtyManager.js +45 -4
- package/src/gateways/ws/background-agent-protocol.js +38 -0
- package/src/gateways/ws/message-dispatcher.js +1 -0
- package/src/gateways/ws/remote-session-protocol.js +65 -6
- package/src/gateways/ws/ws-server.js +18 -2
- package/src/harness/background-task-manager.js +70 -2
- package/src/harness/feature-flags.js +15 -8
- package/src/harness/mcp-client.js +618 -43
- package/src/harness/worktree-isolator.js +14 -3
- package/src/lazy-dispatch.js +30 -1
- package/src/lib/agent-authority.js +22 -0
- package/src/lib/agent-core.js +3 -0
- package/src/lib/agent-sandbox.js +161 -0
- package/src/lib/agent-schedule-store.js +45 -22
- package/src/lib/agent-team/team-runner.js +22 -0
- package/src/lib/artifact-store.js +35 -0
- package/src/lib/async-hook-supervisor.cjs +13 -2
- package/src/lib/audit-mtc.js +3 -2
- package/src/lib/background-agent-supervisor.js +104 -3
- package/src/lib/background-interaction-journal.js +407 -0
- package/src/lib/background-interaction-resolver.js +23 -34
- package/src/lib/background-session-transport.js +12 -2
- package/src/lib/capability-manifest.js +41 -3
- package/src/lib/capability-negotiation.js +1 -1
- package/src/lib/channels/channel-manager.js +33 -0
- package/src/lib/channels/telegram-channel.js +23 -5
- package/src/lib/channels/webhook-channel.js +38 -15
- package/src/lib/chrome-connector.js +169 -23
- package/src/lib/config-manager.js +23 -9
- package/src/lib/context-breakdown.js +31 -0
- package/src/lib/cowork-cron.js +274 -32
- package/src/lib/cross-chain-mtc.js +3 -2
- package/src/lib/durable-security-store.js +124 -0
- package/src/lib/event-runtime-host.js +67 -6
- package/src/lib/event-runtime-store.js +163 -0
- package/src/lib/goal-store.js +32 -12
- package/src/lib/headless-manifest.js +2 -1
- package/src/lib/hook-environment.cjs +58 -0
- package/src/lib/hook-manager.js +12 -5
- package/src/lib/hook-runner.cjs +122 -28
- package/src/lib/hook-runner.js +7 -2
- package/src/lib/hooks-v2-producers.js +36 -0
- package/src/lib/hooks-v2-runtime.js +329 -52
- package/src/lib/interaction-adapter.js +60 -19
- package/src/lib/interaction-binding.js +64 -0
- package/src/lib/json-schema-output.js +439 -14
- package/src/lib/json-schema-validate.js +514 -883
- package/src/lib/lan-pairing-tokens.js +115 -57
- package/src/lib/lsp/__tests__/lsp-client.test.js +32 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +25 -0
- package/src/lib/lsp/lsp-client.js +4 -0
- package/src/lib/lsp/lsp-manager.js +3 -0
- package/src/lib/lsp/lsp-server-registry.js +18 -0
- package/src/lib/mcp-client.js +1 -0
- package/src/lib/mcp-oauth.js +35 -21
- package/src/lib/packer/pkg-config-generator.js +3 -0
- package/src/lib/permission-decision.js +99 -0
- package/src/lib/plugin-monitor-supervisor.js +3 -0
- package/src/lib/plugin-runtime/bin.js +1148 -70
- package/src/lib/plugin-runtime/capabilities.js +50 -0
- package/src/lib/plugin-runtime/capability-consent.js +42 -35
- package/src/lib/plugin-runtime/hooks.js +36 -9
- package/src/lib/plugin-runtime/install.js +23 -7
- package/src/lib/plugin-runtime/lsp.js +4 -0
- package/src/lib/plugin-runtime/manifest.js +112 -8
- package/src/lib/plugin-runtime/mcp.js +30 -5
- package/src/lib/plugin-runtime/monitors.js +11 -0
- package/src/lib/plugin-runtime/plugin-options.js +94 -38
- package/src/lib/plugin-runtime/policy.js +29 -2
- package/src/lib/plugin-runtime/sandbox-policy.js +84 -0
- package/src/lib/plugin-runtime/scopes.js +27 -13
- package/src/lib/plugin-runtime/trust.js +36 -31
- package/src/lib/process-execution-broker/credential-agent.js +114 -16
- package/src/lib/process-execution-broker/credential-transport-worker.js +312 -0
- package/src/lib/process-execution-broker/credential-transport.js +378 -0
- package/src/lib/process-execution-broker/index.js +765 -49
- package/src/lib/process-execution-broker/platform-sandbox.js +4233 -138
- 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/project-mcp-trust.js +21 -26
- package/src/lib/protocol-replay.js +19 -12
- package/src/lib/repl-bang-memorize.js +67 -10
- package/src/lib/settings-hooks.cjs +13 -0
- package/src/lib/skill-loader.js +83 -10
- package/src/lib/slash-commands.js +27 -4
- package/src/lib/sync-credentials.js +5 -4
- package/src/lib/turn-binding.js +8 -6
- package/src/repl/agent-repl.js +117 -31
- package/src/repl/repl-turn-binding.js +5 -2
- package/src/runtime/agent-core.js +699 -70
- package/src/runtime/agent-runtime.js +13 -1
- package/src/runtime/coding-agent-contract-shared.cjs +3 -3
- package/src/runtime/headless-runner.js +105 -7
- package/src/runtime/headless-stream.js +209 -23
- package/src/runtime/mcp-config.js +29 -17
- package/src/runtime/policies/agent-policy.js +2 -0
- package/src/workers/background-agent-worker.js +128 -13
- package/src/assets/web-panel/assets/ChatBubbleRenderer-C7bofJuA.js +0 -1
- package/src/assets/web-panel/assets/Projects-BbgmyQnS.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-Bqk3EYSu.js +0 -4
- package/src/assets/web-panel/assets/Tasks-3bDAUz8K.js +0 -1
- package/src/assets/web-panel/assets/Terminal-CsMLKIoV.js +0 -3
- package/src/assets/web-panel/assets/chat-CJ19O6Dh.js +0 -1
- package/src/assets/web-panel/assets/devWarning-BBMFnsja.js +0 -1
- package/src/assets/web-panel/assets/index-Do8JMV4N.js +0 -1
- package/src/assets/web-panel/assets/index-UYAJ6iNM.js +0 -1
|
@@ -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
|
|
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
|
|
@@ -19,20 +23,268 @@ import os from "node:os";
|
|
|
19
23
|
import path from "node:path";
|
|
20
24
|
import * as fs from "node:fs";
|
|
21
25
|
import crypto from "node:crypto";
|
|
26
|
+
import { spawnSync as nativeSpawnSync } from "node:child_process";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Stable boundary identifiers shared by broker policies, platform plans, and
|
|
31
|
+
* audit records. A guarantee means that the named restriction is enforced by
|
|
32
|
+
* the selected backend; it does not merely describe a configured intent.
|
|
33
|
+
*/
|
|
34
|
+
export const SANDBOX_BOUNDARIES = Object.freeze({
|
|
35
|
+
FILESYSTEM: "filesystem",
|
|
36
|
+
NETWORK: "network",
|
|
37
|
+
PROCESS_EXEC: "process-exec",
|
|
38
|
+
PROCESS_TREE: "process-tree",
|
|
39
|
+
RESOURCE_LIMITS: "resource-limits",
|
|
40
|
+
PRIVILEGE_REDUCTION: "privilege-reduction",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {typeof SANDBOX_BOUNDARIES[keyof typeof SANDBOX_BOUNDARIES]} SandboxBoundary
|
|
45
|
+
*
|
|
46
|
+
* @typedef {Object} SandboxGuaranteePlan
|
|
47
|
+
* @property {1} contractVersion
|
|
48
|
+
* @property {boolean} applied
|
|
49
|
+
* @property {string} platform
|
|
50
|
+
* @property {string} profile
|
|
51
|
+
* @property {string} command
|
|
52
|
+
* @property {readonly string[]} args
|
|
53
|
+
* @property {Readonly<Object>} options
|
|
54
|
+
* @property {string|null} enforcement
|
|
55
|
+
* @property {string|null} backend
|
|
56
|
+
* @property {readonly SandboxBoundary[]} guarantees
|
|
57
|
+
* @property {string|null} reason
|
|
58
|
+
* @property {{required: boolean, mode: "none"|"sync"|"async"}} postSpawn
|
|
59
|
+
*/
|
|
60
|
+
|
|
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";
|
|
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
|
+
]);
|
|
144
|
+
const LINUX_BWRAP_PATH = "/usr/bin/bwrap";
|
|
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",
|
|
208
|
+
]);
|
|
22
209
|
const DEFAULT_RUNTIME = Object.freeze({
|
|
23
210
|
platform: os.platform(),
|
|
211
|
+
arch: process.arch,
|
|
24
212
|
fs,
|
|
25
213
|
tmpdir: () => os.tmpdir(),
|
|
26
214
|
homedir: () => os.homedir(),
|
|
27
215
|
randomBytes: (size) => crypto.randomBytes(size),
|
|
28
216
|
resolvePath: (value) => path.resolve(value),
|
|
29
217
|
joinPath: (...parts) => path.join(...parts),
|
|
218
|
+
// GLOBALROOT resolves the kernel-owned SystemRoot object rather than the
|
|
219
|
+
// caller-controlled WINDIR/SystemRoot environment variables.
|
|
220
|
+
windowsDir: () => WINDOWS_TRUSTED_SYSTEM_ROOT,
|
|
221
|
+
moduleDir: MODULE_DIR,
|
|
222
|
+
now: () => Date.now(),
|
|
223
|
+
sleepSync: (milliseconds) =>
|
|
224
|
+
Atomics.wait(WINDOWS_IDENTITY_WAIT, 0, 0, milliseconds),
|
|
225
|
+
spawnSync: nativeSpawnSync,
|
|
226
|
+
warn: (message) => process.emitWarning(message),
|
|
30
227
|
});
|
|
31
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
|
+
|
|
32
243
|
function resolveRuntime(overrides = {}) {
|
|
33
244
|
return { ...DEFAULT_RUNTIME, ...overrides };
|
|
34
245
|
}
|
|
35
246
|
|
|
247
|
+
function normalizeSandboxRequest(profileOrRequest, explicitRequest) {
|
|
248
|
+
const legacyRequest =
|
|
249
|
+
profileOrRequest &&
|
|
250
|
+
typeof profileOrRequest === "object" &&
|
|
251
|
+
!Array.isArray(profileOrRequest)
|
|
252
|
+
? profileOrRequest
|
|
253
|
+
: { profile: profileOrRequest };
|
|
254
|
+
const request =
|
|
255
|
+
explicitRequest &&
|
|
256
|
+
typeof explicitRequest === "object" &&
|
|
257
|
+
!Array.isArray(explicitRequest)
|
|
258
|
+
? explicitRequest
|
|
259
|
+
: legacyRequest;
|
|
260
|
+
const profile =
|
|
261
|
+
typeof request.profile === "string"
|
|
262
|
+
? request.profile
|
|
263
|
+
: typeof legacyRequest.profile === "string"
|
|
264
|
+
? legacyRequest.profile
|
|
265
|
+
: "default";
|
|
266
|
+
const requiredBoundaries = Array.isArray(request.requiredBoundaries)
|
|
267
|
+
? [
|
|
268
|
+
...new Set(
|
|
269
|
+
request.requiredBoundaries.filter(
|
|
270
|
+
(boundary) => typeof boundary === "string",
|
|
271
|
+
),
|
|
272
|
+
),
|
|
273
|
+
]
|
|
274
|
+
: [];
|
|
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
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
36
288
|
/**
|
|
37
289
|
* A native `spawn(..., { shell: true })` asks Node to execute one command
|
|
38
290
|
* string through the platform shell. Once a sandbox wrapper becomes the
|
|
@@ -45,7 +297,7 @@ function resolveRuntime(overrides = {}) {
|
|
|
45
297
|
* sandbox executable itself is always spawned directly.
|
|
46
298
|
*/
|
|
47
299
|
function normalizeWrappedInvocation(command, args, spawnOpts, platform) {
|
|
48
|
-
if (!spawnOpts?.shell
|
|
300
|
+
if (!spawnOpts?.shell) {
|
|
49
301
|
return {
|
|
50
302
|
command,
|
|
51
303
|
args: [...(args || [])],
|
|
@@ -53,6 +305,19 @@ function normalizeWrappedInvocation(command, args, spawnOpts, platform) {
|
|
|
53
305
|
};
|
|
54
306
|
}
|
|
55
307
|
|
|
308
|
+
if (platform === "win32") {
|
|
309
|
+
const shell =
|
|
310
|
+
typeof spawnOpts.shell === "string"
|
|
311
|
+
? spawnOpts.shell
|
|
312
|
+
: process.env.ComSpec || "C:\\Windows\\System32\\cmd.exe";
|
|
313
|
+
const shellCommand = [command, ...(args || [])].map(String).join(" ");
|
|
314
|
+
return {
|
|
315
|
+
command: shell,
|
|
316
|
+
args: ["/d", "/s", "/c", shellCommand],
|
|
317
|
+
options: { ...spawnOpts, shell: false },
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
56
321
|
const shell =
|
|
57
322
|
typeof spawnOpts.shell === "string" ? spawnOpts.shell : "/bin/sh";
|
|
58
323
|
const shellCommand = [command, ...(args || [])].map(String).join(" ");
|
|
@@ -82,6 +347,8 @@ function normalizeWrappedInvocation(command, args, spawnOpts, platform) {
|
|
|
82
347
|
* args: string[],
|
|
83
348
|
* options: Object,
|
|
84
349
|
* enforcement: string|null,
|
|
350
|
+
* backend: string|null,
|
|
351
|
+
* guarantees: readonly SandboxBoundary[],
|
|
85
352
|
* reason: string|null,
|
|
86
353
|
* cleanup?: Function,
|
|
87
354
|
* postSpawn: {required: boolean, mode: "none"|"sync"|"async"}
|
|
@@ -97,10 +364,15 @@ function createSandboxPlan(input) {
|
|
|
97
364
|
contractVersion: 1,
|
|
98
365
|
applied: false,
|
|
99
366
|
enforcement: null,
|
|
367
|
+
backend: null,
|
|
100
368
|
reason: null,
|
|
101
369
|
...input,
|
|
102
370
|
args: Object.freeze([...(input.args || [])]),
|
|
103
371
|
options: Object.freeze({ ...(input.options || {}) }),
|
|
372
|
+
guarantees: Object.freeze([...(input.guarantees || [])]),
|
|
373
|
+
runtimeProbe: input.runtimeProbe
|
|
374
|
+
? Object.freeze({ ...input.runtimeProbe })
|
|
375
|
+
: null,
|
|
104
376
|
postSpawn,
|
|
105
377
|
});
|
|
106
378
|
}
|
|
@@ -132,6 +404,10 @@ export function generateMacSeatbeltProfile(opts = {}, runtimeOverrides = {}) {
|
|
|
132
404
|
const lines = [
|
|
133
405
|
"(version 1)",
|
|
134
406
|
"(deny default)",
|
|
407
|
+
// Apple's system baseline supplies the loader/device primitives required
|
|
408
|
+
// for ordinary command startup. Our explicit rules below still own
|
|
409
|
+
// process, filesystem write, and network policy.
|
|
410
|
+
'(import "system.sb")',
|
|
135
411
|
// Basic system operations always allowed
|
|
136
412
|
"(allow signal (target self))",
|
|
137
413
|
'(allow process-exec (literal "/usr/bin/env"))',
|
|
@@ -148,6 +424,10 @@ export function generateMacSeatbeltProfile(opts = {}, runtimeOverrides = {}) {
|
|
|
148
424
|
|
|
149
425
|
if (allowNetwork) {
|
|
150
426
|
lines.push("(allow network*)");
|
|
427
|
+
} else {
|
|
428
|
+
// Keep the imported system baseline from widening the caller's explicit
|
|
429
|
+
// no-network policy.
|
|
430
|
+
lines.push("(deny network*)");
|
|
151
431
|
}
|
|
152
432
|
|
|
153
433
|
// Allow read/write to specific paths
|
|
@@ -162,9 +442,17 @@ export function generateMacSeatbeltProfile(opts = {}, runtimeOverrides = {}) {
|
|
|
162
442
|
}
|
|
163
443
|
|
|
164
444
|
// Always allow read access to system paths needed for basic execution
|
|
445
|
+
lines.push('(allow file-read* (subpath "/bin"))');
|
|
446
|
+
lines.push('(allow file-read* (subpath "/usr/bin"))');
|
|
165
447
|
lines.push('(allow file-read* (subpath "/usr/lib"))');
|
|
448
|
+
lines.push('(allow file-read* (subpath "/usr/libexec"))');
|
|
166
449
|
lines.push('(allow file-read* (subpath "/System/Library"))');
|
|
450
|
+
lines.push('(allow file-read* (subpath "/Library/Frameworks"))');
|
|
167
451
|
lines.push('(allow file-read* (subpath "/usr/local/lib"))');
|
|
452
|
+
lines.push(
|
|
453
|
+
'(allow file-read* file-write* (literal "/dev/null") (literal "/dev/stdin") (literal "/dev/stdout") (literal "/dev/stderr"))',
|
|
454
|
+
);
|
|
455
|
+
lines.push('(allow file-read* (literal "/dev/urandom"))');
|
|
168
456
|
lines.push('(allow file-read* (literal "/etc/passwd"))');
|
|
169
457
|
|
|
170
458
|
return lines.join("\n");
|
|
@@ -199,6 +487,7 @@ export function applyMacSandbox(
|
|
|
199
487
|
const base = {
|
|
200
488
|
platform: runtime.platform,
|
|
201
489
|
profile: sandboxOpts.profileName || "default",
|
|
490
|
+
backend: "macos-seatbelt",
|
|
202
491
|
command,
|
|
203
492
|
args,
|
|
204
493
|
options: { ...(spawnOpts || {}) },
|
|
@@ -250,6 +539,11 @@ export function applyMacSandbox(
|
|
|
250
539
|
...base,
|
|
251
540
|
applied: true,
|
|
252
541
|
enforcement: "macos-seatbelt",
|
|
542
|
+
guarantees: [
|
|
543
|
+
SANDBOX_BOUNDARIES.FILESYSTEM,
|
|
544
|
+
...(sandboxOpts.allowNetwork ? [] : [SANDBOX_BOUNDARIES.NETWORK]),
|
|
545
|
+
...(sandboxOpts.allowExec ? [] : [SANDBOX_BOUNDARIES.PROCESS_EXEC]),
|
|
546
|
+
],
|
|
253
547
|
command: sandboxExecutable,
|
|
254
548
|
args: newArgs,
|
|
255
549
|
options: invocation.options,
|
|
@@ -264,173 +558,3971 @@ export function applyMacSandbox(
|
|
|
264
558
|
}
|
|
265
559
|
|
|
266
560
|
// ---------------------------------------------------------------------------
|
|
267
|
-
// Windows Job Object
|
|
561
|
+
// Windows Job Object + Restricted Token
|
|
268
562
|
// ---------------------------------------------------------------------------
|
|
269
563
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
* @param {string[]} args
|
|
276
|
-
* @param {Object} spawnOpts
|
|
277
|
-
* @param {Object} sandboxOpts
|
|
278
|
-
*/
|
|
279
|
-
export function applyWindowsSandbox(
|
|
280
|
-
command,
|
|
281
|
-
args,
|
|
282
|
-
spawnOpts,
|
|
283
|
-
sandboxOpts = {},
|
|
284
|
-
runtimeOverrides = {},
|
|
564
|
+
function cleanupWindowsTemporaryPath(
|
|
565
|
+
runtime,
|
|
566
|
+
targetPath,
|
|
567
|
+
attempts = 100,
|
|
568
|
+
delayMs = 10,
|
|
285
569
|
) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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));
|
|
293
687
|
};
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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;
|
|
299
759
|
}
|
|
760
|
+
}
|
|
300
761
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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();
|
|
308
813
|
});
|
|
309
814
|
}
|
|
310
815
|
|
|
311
816
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
* @param {ChildProcess} proc - The spawned child process
|
|
316
|
-
* @param {Object} sandboxResult - Result from applyWindowsSandbox
|
|
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.
|
|
317
820
|
*/
|
|
318
|
-
export function
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
) {
|
|
325
840
|
return;
|
|
326
841
|
}
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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);
|
|
856
|
+
}
|
|
857
|
+
}, idleTtlMs);
|
|
858
|
+
entry.idleTimer.unref?.();
|
|
332
859
|
}
|
|
333
860
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
}
|
|
337
880
|
|
|
338
881
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* @param {Object} spawnOpts
|
|
344
|
-
* @param {Object} sandboxOpts
|
|
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[]).
|
|
345
886
|
*/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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`,
|
|
359
900
|
);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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" };
|
|
928
|
+
}
|
|
929
|
+
if (error?.code === "EEXIST") {
|
|
930
|
+
return { reason: "windows_native_adapter_random_path_collision" };
|
|
931
|
+
}
|
|
932
|
+
return { reason: "windows_native_adapter_materialize_failed" };
|
|
933
|
+
}
|
|
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
|
+
},
|
|
366
947
|
};
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function materializeWindowsAdapterInvocation(runtime, helperArgs) {
|
|
951
|
+
const payloadPath = runtime.joinPath(
|
|
952
|
+
runtime.tmpdir(),
|
|
953
|
+
`chainless-win-sandbox-invocation-${runtime
|
|
954
|
+
.randomBytes(24)
|
|
955
|
+
.toString("hex")}.json`,
|
|
956
|
+
);
|
|
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",
|
|
371
982
|
});
|
|
983
|
+
} catch (error) {
|
|
984
|
+
if (error?.code === "EEXIST") {
|
|
985
|
+
return { reason: "windows_adapter_invocation_random_path_collision" };
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
reason: cleanupOrTrackWindowsTemporaryPath(runtime, payloadPath)
|
|
989
|
+
? "windows_adapter_invocation_materialize_failed"
|
|
990
|
+
: "windows_adapter_invocation_cleanup_unverified",
|
|
991
|
+
};
|
|
372
992
|
}
|
|
993
|
+
return {
|
|
994
|
+
payloadPath,
|
|
995
|
+
payloadDigest: sha256(content),
|
|
996
|
+
cleanup: () => cleanupOrTrackWindowsTemporaryPath(runtime, payloadPath),
|
|
997
|
+
};
|
|
998
|
+
}
|
|
373
999
|
|
|
374
|
-
|
|
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
|
+
}
|
|
375
1040
|
|
|
376
|
-
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
+
}
|
|
381
1069
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
}
|
|
387
1094
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
1095
|
+
|
|
1096
|
+
const inboxHost = runtime.joinPath(
|
|
1097
|
+
windowsDirectory,
|
|
1098
|
+
"System32",
|
|
1099
|
+
"WindowsPowerShell",
|
|
1100
|
+
"v1.0",
|
|
1101
|
+
"powershell.exe",
|
|
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
|
+
};
|
|
393
1118
|
}
|
|
394
1119
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
1120
|
+
let attestation;
|
|
1121
|
+
try {
|
|
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
|
+
}
|
|
1129
|
+
} catch {
|
|
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,
|
|
401
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
|
+
}
|
|
402
1213
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
command: "/usr/bin/prlimit",
|
|
408
|
-
args: [...prlimitParts, "--", invocation.command, ...invocation.args],
|
|
409
|
-
options,
|
|
410
|
-
});
|
|
1214
|
+
windowsAdapterCacheEntries.add(entry);
|
|
1215
|
+
windowsAdapterCache = entry;
|
|
1216
|
+
registerWindowsAdapterExitCleanup();
|
|
1217
|
+
return { entry };
|
|
411
1218
|
}
|
|
412
1219
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
+
}
|
|
416
1235
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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);
|
|
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
|
+
|
|
1298
|
+
return {
|
|
1299
|
+
ensureExecutable: ensureEntry,
|
|
1300
|
+
createInvocation,
|
|
1301
|
+
spawnSync: (args, options) => {
|
|
1302
|
+
const invocation = createInvocation(args);
|
|
1303
|
+
let result;
|
|
1304
|
+
let failure;
|
|
1305
|
+
try {
|
|
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;
|
|
1321
|
+
}
|
|
1322
|
+
if (failure) throw failure;
|
|
1323
|
+
return result;
|
|
1324
|
+
},
|
|
1325
|
+
release: () => {
|
|
1326
|
+
if (released) return;
|
|
1327
|
+
released = true;
|
|
1328
|
+
releaseWindowsAdapterLease(lease);
|
|
1329
|
+
},
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function waitForWindowsTargetIdentity(
|
|
1334
|
+
proc,
|
|
1335
|
+
identityPath,
|
|
1336
|
+
runtime,
|
|
1337
|
+
expectedAppContainer = null,
|
|
1338
|
+
timeoutMs = 30_000,
|
|
1339
|
+
) {
|
|
1340
|
+
const deadline = runtime.now() + timeoutMs;
|
|
1341
|
+
let lastError;
|
|
1342
|
+
while (runtime.now() < deadline) {
|
|
1343
|
+
try {
|
|
1344
|
+
const identity = JSON.parse(
|
|
1345
|
+
runtime.fs.readFileSync(identityPath, "utf8"),
|
|
1346
|
+
);
|
|
1347
|
+
if (identity?.error) {
|
|
1348
|
+
throw new Error(
|
|
1349
|
+
`Windows sandbox helper rejected the target: ${identity.error}`,
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
const targetPid = Number(identity?.targetPid);
|
|
1353
|
+
const helperPid = Number(identity?.helperPid);
|
|
1354
|
+
if (!Number.isSafeInteger(targetPid) || targetPid <= 0) {
|
|
1355
|
+
throw new Error("Windows sandbox identity omitted a valid target PID");
|
|
1356
|
+
}
|
|
1357
|
+
if (
|
|
1358
|
+
Number.isSafeInteger(helperPid) &&
|
|
1359
|
+
helperPid > 0 &&
|
|
1360
|
+
helperPid !== proc.pid
|
|
1361
|
+
) {
|
|
1362
|
+
throw new Error(
|
|
1363
|
+
`Windows sandbox identity named unexpected helper PID ${helperPid}`,
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
if (targetPid === proc.pid) {
|
|
1367
|
+
throw new Error(
|
|
1368
|
+
"Windows sandbox target PID unexpectedly equals its helper PID",
|
|
1369
|
+
);
|
|
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
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const wrapperPid = proc.pid;
|
|
1392
|
+
proc.sandboxWrapperPid = wrapperPid;
|
|
1393
|
+
proc.sandboxTargetPid = targetPid;
|
|
1394
|
+
if (expectedAppContainer) {
|
|
1395
|
+
proc.sandboxAppContainerProfile =
|
|
1396
|
+
expectedAppContainer.appContainerProfileName;
|
|
1397
|
+
proc.sandboxAppContainerSid = expectedAppContainer.appContainerSid;
|
|
1398
|
+
}
|
|
1399
|
+
// ChildProcess.pid is a writable data property. The native ChildProcess
|
|
1400
|
+
// handle still points at the helper, so kill()/ref()/unref() retain Job
|
|
1401
|
+
// lifetime semantics while callers observe and persist the real target.
|
|
1402
|
+
proc.pid = targetPid;
|
|
1403
|
+
return {
|
|
1404
|
+
targetPid,
|
|
1405
|
+
wrapperPid,
|
|
1406
|
+
...(expectedAppContainer
|
|
1407
|
+
? {
|
|
1408
|
+
appContainerProfileName:
|
|
1409
|
+
expectedAppContainer.appContainerProfileName,
|
|
1410
|
+
appContainerSid: expectedAppContainer.appContainerSid,
|
|
1411
|
+
capabilityCount: 0,
|
|
1412
|
+
}
|
|
1413
|
+
: {}),
|
|
1414
|
+
};
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
lastError = error;
|
|
1417
|
+
if (
|
|
1418
|
+
error?.code !== "ENOENT" &&
|
|
1419
|
+
!(error instanceof SyntaxError) &&
|
|
1420
|
+
!String(error?.message || "").includes("omitted a valid target PID")
|
|
1421
|
+
) {
|
|
1422
|
+
throw error;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
runtime.sleepSync(10);
|
|
1426
|
+
}
|
|
1427
|
+
throw new Error(
|
|
1428
|
+
`Timed out waiting for Windows sandbox target identity${
|
|
1429
|
+
lastError?.message ? `: ${lastError.message}` : ""
|
|
1430
|
+
}`,
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
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,
|
|
2019
|
+
command,
|
|
2020
|
+
args,
|
|
2021
|
+
options: spawnOpts,
|
|
2022
|
+
};
|
|
2023
|
+
let appContainerPolicyDigest = null;
|
|
2024
|
+
const unavailablePlan = (reason, extra = {}) =>
|
|
2025
|
+
createSandboxPlan({
|
|
2026
|
+
...base,
|
|
2027
|
+
...(requiresAppContainer
|
|
2028
|
+
? {
|
|
2029
|
+
backend: null,
|
|
2030
|
+
candidateBackend: WINDOWS_APPCONTAINER_BACKEND,
|
|
2031
|
+
policyAttested: false,
|
|
2032
|
+
policyDigest: appContainerPolicyDigest,
|
|
2033
|
+
}
|
|
2034
|
+
: {}),
|
|
2035
|
+
reason,
|
|
2036
|
+
...extra,
|
|
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");
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// The hosted Windows restricted-token path cannot reliably preserve file
|
|
2068
|
+
// descriptor backed stdout/stderr when its helper is itself detached. Mark
|
|
2069
|
+
// that combination unavailable before launch: strict/required callers fail
|
|
2070
|
+
// closed, while ordinary trusted control-plane callers retain the broker's
|
|
2071
|
+
// explicit audited fallback instead of receiving a phantom target PID.
|
|
2072
|
+
if (
|
|
2073
|
+
spawnOpts?.detached === true &&
|
|
2074
|
+
Array.isArray(spawnOpts?.stdio) &&
|
|
2075
|
+
spawnOpts.stdio.slice(0, 3).some((entry) => Number.isInteger(entry))
|
|
2076
|
+
) {
|
|
2077
|
+
return unavailablePlan("windows_detached_file_stdio_unsupported");
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
const nodeIpcFd = Array.isArray(spawnOpts?.stdio)
|
|
2081
|
+
? spawnOpts.stdio.findIndex((entry) => entry === "ipc")
|
|
2082
|
+
: -1;
|
|
2083
|
+
if (
|
|
2084
|
+
Array.isArray(spawnOpts?.stdio) &&
|
|
2085
|
+
spawnOpts.stdio
|
|
2086
|
+
.slice(3)
|
|
2087
|
+
.some(
|
|
2088
|
+
(entry, offset) =>
|
|
2089
|
+
offset + 3 !== nodeIpcFd &&
|
|
2090
|
+
entry !== "ignore" &&
|
|
2091
|
+
entry !== null &&
|
|
2092
|
+
entry !== undefined,
|
|
2093
|
+
)
|
|
2094
|
+
) {
|
|
2095
|
+
return unavailablePlan("windows_extra_descriptor_unsupported");
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
const invocation = normalizeWrappedInvocation(
|
|
2099
|
+
command,
|
|
2100
|
+
args,
|
|
2101
|
+
spawnOpts,
|
|
2102
|
+
runtime.platform,
|
|
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
|
+
}
|
|
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
|
+
);
|
|
2177
|
+
const limits = sandboxOpts.limits || {};
|
|
2178
|
+
const targetWorkingDirectory = runtime.resolvePath(
|
|
2179
|
+
invocation.options.cwd || process.cwd(),
|
|
2180
|
+
);
|
|
2181
|
+
const identityPath =
|
|
2182
|
+
spawnOpts?.detached === true ||
|
|
2183
|
+
(requiresAppContainer && sandboxOpts.sync !== true)
|
|
2184
|
+
? runtime.joinPath(
|
|
2185
|
+
runtime.tmpdir(),
|
|
2186
|
+
`chainless-win-sandbox-identity-${runtime
|
|
2187
|
+
.randomBytes(24)
|
|
2188
|
+
.toString("hex")}.json`,
|
|
2189
|
+
)
|
|
2190
|
+
: null;
|
|
2191
|
+
const launchSpec = {
|
|
2192
|
+
cpuSeconds: Number(limits.cpu || 0),
|
|
2193
|
+
processMemoryBytes: Number(
|
|
2194
|
+
limits.as || (profile === "strict" ? 256 * 1024 * 1024 : 0),
|
|
2195
|
+
),
|
|
2196
|
+
activeProcessLimit: profile === "strict" ? 16 : 64,
|
|
2197
|
+
allowReparsePaths: loaderMode === "managed-executable",
|
|
2198
|
+
disableAdministratorSids:
|
|
2199
|
+
profile === "strict" ||
|
|
2200
|
+
requiresAppContainer ||
|
|
2201
|
+
Boolean(entrySnapshot.locks),
|
|
2202
|
+
command: invocation.command,
|
|
2203
|
+
args: invocation.args,
|
|
2204
|
+
nodeIpcFd,
|
|
2205
|
+
detached: invocation.options.detached === true,
|
|
2206
|
+
windowsHide: invocation.options.windowsHide === true,
|
|
2207
|
+
workingDirectory: targetWorkingDirectory,
|
|
2208
|
+
environment: targetEnvironment,
|
|
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
|
+
}
|
|
2228
|
+
if (identityPath) launchSpec.identityPath = identityPath;
|
|
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,
|
|
2240
|
+
);
|
|
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"),
|
|
3580
|
+
};
|
|
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
|
+
}
|
|
3598
|
+
}
|
|
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
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
throw error;
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
|
|
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;
|
|
3685
|
+
}
|
|
3686
|
+
const after = runtime.fs.statSync(source);
|
|
3687
|
+
if (
|
|
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)
|
|
3692
|
+
) {
|
|
3693
|
+
return null;
|
|
3694
|
+
}
|
|
3695
|
+
return source;
|
|
3696
|
+
} catch {
|
|
3697
|
+
return null;
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
function attestLinuxBubblewrapBinary(runtime) {
|
|
3702
|
+
return (
|
|
3703
|
+
attestLinuxRootOwnedFile(runtime, LINUX_BWRAP_PATH, {
|
|
3704
|
+
executable: true,
|
|
3705
|
+
}) === LINUX_BWRAP_PATH
|
|
3706
|
+
);
|
|
3707
|
+
}
|
|
3708
|
+
|
|
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" };
|
|
3725
|
+
}
|
|
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
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
return { ok: true, reason: null };
|
|
3744
|
+
}
|
|
3745
|
+
|
|
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
|
+
}
|
|
3759
|
+
|
|
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;
|
|
3781
|
+
try {
|
|
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]);
|
|
3795
|
+
}
|
|
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]);
|
|
3970
|
+
}
|
|
3971
|
+
args.push("--chdir", "/opt/chainless/plugin");
|
|
3972
|
+
return args;
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
function probeLinuxBubblewrapPolicy(
|
|
3976
|
+
runtime,
|
|
3977
|
+
policyArgs,
|
|
3978
|
+
pinnedDescriptors,
|
|
3979
|
+
targetRuntime,
|
|
3980
|
+
) {
|
|
3981
|
+
let result;
|
|
3982
|
+
try {
|
|
3983
|
+
result = runtime.spawnSync(
|
|
3984
|
+
LINUX_BWRAP_PATH,
|
|
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
|
+
],
|
|
3994
|
+
{
|
|
3995
|
+
cwd: "/",
|
|
3996
|
+
shell: false,
|
|
3997
|
+
encoding: "utf8",
|
|
3998
|
+
stdio: linuxStdioWithPinnedMounts(null, pinnedDescriptors, {
|
|
3999
|
+
probe: true,
|
|
4000
|
+
}),
|
|
4001
|
+
timeout: 15_000,
|
|
4002
|
+
env: {
|
|
4003
|
+
PATH: "/usr/bin:/bin",
|
|
4004
|
+
LANG: "C",
|
|
4005
|
+
LC_ALL: "C",
|
|
4006
|
+
},
|
|
4007
|
+
},
|
|
4008
|
+
);
|
|
4009
|
+
} catch {
|
|
4010
|
+
return linuxBubblewrapProbe(
|
|
4011
|
+
true,
|
|
4012
|
+
false,
|
|
4013
|
+
"probe_spawn_failed",
|
|
4014
|
+
targetRuntime,
|
|
4015
|
+
);
|
|
4016
|
+
}
|
|
4017
|
+
if (result?.error || result?.status !== 0) {
|
|
4018
|
+
return linuxBubblewrapProbe(
|
|
4019
|
+
true,
|
|
4020
|
+
false,
|
|
4021
|
+
result?.error ? "probe_spawn_failed" : "probe_failed",
|
|
4022
|
+
targetRuntime,
|
|
4023
|
+
);
|
|
4024
|
+
}
|
|
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);
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
/**
|
|
4037
|
+
* Apply Linux resource limits through the util-linux `prlimit` wrapper.
|
|
4038
|
+
*
|
|
4039
|
+
* @param {string} command
|
|
4040
|
+
* @param {string[]} args
|
|
4041
|
+
* @param {Object} spawnOpts
|
|
4042
|
+
* @param {Object} sandboxOpts
|
|
4043
|
+
*/
|
|
4044
|
+
export function applyLinuxSandbox(
|
|
4045
|
+
command,
|
|
4046
|
+
args,
|
|
4047
|
+
spawnOpts,
|
|
4048
|
+
sandboxOpts = {},
|
|
4049
|
+
runtimeOverrides = {},
|
|
4050
|
+
) {
|
|
4051
|
+
const runtime = resolveRuntime(runtimeOverrides);
|
|
4052
|
+
const invocation = normalizeWrappedInvocation(
|
|
4053
|
+
command,
|
|
4054
|
+
args,
|
|
4055
|
+
spawnOpts,
|
|
4056
|
+
runtime.platform,
|
|
4057
|
+
);
|
|
4058
|
+
const base = {
|
|
4059
|
+
platform: runtime.platform,
|
|
4060
|
+
profile: sandboxOpts.profileName || "default",
|
|
4061
|
+
backend: "linux-prlimit",
|
|
4062
|
+
command,
|
|
4063
|
+
args,
|
|
4064
|
+
options: { ...(spawnOpts || {}) },
|
|
4065
|
+
};
|
|
4066
|
+
if (runtime.platform !== "linux") {
|
|
4067
|
+
return createSandboxPlan({
|
|
4068
|
+
...base,
|
|
4069
|
+
reason: "platform_mismatch",
|
|
4070
|
+
});
|
|
4071
|
+
}
|
|
4072
|
+
|
|
4073
|
+
const { limits = {} } = sandboxOpts;
|
|
4074
|
+
const requiredBoundaries = Array.isArray(sandboxOpts.requiredBoundaries)
|
|
4075
|
+
? sandboxOpts.requiredBoundaries
|
|
4076
|
+
: [];
|
|
4077
|
+
const requiresStrongLinuxBoundary = requiredBoundaries.some(
|
|
4078
|
+
(boundary) =>
|
|
4079
|
+
boundary === SANDBOX_BOUNDARIES.FILESYSTEM ||
|
|
4080
|
+
boundary === SANDBOX_BOUNDARIES.NETWORK,
|
|
4081
|
+
);
|
|
4082
|
+
|
|
4083
|
+
if (requiresStrongLinuxBoundary) {
|
|
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
|
+
};
|
|
4433
|
+
return createSandboxPlan({
|
|
4434
|
+
...base,
|
|
4435
|
+
applied: true,
|
|
4436
|
+
enforcement: LINUX_BWRAP_BACKEND,
|
|
4437
|
+
backend: LINUX_BWRAP_BACKEND,
|
|
4438
|
+
candidateBackend: null,
|
|
4439
|
+
policyAttested: true,
|
|
4440
|
+
policyDigest,
|
|
4441
|
+
runtimeProbe,
|
|
4442
|
+
reason: null,
|
|
4443
|
+
guarantees: [SANDBOX_BOUNDARIES.FILESYSTEM, SANDBOX_BOUNDARIES.NETWORK],
|
|
4444
|
+
command: LINUX_BWRAP_PATH,
|
|
4445
|
+
args: [...policyArgs, "--", ...targetArgs],
|
|
4446
|
+
options,
|
|
4447
|
+
cleanup,
|
|
4448
|
+
});
|
|
4449
|
+
}
|
|
4450
|
+
|
|
4451
|
+
const prlimitParts = [];
|
|
4452
|
+
if (limits.cpu) prlimitParts.push(`--cpu=${limits.cpu}`);
|
|
4453
|
+
if (limits.as) prlimitParts.push(`--as=${limits.as}`);
|
|
4454
|
+
if (limits.nofile) prlimitParts.push(`--nofile=${limits.nofile}`);
|
|
4455
|
+
if (limits.nproc) prlimitParts.push(`--nproc=${limits.nproc}`);
|
|
4456
|
+
|
|
4457
|
+
if (prlimitParts.length === 0) {
|
|
4458
|
+
return createSandboxPlan({
|
|
4459
|
+
...base,
|
|
4460
|
+
reason: "linux_resource_limits_not_configured",
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
4463
|
+
if (!runtime.fs.existsSync("/usr/bin/prlimit")) {
|
|
4464
|
+
return createSandboxPlan({
|
|
4465
|
+
...base,
|
|
4466
|
+
reason: "linux_prlimit_unavailable",
|
|
4467
|
+
});
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
const options = {
|
|
4471
|
+
...invocation.options,
|
|
4472
|
+
env: {
|
|
4473
|
+
...(invocation.options.env || process.env),
|
|
4474
|
+
CHAINLESS_SANDBOXED: "1",
|
|
4475
|
+
},
|
|
4476
|
+
};
|
|
4477
|
+
|
|
4478
|
+
return createSandboxPlan({
|
|
4479
|
+
...base,
|
|
4480
|
+
applied: true,
|
|
4481
|
+
enforcement: "linux-prlimit",
|
|
4482
|
+
guarantees: [SANDBOX_BOUNDARIES.RESOURCE_LIMITS],
|
|
4483
|
+
command: "/usr/bin/prlimit",
|
|
4484
|
+
args: [...prlimitParts, "--", invocation.command, ...invocation.args],
|
|
4485
|
+
options,
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
// ---------------------------------------------------------------------------
|
|
4490
|
+
// Unified sandbox apply
|
|
4491
|
+
// ---------------------------------------------------------------------------
|
|
4492
|
+
|
|
4493
|
+
/**
|
|
4494
|
+
* Apply platform-appropriate sandbox for a child process.
|
|
4495
|
+
*
|
|
4496
|
+
* @param {string} command
|
|
4497
|
+
* @param {string[]} args
|
|
4498
|
+
* @param {Object} spawnOpts - Original spawn options
|
|
4499
|
+
* @param {"default"|"strict"|"network-only"|{
|
|
4500
|
+
* profile?: "default"|"strict"|"network-only",
|
|
4501
|
+
* requiredBoundaries?: string[]
|
|
4502
|
+
* }} profileOrRequest
|
|
4503
|
+
* @param {Object} runtimeOverrides
|
|
4504
|
+
* @param {{
|
|
4505
|
+
* profile?: string,
|
|
4506
|
+
* requiredBoundaries?: string[],
|
|
4507
|
+
* sync?: boolean,
|
|
4508
|
+
* executionContract?: Readonly<Object>|null
|
|
4509
|
+
* }|null} explicitRequest
|
|
4510
|
+
* @returns {ReturnType<typeof createSandboxPlan>}
|
|
4511
|
+
*/
|
|
4512
|
+
export function applySandbox(
|
|
4513
|
+
command,
|
|
4514
|
+
args,
|
|
4515
|
+
spawnOpts,
|
|
4516
|
+
profileOrRequest = "default",
|
|
4517
|
+
runtimeOverrides = {},
|
|
4518
|
+
explicitRequest = null,
|
|
4519
|
+
) {
|
|
4520
|
+
const runtime = resolveRuntime(runtimeOverrides);
|
|
4521
|
+
const sandboxRequest = normalizeSandboxRequest(
|
|
4522
|
+
profileOrRequest,
|
|
4523
|
+
explicitRequest,
|
|
4524
|
+
);
|
|
4525
|
+
const profileName = sandboxRequest.profile;
|
|
434
4526
|
const profiles = {
|
|
435
4527
|
default: {
|
|
436
4528
|
allowNetwork: false,
|
|
@@ -471,6 +4563,9 @@ export function applySandbox(
|
|
|
471
4563
|
const profile = {
|
|
472
4564
|
...(profiles[profileName] || profiles.default),
|
|
473
4565
|
profileName: profiles[profileName] ? profileName : "default",
|
|
4566
|
+
requiredBoundaries: sandboxRequest.requiredBoundaries,
|
|
4567
|
+
sync: sandboxRequest.sync,
|
|
4568
|
+
executionContract: sandboxRequest.executionContract,
|
|
474
4569
|
};
|
|
475
4570
|
|
|
476
4571
|
// Dispatch to platform handler
|