From fb5b46ae48903fa67f51c6846d107880445f58cd Mon Sep 17 00:00:00 2001 From: TinyClaw Date: Tue, 28 Apr 2026 02:30:43 +0200 Subject: [PATCH] fix(bonjour): suppress ciao crash when networkInterfaces() is denied Classify ciao interface-enumeration SystemErrors from restricted sandboxes and suppress mDNS advertising instead of letting the Gateway crash. --- extensions/bonjour/src/advertiser.ts | 7 +++++++ extensions/bonjour/src/ciao.test.ts | 21 +++++++++++++++++++++ extensions/bonjour/src/ciao.ts | 10 +++++++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/extensions/bonjour/src/advertiser.ts b/extensions/bonjour/src/advertiser.ts index 76a1f4f6fd2..63114d9d507 100644 --- a/extensions/bonjour/src/advertiser.ts +++ b/extensions/bonjour/src/advertiser.ts @@ -359,6 +359,13 @@ export async function startGatewayBonjourAdvertiser( if (classification.kind === "cancellation") { logger.debug(`bonjour: ignoring unhandled ciao rejection: ${classification.formatted}`); + } else if (classification.kind === "interface-enumeration-failure") { + // Restricted sandboxes can refuse os.networkInterfaces(); mDNS cannot + // function without it, so surface a single warning and skip recovery. + // Recovery would just re-enter the same failing syscall. + logger.warn( + `bonjour: disabling mDNS — networkInterfaces() unavailable in this environment: ${classification.formatted}`, + ); } else { const label = classification.kind === "netmask-assertion" ? "netmask assertion" : "interface assertion"; diff --git a/extensions/bonjour/src/ciao.test.ts b/extensions/bonjour/src/ciao.test.ts index dacd7d7a1f0..48f7726048b 100644 --- a/extensions/bonjour/src/ciao.test.ts +++ b/extensions/bonjour/src/ciao.test.ts @@ -100,6 +100,27 @@ describe("bonjour-ciao", () => { expect(ignoreCiaoUnhandledRejection(error)).toBe(true); }); + it("classifies networkInterfaces SystemError failures (restricted sandboxes)", () => { + const err = Object.assign( + new Error("A system error occurred: uv_interface_addresses returned Unknown system error 1"), + { name: "SystemError" }, + ); + expect(classifyCiaoUnhandledRejection(err)).toEqual({ + kind: "interface-enumeration-failure", + formatted: + "SystemError: A system error occurred: uv_interface_addresses returned Unknown system error 1", + }); + }); + + it("suppresses networkInterfaces failures wrapped in cause chains", () => { + const inner = Object.assign( + new Error("A system error occurred: uv_interface_addresses returned Unknown system error 1"), + { name: "SystemError" }, + ); + const wrapper = new Error("ciao NetworkManager init failed", { cause: inner }); + expect(ignoreCiaoUnhandledRejection(wrapper)).toBe(true); + }); + it("keeps unrelated rejections visible", () => { expect(ignoreCiaoUnhandledRejection(new Error("boom"))).toBe(false); }); diff --git a/extensions/bonjour/src/ciao.ts b/extensions/bonjour/src/ciao.ts index 7f129c968b5..b5d37a502da 100644 --- a/extensions/bonjour/src/ciao.ts +++ b/extensions/bonjour/src/ciao.ts @@ -5,11 +5,16 @@ const CIAO_INTERFACE_ASSERTION_MESSAGE_RE = /REACHED ILLEGAL STATE!?\s+IPV4 ADDRESS CHANGE FROM (?:DEFINED TO UNDEFINED|UNDEFINED TO DEFINED)!?/u; const CIAO_NETMASK_ASSERTION_MESSAGE_RE = /IP ADDRESS VERSION MUST MATCH\.\s+NETMASK CANNOT HAVE A VERSION DIFFERENT FROM THE ADDRESS!?/u; +// Restricted sandboxes (NemoClaw, Docker-in-Docker, k3s with locked-down policy) +// can refuse os.networkInterfaces(), which ciao calls during NetworkManager init. +// Node surfaces this as a SystemError mentioning the libuv syscall by name. +const CIAO_INTERFACE_ENUMERATION_FAILURE_RE = /\bUV_INTERFACE_ADDRESSES\b/u; export type CiaoProcessErrorClassification = | { kind: "cancellation"; formatted: string } | { kind: "interface-assertion"; formatted: string } - | { kind: "netmask-assertion"; formatted: string }; + | { kind: "netmask-assertion"; formatted: string } + | { kind: "interface-enumeration-failure"; formatted: string }; function collectCiaoProcessErrorCandidates(reason: unknown): unknown[] { const queue: unknown[] = [reason]; @@ -64,6 +69,9 @@ export function classifyCiaoProcessError(reason: unknown): CiaoProcessErrorClass if (CIAO_NETMASK_ASSERTION_MESSAGE_RE.test(message)) { return { kind: "netmask-assertion", formatted }; } + if (CIAO_INTERFACE_ENUMERATION_FAILURE_RE.test(message)) { + return { kind: "interface-enumeration-failure", formatted }; + } } return null; }