chainlesschain 0.166.77 → 0.166.78
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/README.md +16 -7
- package/completions/_cc +1 -1
- package/completions/cc.bash +1 -1
- package/completions/cc.fish +1 -1
- package/completions/cc.ps1 +1 -1
- package/package.json +1 -1
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +10 -0
- package/src/assets/linux-subreaper/linux-arm64/supervisor +0 -0
- package/src/assets/linux-subreaper/linux-x64/manifest.json +10 -0
- package/src/assets/linux-subreaper/linux-x64/supervisor +0 -0
- package/src/assets/web-panel/assets/{AIOps-CpiCBhRh.js → AIOps-DSyZEY2G.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CnU-Xvx-.js → ActionButton-DhNyZ577.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BJW-CNtf.js → Analytics-HMMpxmyi.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-UFQJL3G9.js → AppLayout-CwKMM5Ie.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-Dr_IUDoY.js → Artifacts-CNzDmph7.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-aglWo6rr.js → Audit-DxWpUW3V.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-Dm_TCiXC.js → BackgroundAgents-C1R2ST-P.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DUdX4-gA.js → Backup-B6biTaEV.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BTcLjy1o.js → BaseInput-CVnkD2Z5.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-Bhy9qZrl.js → Chat-Dbewl4Je.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-D0WrJQaQ.js → ChatBubbleRenderer-DVZ6SwC5.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-CblrPa21.js → Checkbox-BqqiF1JE.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-SEAHzOlr.js → Codegen-BVMkGumc.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DJz8iFOR.js → Col-DmiRHy96.js} +1 -1
- package/src/assets/web-panel/assets/{Community-RufKhcQt.js → Community-BDlpygqA.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Dlifx08U.js → Compact-B2NME_m-.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DdhQWcHh.js → Compliance-BlzDLE-R.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BLNn6oOm.js → Cowork-C8raPybB.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-Dv-JWY2d.js → Cron-DA8Py0zP.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-k7Pz7O-b.js → Crosschain-DU2XyHqB.js} +1 -1
- package/src/assets/web-panel/assets/{DID-C_10syUW.js → DID-Df098fT_.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CMyH6Szx.js → Dashboard-D1rT702x.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DjmD2Saa.js → Dropdown-BweXGo1O.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-36_IRUSo.js → EmailListRenderer-B9ShbgJH.js} +1 -1
- package/src/assets/web-panel/assets/EvolutionSettings-D75_iBoc.js +1 -0
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DEgf9kyP.js → FamilyGuardDashboard-DrznZFVs.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DGO1wQls.js → Federation-DQZ2adIA.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Doj1FmKW.js → FormItemContext-DN30Dowh.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-GzOU27iT.js → GenericCardRenderer-DRdt0j1e.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Dd7D1EDK.js → Git-wwRgStzk.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-6IY8LcjI.js → Governance-CI4Uw0Nh.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BB9gl8q1.js → Inference-5TuE2AJI.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CeCed5wd.js → KnowledgeGraph-oXyvmFSR.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CYOeHuaI.js → Logs-ewudSWuv.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-Bk8Zqc2R.js → MarkdownRenderer-Cz5nyufm.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-D-_xgHgM.js → Marketplace-DcZeWFrs.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-NnaUTF5t.js → McpTools-BcV9p1RS.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CndN9pMt.js → Memory-CMhYvugt.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BXblFmhY.js → MobileBridge-5RP2uwvT.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CC26Dm2k.js → MobileProjects-BIuM9Ze4.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BhaqWCiW.js → Mtc-DmBszTDe.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-D6fktM2w.js → MtcAudit-Czdp5ZGt.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-CFvahz7s.js → Multisig-DRWUuNr-.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Co73Ivya.js → NLProgramming-IRQQN0-c.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B4g33L1o.js → Notes-CGhD1lW9.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-lXd17EbN.js → NotificationSettings-BtyPgQAh.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BiOB0GIi.js → OrderTableRenderer-yUaqwCty.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-JQ7_wQIA.js → Organization-C3kTVn5i.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-gwgJgozQ.js → Overflow-C2-0iZhq.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D9L_0gie.js → P2P-CISoEUxA.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CbOPsFE9.js → PdhVaultBrowser-NfTVtvKQ.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-Cz42HNQf.js → Permissions-CfK0jJhm.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DjorYtrF.js → PersonalDataHub-OsVxxh_j.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-DyWPhmm5.js → Pipeline-DEmK_Ch2.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DyfLMl2j.js → Privacy-D61s73b0.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CyqDx1OE.js → ProjectInit-DkLF5gHb.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DsBw0Csy.js → ProjectSettings-BA7odTrI.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CRaSI25Z.js → Projects-CdI1GcAG.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CCI_klN-.js → Providers-D9BYyP1r.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DNsnlDdS.js → QrScannerModal-CXMItUDD.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-B9r2iaee.js → QuickAsk-8-tF9ffZ.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BJDe03GR.js → Recommend-C2QGWOSi.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-J7JR3rRo.js → RemoteSession-B1M_v1lx.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-DR39AJeV.js → Reputation-D0WZHgv1.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Dh77m3NZ.js → Row-D6nf9-c-.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BUdg28nJ.js → RssFeed-ClZ6M-mN.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BNC7sGhj.js → Search-ZWkt8vuO.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DISENGI7.js → Security-BUlQut2m.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BBBGpJ1-.js → Services-Cd_3xJOc.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DMcIoIv2.js → Skeleton-C8e2-cHW.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-QAyFbI3r.js → Skills-DucVRJp1.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CSg5wLH7.js → Sla-Csr5pUwI.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DMPxTc5x.js → SpeechSettings-CXvyaRAT.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CPG7l30y.js → SyncSettings-KdsIcV5h.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Y1kOVvVL.js → Tasks-B-eHmJm9.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-jt9NagL0.js → Templates-BglVeaLK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DauuJICx.js → Tenant-BBbQ8qan.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-E9rnKeGq.js → Terminal-Ca_pCVBi.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Cc7z2APG.js → TimelineRenderer-YD6XG_vZ.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CV0UthRA.js → Tokens-2lRoeVli.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BFcJJGfu.js → Trigger-C-3XNAU9.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BAydbNJA.js → Trust-k-1PxdLG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DcOS6sqL.js → UkeySign-FG-qW646.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BLHJZvif.js → VideoEditing-BNOe1cmd.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-4QFLKHwW.js → Wallet-D5C3i2Kw.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-BocEOOSj.js → WebAuthn-BOpztqd3.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DCfF7D0B.js → WorkflowEditor-BpVrpUEv.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CxvfsYGr.js → chat-DIuMHb38.js} +1 -1
- package/src/assets/web-panel/assets/{colors-KtQB9DmQ.js → colors-CTFJPyzQ.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Dx4J6arN.js → compact-item-CONfmJcW.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-Cq0olgzo.js → createContext-RkW5CRrz.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DaEBln_q.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Bf9-h3D-.js → hasIn-CW4XOjDC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CK2U_AHi.js → index-7wMSBCU7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMR-keCL.js → index-Akmg1WdH.js} +1 -1
- package/src/assets/web-panel/assets/{index-6dDDSCHD.js → index-B54qoVff.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmQuG0fr.js → index-B8r2t0Ch.js} +1 -1
- package/src/assets/web-panel/assets/{index-ClUfSChI.js → index-B9XoBoty.js} +1 -1
- package/src/assets/web-panel/assets/{index-B3xfiItB.js → index-B9aS2VLu.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQDR-_ai.js → index-BFs1Tznc.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZWjYfWt.js → index-BKgaRT0b.js} +1 -1
- package/src/assets/web-panel/assets/{index-nmyDOh_b.js → index-BM7bOUvu.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_8UcDan.js → index-BMg2JFmW.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9yaUHm9.js → index-BRZsPxNn.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cz7f4Jaq.js → index-Bf19fKef.js} +1 -1
- package/src/assets/web-panel/assets/{index-CpO6DKuu.js → index-BkAbfBF-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFdEDvFn.js → index-CBjb2A5R.js} +1 -1
- package/src/assets/web-panel/assets/{index-gt5N6VM_.js → index-CEZWQthY.js} +1 -1
- package/src/assets/web-panel/assets/{index-0fi8DIVQ.js → index-CFGLf88d.js} +1 -1
- package/src/assets/web-panel/assets/{index-hSzyoXri.js → index-CLUa68JH.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cy865Hhq.js → index-CM2X25Er.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDAe1UmZ.js → index-CX4T2uKN.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMcC4cbj.js → index-CixqfdRk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsCdl2Od.js → index-CkRojEip.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnU-dUWd.js → index-Cvmc3f25.js} +1 -1
- package/src/assets/web-panel/assets/{index-Br1mDMfO.js → index-D-lpTPS8.js} +1 -1
- package/src/assets/web-panel/assets/{index-CaIclw-E.js → index-D59GbWCN.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBRlZdZj.js → index-DAESljiJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8WuPI-h.js → index-DDHUx4Yz.js} +1 -1
- package/src/assets/web-panel/assets/index-DG1mcYTa.js +1 -0
- package/src/assets/web-panel/assets/{index-D2BfSIrn.js → index-DP5pPedy.js} +1 -1
- package/src/assets/web-panel/assets/index-DRs5Pe8u.js +1 -0
- package/src/assets/web-panel/assets/{index-CqnggDos.js → index-DSlXL6Ju.js} +1 -1
- package/src/assets/web-panel/assets/{index-_FIz-0tp.js → index-DWCJnn2O.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKtQyb2o.js → index-DWY7YwDu.js} +1 -1
- package/src/assets/web-panel/assets/{index-DfrFNMvz.js → index-DZTwP364.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHmdMKYl.js → index-DidrMvOx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BO6XvO-C.js → index-Dj3K5kHs.js} +1 -1
- package/src/assets/web-panel/assets/{index-iE9tqEN5.js → index-YaeGscSw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DI5bRqNG.js → index-YlMnFNOi.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cfsn7N18.js → index-asWsB34i.js} +1 -1
- package/src/assets/web-panel/assets/{index-CeYP_kT9.js → index-gkjiFjQd.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DvzKJ6o5.js → initDefaultProps-BOr4igh3.js} +1 -1
- package/src/assets/web-panel/assets/{motion-D1yTvmxW.js → motion-CK469n2j.js} +1 -1
- package/src/assets/web-panel/assets/{move-DxtSIF8K.js → move-DzjudlYQ.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BDsQgIHY.js → mtc-parser-BQaKUxC6.js} +1 -1
- package/src/assets/web-panel/assets/{omit-UVBJW8kh.js → omit-MciovCFD.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-D4QqtTyX.js → pickAttrs-DDPJ4zAz.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-QXmu6vBf.js → placementArrow-Cznyb-jI.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-D-PmXO9M.js → responsiveObserve-AJK3SLzJ.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CKbGxEZJ.js → slide-CPQ28wu7.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BbgXPkXm.js → statusUtils-CFjzvqf_.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DOr9rk68.js → styleChecker-BPyFfTuR.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-B2wO2r36.js → useFlexGapSupport-Dq7N6R05.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-SeVGBK34.js → useFs-C3GS0uTc.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DUJBCxjD.js → usePersonalDataHub-BhgIVzP_.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CF4EYsig.js → vnode-4ZD9-MDr.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-pRUs5pqI.js → zoom-Bx4MRVNW.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +1 -1
- package/src/command-manifest.json +1 -1
- package/src/commands/checkpoint.js +42 -7
- package/src/commands/orchestrate.js +85 -25
- package/src/commands/session-location.js +5 -0
- package/src/commands/session-show.js +109 -1
- package/src/commands/session.js +26 -2
- package/src/commands/tokens.js +9 -4
- package/src/data/changelog.json +13 -0
- package/src/generated/cc-agent-protocol.schema.json +71 -0
- package/src/harness/jsonl-session-store.js +357 -114
- package/src/harness/mcp-client.js +44 -1
- package/src/lib/agent-router.js +35 -7
- package/src/lib/app-server/server.js +14 -0
- package/src/lib/background-agent-supervisor.js +14 -4
- package/src/lib/causal-observability.js +64 -1
- package/src/lib/checkpoint-summary-projection.js +98 -0
- package/src/lib/checkpoint-timeline-authority.js +15 -51
- package/src/lib/claude-code-bridge.js +302 -60
- package/src/lib/codex-app-server-adapter.js +161 -35
- package/src/lib/cost-budget.js +4 -0
- package/src/lib/doctor-checkup.js +31 -2
- package/src/lib/durable-security-store.js +6 -1
- package/src/lib/evolution/evolution-artifact-identity.js +21 -0
- package/src/lib/evolution/evolution-artifact-ports.js +5 -11
- package/src/lib/evolution/evolution-eval-cohort-receipt-reconciliation.js +389 -0
- package/src/lib/execution-location-failure-site.js +63 -0
- package/src/lib/execution-location-target.js +102 -8
- package/src/lib/external-agent-adapters.js +2 -0
- package/src/lib/image-file-boundary.js +154 -0
- package/src/lib/image-input.js +16 -3
- package/src/lib/llm-pricing.js +114 -8
- package/src/lib/mcp-call-ledger-store.js +7 -2
- package/src/lib/model-capabilities.js +21 -10
- package/src/lib/model-context-catalog.js +60 -1
- package/src/lib/openai-responses.js +7 -1
- package/src/lib/orchestrator.js +8 -2
- package/src/lib/process-execution-broker/index.js +93 -1
- package/src/lib/process-execution-broker/linux-subreaper-artifact.js +120 -0
- package/src/lib/process-execution-broker/linux-subreaper-helper.js +344 -0
- package/src/lib/process-execution-broker/linux-subreaper-process.js +326 -0
- package/src/lib/process-execution-broker/linux-subreaper-supervisor.c +360 -0
- package/src/lib/process-execution-broker/owned-posix-process-group-worker.mjs +131 -0
- package/src/lib/process-execution-broker/owned-posix-process-group.js +267 -0
- package/src/lib/process-execution-broker/process-ownership-journal.js +297 -0
- package/src/lib/process-execution-broker/process-ownership-quarantine.js +91 -0
- package/src/lib/runtime-usage-ledger.js +3 -0
- package/src/lib/secure-fs.js +158 -42
- package/src/lib/session-branch-history.js +240 -0
- package/src/lib/session-history-origins.js +285 -0
- package/src/lib/session-input-receipt.js +110 -0
- package/src/lib/session-resource-budget.js +8 -8
- package/src/lib/session-transcript-history.js +456 -0
- package/src/lib/session-transcript-page.js +164 -0
- package/src/lib/session-transcript-reference.js +38 -0
- package/src/lib/session-usage.js +8 -0
- package/src/lib/token-tracker.js +32 -54
- package/src/lib/usage-pricing-context.js +93 -0
- package/src/runtime/headless-stream.js +202 -4
- package/src/assets/web-panel/assets/EvolutionSettings-CmtU0XmM.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CmgDNkoO.js +0 -1
- package/src/assets/web-panel/assets/index-BpIZ2S01.js +0 -1
- package/src/assets/web-panel/assets/index-C-VmKuyv.js +0 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { constants } from "node:os";
|
|
4
|
+
import { consumeLinuxSubreaperHelper } from "./linux-subreaper-helper.js";
|
|
5
|
+
|
|
6
|
+
const MAGIC = Buffer.from("CCSUBR01");
|
|
7
|
+
const MAX_FRAME = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export function encodeLinuxSubreaperLaunch(command, args, options) {
|
|
10
|
+
if (
|
|
11
|
+
typeof command !== "string" ||
|
|
12
|
+
!command ||
|
|
13
|
+
command.includes("\0") ||
|
|
14
|
+
!Array.isArray(args) ||
|
|
15
|
+
args.length >= 4096 ||
|
|
16
|
+
args.some((arg) => typeof arg !== "string" || arg.includes("\0")) ||
|
|
17
|
+
typeof options.cwd !== "string" ||
|
|
18
|
+
!isAbsolute(options.cwd) ||
|
|
19
|
+
options.cwd.includes("\0") ||
|
|
20
|
+
!options.env ||
|
|
21
|
+
typeof options.env !== "object" ||
|
|
22
|
+
Array.isArray(options.env)
|
|
23
|
+
)
|
|
24
|
+
throw new TypeError("Invalid Linux subreaper launch");
|
|
25
|
+
const entries = Object.entries(options.env);
|
|
26
|
+
if (
|
|
27
|
+
entries.length > 8192 ||
|
|
28
|
+
entries.some(
|
|
29
|
+
([key, value]) =>
|
|
30
|
+
!key ||
|
|
31
|
+
key.includes("=") ||
|
|
32
|
+
key.includes("\0") ||
|
|
33
|
+
typeof value !== "string" ||
|
|
34
|
+
value.includes("\0"),
|
|
35
|
+
)
|
|
36
|
+
) {
|
|
37
|
+
throw new TypeError("Invalid Linux subreaper environment");
|
|
38
|
+
}
|
|
39
|
+
const graceMs = options.graceMs ?? 250;
|
|
40
|
+
if (!Number.isSafeInteger(graceMs) || graceMs < 0 || graceMs > 2147483647)
|
|
41
|
+
throw new TypeError("Invalid Linux subreaper grace period");
|
|
42
|
+
const strings = [
|
|
43
|
+
options.cwd,
|
|
44
|
+
command,
|
|
45
|
+
...args,
|
|
46
|
+
...entries.map(([key, value]) => `${key}=${value}`),
|
|
47
|
+
].map((value) => Buffer.from(value, "utf8"));
|
|
48
|
+
const length = 24 + strings.reduce((sum, value) => sum + 4 + value.length, 0);
|
|
49
|
+
if (length > MAX_FRAME)
|
|
50
|
+
throw new TypeError("Linux subreaper launch exceeds its byte limit");
|
|
51
|
+
const frame = Buffer.alloc(length);
|
|
52
|
+
MAGIC.copy(frame);
|
|
53
|
+
frame.writeUInt32BE(length, 8);
|
|
54
|
+
frame.writeUInt32BE(args.length + 1, 12);
|
|
55
|
+
frame.writeUInt32BE(entries.length, 16);
|
|
56
|
+
frame.writeUInt32BE(graceMs, 20);
|
|
57
|
+
let offset = 24;
|
|
58
|
+
for (const value of strings) {
|
|
59
|
+
frame.writeUInt32BE(value.length, offset);
|
|
60
|
+
offset += 4;
|
|
61
|
+
value.copy(frame, offset);
|
|
62
|
+
offset += value.length;
|
|
63
|
+
}
|
|
64
|
+
return frame;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Native lifecycle primitive for an admitted Broker launch plan. Production
|
|
69
|
+
* uses a one-use, privately branded helper descriptor. The explicit path seam
|
|
70
|
+
* remains for native build/verification tests, never Broker request options.
|
|
71
|
+
*/
|
|
72
|
+
export function spawnLinuxSubreaper(command, args, options, native) {
|
|
73
|
+
// Claim the lease before validating the launch so every rejection closes its
|
|
74
|
+
// descriptor. A bad plan must not strand an otherwise valid installation pin.
|
|
75
|
+
const helper = options.helper
|
|
76
|
+
? consumeLinuxSubreaperHelper(options.helper)
|
|
77
|
+
: null;
|
|
78
|
+
let launch;
|
|
79
|
+
let child;
|
|
80
|
+
try {
|
|
81
|
+
if (
|
|
82
|
+
(native.platform ?? process.platform) !== "linux" ||
|
|
83
|
+
typeof native.spawn !== "function"
|
|
84
|
+
)
|
|
85
|
+
throw new TypeError("Linux Broker native spawn is required");
|
|
86
|
+
if (
|
|
87
|
+
!options.helper &&
|
|
88
|
+
(typeof options.helperPath !== "string" ||
|
|
89
|
+
!isAbsolute(options.helperPath))
|
|
90
|
+
)
|
|
91
|
+
throw new TypeError(
|
|
92
|
+
"An absolute Linux subreaper helper path is required",
|
|
93
|
+
);
|
|
94
|
+
if (options.helper && options.helperPath)
|
|
95
|
+
throw new TypeError("Ambiguous Linux subreaper helper identity");
|
|
96
|
+
if (
|
|
97
|
+
Object.keys(options).some(
|
|
98
|
+
(key) =>
|
|
99
|
+
!["cwd", "env", "graceMs", "helperPath", "helper"].includes(key),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
throw new TypeError("Unsupported Linux subreaper option");
|
|
103
|
+
launch = encodeLinuxSubreaperLaunch(command, args, options);
|
|
104
|
+
child = native.spawn(helper ? "/proc/self/fd/4" : options.helperPath, [], {
|
|
105
|
+
cwd: options.cwd,
|
|
106
|
+
env: { PATH: "/usr/bin:/bin", LC_ALL: "C" },
|
|
107
|
+
shell: false,
|
|
108
|
+
detached: false,
|
|
109
|
+
stdio: [
|
|
110
|
+
"pipe",
|
|
111
|
+
"pipe",
|
|
112
|
+
"pipe",
|
|
113
|
+
"pipe",
|
|
114
|
+
...(helper ? [helper.descriptor] : []),
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
// Native spawn duplicates stdio synchronously. Never reopen the verified
|
|
119
|
+
// executable path; the child's descriptor now owns that exact image.
|
|
120
|
+
helper?.release();
|
|
121
|
+
}
|
|
122
|
+
const control = child.stdio[3];
|
|
123
|
+
let closed = false;
|
|
124
|
+
let requestedSignal = null;
|
|
125
|
+
let buffer = "";
|
|
126
|
+
let bytes = 0;
|
|
127
|
+
let targetPid = null;
|
|
128
|
+
let target = null;
|
|
129
|
+
let cleanup = null;
|
|
130
|
+
let protocolError = null;
|
|
131
|
+
const terminate = (signal = "SIGTERM") => {
|
|
132
|
+
if (!["SIGTERM", "SIGKILL"].includes(signal))
|
|
133
|
+
throw new TypeError("Invalid Linux subreaper stop signal");
|
|
134
|
+
if (closed || control.destroyed || !control.writable) return false;
|
|
135
|
+
if (requestedSignal === "SIGKILL" || requestedSignal === signal)
|
|
136
|
+
return true;
|
|
137
|
+
requestedSignal = signal;
|
|
138
|
+
control.write(signal === "SIGKILL" ? "K" : "T");
|
|
139
|
+
return true;
|
|
140
|
+
};
|
|
141
|
+
control.on("error", () => {
|
|
142
|
+
protocolError ??= "control-channel-failed";
|
|
143
|
+
control.destroy();
|
|
144
|
+
});
|
|
145
|
+
control.on("data", (chunk) => {
|
|
146
|
+
bytes += chunk.length;
|
|
147
|
+
if (bytes > 16 * 1024) {
|
|
148
|
+
protocolError = "control-reply-limit";
|
|
149
|
+
control.destroy();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
buffer += chunk.toString("ascii");
|
|
153
|
+
let newline;
|
|
154
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
155
|
+
const line = buffer.slice(0, newline);
|
|
156
|
+
buffer = buffer.slice(newline + 1);
|
|
157
|
+
try {
|
|
158
|
+
const message = JSON.parse(line);
|
|
159
|
+
if (
|
|
160
|
+
message.type === "started" &&
|
|
161
|
+
targetPid === null &&
|
|
162
|
+
Number.isSafeInteger(message.pid) &&
|
|
163
|
+
message.pid > 0
|
|
164
|
+
) {
|
|
165
|
+
targetPid = message.pid;
|
|
166
|
+
} else if (
|
|
167
|
+
message.type === "target-exit" &&
|
|
168
|
+
target === null &&
|
|
169
|
+
targetPid !== null &&
|
|
170
|
+
Number.isInteger(message.code) &&
|
|
171
|
+
message.code >= -1 &&
|
|
172
|
+
message.code <= 255 &&
|
|
173
|
+
Number.isInteger(message.signal) &&
|
|
174
|
+
message.signal >= 0 &&
|
|
175
|
+
message.signal <= 64 &&
|
|
176
|
+
Number.isInteger(message.spawnErrno) &&
|
|
177
|
+
message.spawnErrno >= -1 &&
|
|
178
|
+
message.spawnErrno <= 4095
|
|
179
|
+
) {
|
|
180
|
+
target = {
|
|
181
|
+
code: message.code,
|
|
182
|
+
signal: message.signal,
|
|
183
|
+
spawnErrno: message.spawnErrno,
|
|
184
|
+
};
|
|
185
|
+
} else if (
|
|
186
|
+
message.type === "cleanup" &&
|
|
187
|
+
cleanup === null &&
|
|
188
|
+
target !== null &&
|
|
189
|
+
typeof message.confirmed === "boolean" &&
|
|
190
|
+
message.rootReaped === true &&
|
|
191
|
+
Number.isSafeInteger(message.reaped) &&
|
|
192
|
+
message.reaped >= 1
|
|
193
|
+
) {
|
|
194
|
+
cleanup = message;
|
|
195
|
+
} else if (
|
|
196
|
+
message.type === "error" &&
|
|
197
|
+
["subreaper-unavailable", "cleanup-unconfirmed"].includes(
|
|
198
|
+
message.code,
|
|
199
|
+
)
|
|
200
|
+
) {
|
|
201
|
+
protocolError ??= message.code;
|
|
202
|
+
} else throw new Error("invalid reply");
|
|
203
|
+
} catch {
|
|
204
|
+
protocolError = "invalid-control-reply";
|
|
205
|
+
control.destroy();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
const completion = new Promise((resolve) => {
|
|
211
|
+
child.on("error", () => {
|
|
212
|
+
protocolError ??= "supervisor-spawn-failed";
|
|
213
|
+
});
|
|
214
|
+
child.once("close", (code, signal) => {
|
|
215
|
+
closed = true;
|
|
216
|
+
control.destroy();
|
|
217
|
+
if (buffer) protocolError ??= "truncated-control-reply";
|
|
218
|
+
if (!cleanup) protocolError ??= "missing-cleanup-receipt";
|
|
219
|
+
else if (!cleanup.confirmed)
|
|
220
|
+
protocolError ??= "unconfirmed-cleanup-receipt";
|
|
221
|
+
const confirmed =
|
|
222
|
+
cleanup?.confirmed === true &&
|
|
223
|
+
code === 0 &&
|
|
224
|
+
signal === null &&
|
|
225
|
+
!protocolError;
|
|
226
|
+
resolve({
|
|
227
|
+
targetPid,
|
|
228
|
+
target,
|
|
229
|
+
supervisor: { code, signal, closed: true },
|
|
230
|
+
cleanup: {
|
|
231
|
+
mode: "linux-subreaper",
|
|
232
|
+
confirmed,
|
|
233
|
+
descendantsReaped: confirmed,
|
|
234
|
+
reaped: cleanup?.reaped ?? null,
|
|
235
|
+
protocolError,
|
|
236
|
+
// The zero-child fence is a lifecycle fact, not hostile-code isolation.
|
|
237
|
+
processTreeContained: false,
|
|
238
|
+
},
|
|
239
|
+
...(helper
|
|
240
|
+
? {
|
|
241
|
+
helper: {
|
|
242
|
+
sourceDigest: helper.sourceDigest,
|
|
243
|
+
imageDigest: helper.imageDigest,
|
|
244
|
+
binding: "unlinked-inherited-fd",
|
|
245
|
+
distribution: helper.distribution,
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
control.write(launch);
|
|
253
|
+
return Object.freeze({ child, terminate, completion });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Broker-facing process handle: close is fenced by the kernel tree receipt. */
|
|
257
|
+
export function spawnLinuxSubreaperChild(command, args, options, native) {
|
|
258
|
+
const owner = spawnLinuxSubreaper(command, args, options, native);
|
|
259
|
+
const raw = owner.child;
|
|
260
|
+
const child = new EventEmitter();
|
|
261
|
+
Object.assign(child, {
|
|
262
|
+
pid: raw.pid,
|
|
263
|
+
sandboxWrapperPid: raw.pid,
|
|
264
|
+
stdin: raw.stdin,
|
|
265
|
+
stdout: raw.stdout,
|
|
266
|
+
stderr: raw.stderr,
|
|
267
|
+
stdio: raw.stdio,
|
|
268
|
+
exitCode: null,
|
|
269
|
+
signalCode: null,
|
|
270
|
+
killed: false,
|
|
271
|
+
ownedProcessTreeClosed: owner.completion,
|
|
272
|
+
kill(signal = "SIGTERM") {
|
|
273
|
+
const requested = owner.terminate(signal);
|
|
274
|
+
if (requested) child.killed = true;
|
|
275
|
+
return requested;
|
|
276
|
+
},
|
|
277
|
+
ref() {
|
|
278
|
+
raw.ref?.();
|
|
279
|
+
return child;
|
|
280
|
+
},
|
|
281
|
+
unref() {
|
|
282
|
+
raw.unref?.();
|
|
283
|
+
return child;
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
raw.once("spawn", () => child.emit("spawn"));
|
|
287
|
+
owner.completion.then((receipt) => {
|
|
288
|
+
child.ownedProcessTreeEvidence = receipt;
|
|
289
|
+
child.sandboxTargetPid = receipt.targetPid;
|
|
290
|
+
if (!receipt.cleanup.confirmed) {
|
|
291
|
+
const error = new Error(
|
|
292
|
+
"Linux external-agent process-tree cleanup is unconfirmed",
|
|
293
|
+
);
|
|
294
|
+
error.code = raw.pid
|
|
295
|
+
? "EXTERNAL_AGENT_CLEANUP_UNCONFIRMED"
|
|
296
|
+
: "EXTERNAL_AGENT_SPAWN_FAILED";
|
|
297
|
+
// A failed native spawn never created an owner or target. All other
|
|
298
|
+
// losses retain the close fence: helper exit alone cannot release an
|
|
299
|
+
// agent while an unobserved adopted descendant may still be executing.
|
|
300
|
+
child.emit("error", error);
|
|
301
|
+
if (!raw.pid) {
|
|
302
|
+
child.exitCode = -1;
|
|
303
|
+
child.emit("exit", -1, null);
|
|
304
|
+
child.emit("close", -1, null);
|
|
305
|
+
} else {
|
|
306
|
+
child.emit("cleanup:unconfirmed", receipt);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (receipt.target.spawnErrno) {
|
|
311
|
+
const error = new Error("External agent executable could not be started");
|
|
312
|
+
error.code = "EXTERNAL_AGENT_SPAWN_FAILED";
|
|
313
|
+
child.emit("error", error);
|
|
314
|
+
}
|
|
315
|
+
child.exitCode = receipt.target.code < 0 ? null : receipt.target.code;
|
|
316
|
+
child.signalCode =
|
|
317
|
+
receipt.target.signal === 0
|
|
318
|
+
? null
|
|
319
|
+
: (Object.keys(constants.signals).find(
|
|
320
|
+
(signal) => constants.signals[signal] === receipt.target.signal,
|
|
321
|
+
) ?? "SIGUNKNOWN");
|
|
322
|
+
child.emit("exit", child.exitCode, child.signalCode);
|
|
323
|
+
child.emit("close", child.exitCode, child.signalCode);
|
|
324
|
+
});
|
|
325
|
+
return child;
|
|
326
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#define _GNU_SOURCE
|
|
2
|
+
/*
|
|
3
|
+
* Linux lifecycle owner for one pre-admitted Broker plan. This is not a
|
|
4
|
+
* sandbox: it does not constrain filesystem/network access or prevent an
|
|
5
|
+
* executable from attacking its supervisor. Only an intact, zero-child
|
|
6
|
+
* receipt plus the supervisor's successful close proves cleanup.
|
|
7
|
+
*
|
|
8
|
+
* fd 3: private Unix socket. Request: CCSUBR01, then BE32 frame length,
|
|
9
|
+
* argc, envc, grace-ms; then length-prefixed cwd, argv (including executable),
|
|
10
|
+
* and KEY=VALUE environment strings. Subsequent bytes T/K request TERM/KILL.
|
|
11
|
+
* Replies are bounded JSONL, never target stdout/stderr.
|
|
12
|
+
*/
|
|
13
|
+
#include <ctype.h>
|
|
14
|
+
#include <dirent.h>
|
|
15
|
+
#include <errno.h>
|
|
16
|
+
#include <fcntl.h>
|
|
17
|
+
#include <limits.h>
|
|
18
|
+
#include <poll.h>
|
|
19
|
+
#include <signal.h>
|
|
20
|
+
#include <stdint.h>
|
|
21
|
+
#include <stdio.h>
|
|
22
|
+
#include <stdlib.h>
|
|
23
|
+
#include <string.h>
|
|
24
|
+
#include <sys/prctl.h>
|
|
25
|
+
#include <sys/socket.h>
|
|
26
|
+
#include <sys/stat.h>
|
|
27
|
+
#include <sys/types.h>
|
|
28
|
+
#include <sys/wait.h>
|
|
29
|
+
#include <time.h>
|
|
30
|
+
#include <unistd.h>
|
|
31
|
+
|
|
32
|
+
#define CONTROL_FD 3
|
|
33
|
+
#define MAX_FRAME (1024U * 1024U)
|
|
34
|
+
#define MAX_CHILDREN 65536U
|
|
35
|
+
#define HEADER_SIZE 24U
|
|
36
|
+
|
|
37
|
+
static volatile sig_atomic_t stop_level;
|
|
38
|
+
static int protocol_ok = 1;
|
|
39
|
+
static int control_closed;
|
|
40
|
+
static pid_t soft_pids[MAX_CHILDREN];
|
|
41
|
+
static size_t soft_count;
|
|
42
|
+
|
|
43
|
+
static void on_stop(int signal_number) {
|
|
44
|
+
(void)signal_number;
|
|
45
|
+
if (stop_level < 1) stop_level = 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static int64_t monotonic_ms(void) {
|
|
49
|
+
struct timespec value;
|
|
50
|
+
if (clock_gettime(CLOCK_MONOTONIC, &value) != 0) _exit(70);
|
|
51
|
+
return (int64_t)value.tv_sec * 1000 + value.tv_nsec / 1000000;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static uint32_t be32(const unsigned char *bytes) {
|
|
55
|
+
return ((uint32_t)bytes[0] << 24) | ((uint32_t)bytes[1] << 16) |
|
|
56
|
+
((uint32_t)bytes[2] << 8) | (uint32_t)bytes[3];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static void reply(const char *message) {
|
|
60
|
+
size_t length = strlen(message);
|
|
61
|
+
ssize_t count;
|
|
62
|
+
do {
|
|
63
|
+
count = send(CONTROL_FD, message, length, MSG_DONTWAIT | MSG_NOSIGNAL);
|
|
64
|
+
} while (count < 0 && errno == EINTR);
|
|
65
|
+
if (count != (ssize_t)length) protocol_ok = 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static int read_exact(unsigned char *bytes, size_t length, int64_t deadline) {
|
|
69
|
+
size_t offset = 0;
|
|
70
|
+
while (offset < length && !stop_level) {
|
|
71
|
+
int64_t remaining = deadline - monotonic_ms();
|
|
72
|
+
if (remaining <= 0) return -1;
|
|
73
|
+
struct pollfd item = {CONTROL_FD, POLLIN, 0};
|
|
74
|
+
int result = poll(&item, 1, remaining > 100 ? 100 : (int)remaining);
|
|
75
|
+
if (result < 0 && errno == EINTR) continue;
|
|
76
|
+
if (result < 0) return -1;
|
|
77
|
+
if (result == 0) continue;
|
|
78
|
+
ssize_t count = read(CONTROL_FD, bytes + offset, length - offset);
|
|
79
|
+
if (count < 0 && (errno == EINTR || errno == EAGAIN)) continue;
|
|
80
|
+
if (count <= 0) return -1;
|
|
81
|
+
offset += (size_t)count;
|
|
82
|
+
}
|
|
83
|
+
return offset == length ? 0 : -1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static char *next_string(unsigned char *body, size_t length, size_t *offset) {
|
|
87
|
+
if (*offset > length || length - *offset < 4) return NULL;
|
|
88
|
+
uint32_t size = be32(body + *offset);
|
|
89
|
+
*offset += 4;
|
|
90
|
+
if (size > length - *offset || memchr(body + *offset, 0, size)) return NULL;
|
|
91
|
+
char *value = malloc((size_t)size + 1);
|
|
92
|
+
if (!value) return NULL;
|
|
93
|
+
memcpy(value, body + *offset, size);
|
|
94
|
+
value[size] = 0;
|
|
95
|
+
*offset += size;
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* No automatic shell fallback for an executable with an invalid format. */
|
|
100
|
+
static void execute_target(char **args, char **environment) {
|
|
101
|
+
if (strchr(args[0], '/')) {
|
|
102
|
+
execve(args[0], args, environment);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const char *search = "/usr/bin:/bin";
|
|
106
|
+
for (size_t i = 0; environment[i]; i++) {
|
|
107
|
+
if (strncmp(environment[i], "PATH=", 5) == 0) search = environment[i] + 5;
|
|
108
|
+
}
|
|
109
|
+
int denied = 0;
|
|
110
|
+
for (;;) {
|
|
111
|
+
const char *end = strchr(search, ':');
|
|
112
|
+
size_t directory_length = end ? (size_t)(end - search) : strlen(search);
|
|
113
|
+
size_t command_length = strlen(args[0]);
|
|
114
|
+
char *candidate = malloc(directory_length + command_length + 2);
|
|
115
|
+
if (!candidate) { errno = ENOMEM; return; }
|
|
116
|
+
if (directory_length) {
|
|
117
|
+
memcpy(candidate, search, directory_length);
|
|
118
|
+
candidate[directory_length] = '/';
|
|
119
|
+
memcpy(candidate + directory_length + 1, args[0], command_length + 1);
|
|
120
|
+
} else {
|
|
121
|
+
memcpy(candidate, args[0], command_length + 1);
|
|
122
|
+
}
|
|
123
|
+
execve(candidate, args, environment);
|
|
124
|
+
int error = errno;
|
|
125
|
+
free(candidate);
|
|
126
|
+
if (error == EACCES) denied = 1;
|
|
127
|
+
else if (error != ENOENT && error != ENOTDIR) { errno = error; return; }
|
|
128
|
+
if (!end) break;
|
|
129
|
+
search = end + 1;
|
|
130
|
+
}
|
|
131
|
+
errno = denied ? EACCES : ENOENT;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static void remove_soft_pid(pid_t pid) {
|
|
135
|
+
for (size_t i = 0; i < soft_count; i++) {
|
|
136
|
+
if (soft_pids[i] == pid) { soft_pids[i] = soft_pids[--soft_count]; return; }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static int signal_owned_pid(pid_t pid, int signal_number) {
|
|
141
|
+
if (signal_number == SIGTERM) {
|
|
142
|
+
for (size_t i = 0; i < soft_count; i++) if (soft_pids[i] == pid) return 0;
|
|
143
|
+
}
|
|
144
|
+
if (kill(pid, signal_number) != 0 && errno != ESRCH) return -1;
|
|
145
|
+
if (signal_number == SIGTERM && soft_count < MAX_CHILDREN) soft_pids[soft_count++] = pid;
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* WSL1 implements subreapers but not /proc/<pid>/task/<tid>/children. */
|
|
150
|
+
static int signal_children_from_proc(int signal_number) {
|
|
151
|
+
DIR *directory = opendir("/proc");
|
|
152
|
+
if (!directory) return -1;
|
|
153
|
+
int ok = 1;
|
|
154
|
+
struct dirent *entry;
|
|
155
|
+
while ((entry = readdir(directory)) != NULL) {
|
|
156
|
+
char *end;
|
|
157
|
+
errno = 0;
|
|
158
|
+
long pid = strtol(entry->d_name, &end, 10);
|
|
159
|
+
if (errno || *end || end == entry->d_name || pid <= 0 || pid > INT_MAX) continue;
|
|
160
|
+
char pathname[96], data[8192];
|
|
161
|
+
int size = snprintf(pathname, sizeof(pathname), "/proc/%ld/stat", pid);
|
|
162
|
+
if (size <= 0 || (size_t)size >= sizeof(pathname)) { ok = 0; continue; }
|
|
163
|
+
int descriptor = open(pathname, O_RDONLY | O_CLOEXEC);
|
|
164
|
+
if (descriptor < 0) {
|
|
165
|
+
if (errno != ENOENT && errno != ESRCH) ok = 0;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
ssize_t length;
|
|
169
|
+
do { length = read(descriptor, data, sizeof(data) - 1); } while (length < 0 && errno == EINTR);
|
|
170
|
+
int read_error = errno;
|
|
171
|
+
close(descriptor);
|
|
172
|
+
if (length <= 0) {
|
|
173
|
+
if (length == 0 || (read_error != ENOENT && read_error != ESRCH)) ok = 0;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
data[length] = 0;
|
|
177
|
+
char *tail = strrchr(data, ')');
|
|
178
|
+
char state;
|
|
179
|
+
long ppid;
|
|
180
|
+
if (!tail || sscanf(tail + 1, " %c %ld", &state, &ppid) != 2) { ok = 0; continue; }
|
|
181
|
+
if (ppid == (long)getpid() && signal_owned_pid((pid_t)pid, signal_number)) ok = 0;
|
|
182
|
+
}
|
|
183
|
+
closedir(directory);
|
|
184
|
+
return ok ? 0 : -1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/*
|
|
188
|
+
* Only this single thread reaps children. Between the children snapshot and
|
|
189
|
+
* kill(), no wait is performed: a child PID stays reserved even if it exits.
|
|
190
|
+
* Adopted descendants are direct children too, including setsid/double-fork.
|
|
191
|
+
* No saved original-root PID or process-group ID authorizes a delayed kill.
|
|
192
|
+
*/
|
|
193
|
+
static int signal_owned_children(int signal_number) {
|
|
194
|
+
char pathname[96];
|
|
195
|
+
int size = snprintf(pathname, sizeof(pathname), "/proc/self/task/%ld/children", (long)getpid());
|
|
196
|
+
if (size <= 0 || (size_t)size >= sizeof(pathname)) return -1;
|
|
197
|
+
int descriptor = open(pathname, O_RDONLY | O_CLOEXEC);
|
|
198
|
+
if (descriptor < 0) return signal_children_from_proc(signal_number);
|
|
199
|
+
char *data = malloc(MAX_FRAME + 1);
|
|
200
|
+
if (!data) { close(descriptor); return -1; }
|
|
201
|
+
size_t length = 0;
|
|
202
|
+
int ok = 1;
|
|
203
|
+
while (length < MAX_FRAME) {
|
|
204
|
+
ssize_t count = read(descriptor, data + length, MAX_FRAME - length);
|
|
205
|
+
if (count < 0 && errno == EINTR) continue;
|
|
206
|
+
if (count < 0) { ok = 0; break; }
|
|
207
|
+
if (count == 0) break;
|
|
208
|
+
length += (size_t)count;
|
|
209
|
+
}
|
|
210
|
+
close(descriptor);
|
|
211
|
+
if (length == MAX_FRAME) ok = 0;
|
|
212
|
+
data[length] = 0;
|
|
213
|
+
char *cursor = data;
|
|
214
|
+
while (ok && *cursor) {
|
|
215
|
+
while (isspace((unsigned char)*cursor)) cursor++;
|
|
216
|
+
if (!*cursor) break;
|
|
217
|
+
char *end;
|
|
218
|
+
errno = 0;
|
|
219
|
+
long number = strtol(cursor, &end, 10);
|
|
220
|
+
if (errno || end == cursor || number <= 0 || number > INT_MAX ||
|
|
221
|
+
(*end && !isspace((unsigned char)*end))) { ok = 0; break; }
|
|
222
|
+
cursor = end;
|
|
223
|
+
pid_t pid = (pid_t)number;
|
|
224
|
+
if (signal_owned_pid(pid, signal_number)) ok = 0;
|
|
225
|
+
}
|
|
226
|
+
free(data);
|
|
227
|
+
return ok ? 0 : -1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
static void read_control(void) {
|
|
231
|
+
unsigned char buffer[64];
|
|
232
|
+
ssize_t count = read(CONTROL_FD, buffer, sizeof(buffer));
|
|
233
|
+
if (count < 0 && (errno == EAGAIN || errno == EINTR)) return;
|
|
234
|
+
if (count <= 0) { protocol_ok = 0; control_closed = 1; if (!stop_level) stop_level = 1; return; }
|
|
235
|
+
for (ssize_t i = 0; i < count; i++) {
|
|
236
|
+
if (buffer[i] == 'K') stop_level = 2;
|
|
237
|
+
else if (buffer[i] == 'T') { if (!stop_level) stop_level = 1; }
|
|
238
|
+
else { protocol_ok = 0; stop_level = 2; }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
int main(void) {
|
|
243
|
+
struct stat info;
|
|
244
|
+
if (fstat(CONTROL_FD, &info) || !S_ISSOCK(info.st_mode)) return 70;
|
|
245
|
+
if (fcntl(CONTROL_FD, F_SETFD, FD_CLOEXEC) ||
|
|
246
|
+
fcntl(CONTROL_FD, F_SETFL, O_NONBLOCK)) return 70;
|
|
247
|
+
struct sigaction action;
|
|
248
|
+
memset(&action, 0, sizeof(action));
|
|
249
|
+
sigemptyset(&action.sa_mask);
|
|
250
|
+
action.sa_handler = on_stop;
|
|
251
|
+
if (sigaction(SIGTERM, &action, NULL) || sigaction(SIGINT, &action, NULL)) return 70;
|
|
252
|
+
action.sa_handler = SIG_IGN;
|
|
253
|
+
if (sigaction(SIGPIPE, &action, NULL)) return 70;
|
|
254
|
+
action.sa_handler = SIG_DFL;
|
|
255
|
+
if (sigaction(SIGCHLD, &action, NULL)) return 70;
|
|
256
|
+
int enabled = 0;
|
|
257
|
+
if (prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) ||
|
|
258
|
+
prctl(PR_GET_CHILD_SUBREAPER, &enabled, 0, 0, 0) || enabled != 1 ||
|
|
259
|
+
prctl(PR_SET_DUMPABLE, 0, 0, 0, 0)) {
|
|
260
|
+
reply("{\"type\":\"error\",\"code\":\"subreaper-unavailable\"}\n");
|
|
261
|
+
return 70;
|
|
262
|
+
}
|
|
263
|
+
unsigned char header[HEADER_SIZE];
|
|
264
|
+
int64_t startup_deadline = monotonic_ms() + 10000;
|
|
265
|
+
if (read_exact(header, sizeof(header), startup_deadline)) return 70;
|
|
266
|
+
uint32_t length = be32(header + 8), argc = be32(header + 12);
|
|
267
|
+
uint32_t envc = be32(header + 16), grace_ms = be32(header + 20);
|
|
268
|
+
if (memcmp(header, "CCSUBR01", 8) || length < HEADER_SIZE || length > MAX_FRAME ||
|
|
269
|
+
argc < 1 || argc > 4096 || envc > 8192 || grace_ms > INT_MAX) return 70;
|
|
270
|
+
size_t body_length = length - HEADER_SIZE, offset = 0;
|
|
271
|
+
unsigned char *body = malloc(body_length ? body_length : 1);
|
|
272
|
+
char **args = calloc((size_t)argc + 1, sizeof(char *));
|
|
273
|
+
char **environment = calloc((size_t)envc + 1, sizeof(char *));
|
|
274
|
+
if (!body || !args || !environment || read_exact(body, body_length, startup_deadline)) return 70;
|
|
275
|
+
char *cwd = next_string(body, body_length, &offset);
|
|
276
|
+
if (!cwd || cwd[0] != '/') return 70;
|
|
277
|
+
for (uint32_t i = 0; i < argc; i++) if (!(args[i] = next_string(body, body_length, &offset))) return 70;
|
|
278
|
+
if (!args[0][0]) return 70;
|
|
279
|
+
for (uint32_t i = 0; i < envc; i++) {
|
|
280
|
+
environment[i] = next_string(body, body_length, &offset);
|
|
281
|
+
if (!environment[i] || !strchr(environment[i], '=') || environment[i][0] == '=') return 70;
|
|
282
|
+
}
|
|
283
|
+
if (offset != body_length) return 70;
|
|
284
|
+
free(body);
|
|
285
|
+
int error_pipe[2];
|
|
286
|
+
if (pipe2(error_pipe, O_CLOEXEC | O_NONBLOCK)) return 70;
|
|
287
|
+
pid_t supervisor_pid = getpid();
|
|
288
|
+
pid_t root_pid = fork();
|
|
289
|
+
if (root_pid < 0) return 70;
|
|
290
|
+
if (root_pid == 0) {
|
|
291
|
+
close(error_pipe[0]);
|
|
292
|
+
close(CONTROL_FD);
|
|
293
|
+
/* Optional pinned helper image; never pass installation handles to target. */
|
|
294
|
+
if (error_pipe[1] != 4) close(4);
|
|
295
|
+
/* Reset handlers inherited across fork, including SIGPIPE for the target. */
|
|
296
|
+
action.sa_handler = SIG_DFL;
|
|
297
|
+
if (sigaction(SIGTERM, &action, NULL) || sigaction(SIGINT, &action, NULL) ||
|
|
298
|
+
sigaction(SIGPIPE, &action, NULL) || prctl(PR_SET_PDEATHSIG, SIGKILL) ||
|
|
299
|
+
getppid() != supervisor_pid || chdir(cwd)) {
|
|
300
|
+
int error = errno ? errno : ECHILD;
|
|
301
|
+
if (write(error_pipe[1], &error, sizeof(error)) != sizeof(error)) _exit(126);
|
|
302
|
+
_exit(127);
|
|
303
|
+
}
|
|
304
|
+
execute_target(args, environment);
|
|
305
|
+
int error = errno;
|
|
306
|
+
if (write(error_pipe[1], &error, sizeof(error)) != sizeof(error)) _exit(126);
|
|
307
|
+
_exit(127);
|
|
308
|
+
}
|
|
309
|
+
close(error_pipe[1]);
|
|
310
|
+
for (uint32_t i = 0; i < argc; i++) free(args[i]);
|
|
311
|
+
for (uint32_t i = 0; i < envc; i++) free(environment[i]);
|
|
312
|
+
free(args); free(environment); free(cwd);
|
|
313
|
+
char message[256];
|
|
314
|
+
snprintf(message, sizeof(message), "{\"type\":\"started\",\"pid\":%ld}\n", (long)root_pid);
|
|
315
|
+
reply(message);
|
|
316
|
+
int root_reaped = 0, warned = 0;
|
|
317
|
+
unsigned long reaped = 0;
|
|
318
|
+
int64_t stop_started = 0;
|
|
319
|
+
for (;;) {
|
|
320
|
+
int status;
|
|
321
|
+
pid_t waited;
|
|
322
|
+
while ((waited = waitpid(-1, &status, WNOHANG)) > 0) {
|
|
323
|
+
reaped++;
|
|
324
|
+
remove_soft_pid(waited);
|
|
325
|
+
if (waited == root_pid && !root_reaped) {
|
|
326
|
+
root_reaped = 1;
|
|
327
|
+
int spawn_errno = 0;
|
|
328
|
+
ssize_t bytes = read(error_pipe[0], &spawn_errno, sizeof(spawn_errno));
|
|
329
|
+
if (bytes != 0 && bytes != sizeof(spawn_errno)) { protocol_ok = 0; spawn_errno = -1; }
|
|
330
|
+
close(error_pipe[0]);
|
|
331
|
+
snprintf(message, sizeof(message), "{\"type\":\"target-exit\",\"code\":%d,\"signal\":%d,\"spawnErrno\":%d}\n",
|
|
332
|
+
WIFEXITED(status) ? WEXITSTATUS(status) : -1,
|
|
333
|
+
WIFSIGNALED(status) ? WTERMSIG(status) : 0, spawn_errno);
|
|
334
|
+
reply(message);
|
|
335
|
+
if (!stop_level) stop_level = 1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (waited < 0 && errno == ECHILD && root_reaped) {
|
|
339
|
+
snprintf(message, sizeof(message), "{\"type\":\"cleanup\",\"confirmed\":%s,\"reaped\":%lu,\"rootReaped\":true}\n", protocol_ok ? "true" : "false", reaped);
|
|
340
|
+
reply(message);
|
|
341
|
+
return protocol_ok ? 0 : 70;
|
|
342
|
+
}
|
|
343
|
+
if (waited < 0 && errno != EINTR && errno != ECHILD) { protocol_ok = 0; stop_level = 2; }
|
|
344
|
+
if (stop_level) {
|
|
345
|
+
int64_t now = monotonic_ms();
|
|
346
|
+
if (!stop_started) stop_started = now;
|
|
347
|
+
int signal_number = stop_level == 2 || now - stop_started >= grace_ms ? SIGKILL : SIGTERM;
|
|
348
|
+
if (signal_owned_children(signal_number)) protocol_ok = 0;
|
|
349
|
+
if (!warned && now - stop_started > 10000) {
|
|
350
|
+
warned = 1;
|
|
351
|
+
reply("{\"type\":\"error\",\"code\":\"cleanup-unconfirmed\"}\n");
|
|
352
|
+
/* Retain ownership and keep reaping; never exit with live children. */
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
struct pollfd item = {control_closed ? -1 : CONTROL_FD, POLLIN, 0};
|
|
356
|
+
int observed = poll(&item, 1, stop_level ? 10 : 100);
|
|
357
|
+
if (observed > 0) read_control();
|
|
358
|
+
if (observed < 0 && errno != EINTR) { protocol_ok = 0; stop_level = 2; }
|
|
359
|
+
}
|
|
360
|
+
}
|