Repository navigation
[Bubblewrap/SDK] Schema-0.8 network gate: an absent version disables it, and the Node SDK trips it on a field callers never set #945
Description
Relevant area(s)
Linux (Bubblewrap backend), plus the Node SDK
Brief description of your issue
PR #943 makes an unenforceable Bubblewrap network policy a hard error on schema
0.8+ (enforcementMode: firewall/both, and an allowLocalNetwork the
namespace cannot honor). Two entry points into that gate are wrong in opposite
directions. They are filed together because both are about which requests
reach the gate, not about the gate's own logic.
1. An absent version silently disables the gate.
schema_enforces_network_strictly treats an unparsable version as pre-0.8
(src/core/wxc_common/src/config_parser.rs:300-303), and version is optional
at every layer: the parser defaults it to ""
(config_parser.rs:797, cfg.version.unwrap_or_default()), validate_schema_version
returns Ok for an empty string (config_parser.rs:308-312), the dev schema
declares no top-level required, and docs/versioning.md:368-372 documents
"in range / absent → continue". The existing test
schema_version_absent_accepted pins this.
So a config that simply omits version gets the pre-0.8 lenient path and keeps
the silently-unenforced firewall behavior. This is not a privilege bypass — the
config author is the policy author — but it does mean the fail-closed behavior
depends on a field nothing requires. Omission currently resolves to the
oldest semantics rather than the newest.
2. The Node SDK sets enforcementMode: 'firewall' on the caller's behalf,
so Bubblewrap callers hit the gate for a field they never wrote.
applyLinuxNetworkPolicy promotes the mode whenever host rules are present
without a proxy:
sdk/node/src/helper.ts:135-139
const hasHostRules =
!!(config.network.allowedHosts?.length || config.network.blockedHosts?.length);
if (hasHostRules && !hasProxy) {
config.network.enforcementMode = 'firewall';
}That helper is explicitly shared with the Bubblewrap builder (helper.ts:109-111),
and sdk/node/src/sandbox.ts:14 accepts 0.8.0-alpha. A caller who sets only
allowedHosts on Bubblewrap at 0.8 therefore gets a rejection naming
network.enforcementMode, which does not appear anywhere in their policy.
The rejection itself is correct — that caller is silently unenforced today,
which is the bug #943 closes. What is wrong is that the SDK auto-selects an
enforcement path Bubblewrap cannot provide instead of routing the caller to the
one it can (network.proxy), and then surfaces an error about a field the SDK
authored.
Steps to reproduce
Bypass (1), on Linux with the Bubblewrap backend:
- Take any config that sets
network.enforcementMode: "firewall"with
allowedHosts, e.g.tests/configs/bubblewrap_network_firewall_rejected.json. - Delete its
"version"field. - Run it:
lxc-exec --experimental <config>.json.
Breakage (2), via the Node SDK on Linux:
- Build a
SandboxPolicywithversion: '0.8.0-alpha',containment: 'bubblewrap', andnetwork.allowedHosts: ['api.github.com']. Set no proxy
and noenforcementMode. - Call
spawnSandbox.
Expected behavior
- Omitting
versionshould not select the more permissive of two security
postures. Either treat an absent version as the current schema (fail closed),
or reject an absent version specifically when a policy requests one of these
unenforceable controls. - The SDK should not promote Bubblewrap to an enforcement mode the backend
cannot honor. It should either leave the mode unset and report that host
filtering on Bubblewrap requiresnetwork.proxy, or promote only for LXC,
which has the veth and privilege the firewall path needs.
Actual behavior
- The run is accepted on the pre-0.8 path.
enforcementMode: "firewall"builds
an iptables chain that is never hooked intoFORWARD(see [LXC/Bubblewrap] Firewall mode fails open when no veth interface is set (no FORWARD hook) #755, folded into
[LXC/Bubblewrap] Network policy: egress enforcement, hook scoping, and proxy pinning #896), the run exits 0, and no traffic is filtered. - The request is rejected at parse time with
Bubblewrap: network.enforcementMode='firewall' and 'both' are not supported...,
naming a field the caller never set.
Related: #901 (SDK surfaces disagreeing with engine behavior) — (2) is the same
family but is a behavioral auto-promotion rather than a type mismatch.
#755 / #896 for the underlying unattached-chain fail-open that #943 closes.
Activity
github-actions commentedon Aug 19, 2026
Triage complete.
Labels applied:
OS-Linux,Area-SDK-Configuration,Area-SDK-Api,Area-Executor-SchemaOwners assigned: Soham Das (@SohamDas2021) (Linux / Bubblewrap),
@huzaifa-d(SDK API / executor schema)This issue has two distinct problems: (1) an absent
versionfield silently bypasses the schema-0.8 network enforcement gate inconfig_parser.rs, and (2) the Node SDK auto-promotesenforcementModeto'firewall'for Bubblewrap callers who never set it, causing a rejection naming a field the caller didn't author.Maintainers: Comment
/investigateto check whether this issue or bug is valid against the most current code. Copilot will also produce a report with the changes that will be needed. For a small, unambiguous documentation or test fix, it may also create one draft PR.SohamDas2021 commentedon Aug 19, 2026
Both halves of this shift with the Bubblewrap firewall-enforcement work on
user/sodas/bwrap-firewall-enforce(PR to follow).Part 2 is obsolete as written. It describes callers hitting a rejection for a field the Node SDK sets on their behalf. That rejection no longer exists:
enforcementMode: firewall/bothis not rejected on Bubblewrap at 0.8 any more — it genuinely enforces, via iptables programmed inside the sandbox's own network namespace.applyLinuxNetworkPolicypromoting the mode therefore no longer trips a gate.It does invert into a different question worth keeping: those SDK callers now silently get a private netns with real filtering where they previously got an error. That is the intended direction, but it is still a behavior change for existing SDK users that nobody has explicitly signed off. Flagging rather than closing part 2 — retitle or split as triage prefers.
Part 1 still stands.
schema_enforces_network_strictlycontinues to returnfalsefor an empty version (src/core/wxc_common/src/config_parser.rs:307-309), so a config that omitsversionkeeps pre-0.8 semantics and the core complaint — omission resolving to the oldest semantics — is unchanged.One adjacent change worth recording: a non-empty unparsable version now resolves strictly rather than leniently, so a typo like
"0.8"no longer buys pre-0.8 leniency. Empty stays lenient by design, since programmatic callers who never set a version are the pre-0.8 population. This also collapsed the two schema-version predicates that previously disagreed on exactly that input.kanismohammed commentedon Sep 21, 2026
This is specific to the version being absent which will be enforced in the parser