Skip to content

[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

Closed

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:

  1. Take any config that sets network.enforcementMode: "firewall" with
    allowedHosts, e.g. tests/configs/bubblewrap_network_firewall_rejected.json.
  2. Delete its "version" field.
  3. Run it: lxc-exec --experimental <config>.json.

Breakage (2), via the Node SDK on Linux:

  1. Build a SandboxPolicy with version: '0.8.0-alpha', containment: 'bubblewrap', and network.allowedHosts: ['api.github.com']. Set no proxy
    and no enforcementMode.
  2. Call spawnSandbox.

Expected behavior

  1. Omitting version should 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.
  2. 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 requires network.proxy, or promote only for LXC,
    which has the veth and privilege the firewall path needs.

Actual behavior

  1. The run is accepted on the pre-0.8 path. enforcementMode: "firewall" builds
    an iptables chain that is never hooked into FORWARD (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.
  2. 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

  1. github-actions commented on Aug 19, 2026

    @github-actions

    Triage complete.

    Labels applied: OS-Linux, Area-SDK-Configuration, Area-SDK-Api, Area-Executor-Schema

    Owners assigned: Soham Das (@SohamDas2021) (Linux / Bubblewrap), @huzaifa-d (SDK API / executor schema)

    This issue has two distinct problems: (1) an absent version field silently bypasses the schema-0.8 network enforcement gate in config_parser.rs, and (2) the Node SDK auto-promotes enforcementMode to 'firewall' for Bubblewrap callers who never set it, causing a rejection naming a field the caller didn't author.

    Maintainers: Comment /investigate to 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.

    Generated by 🏷️ Issue Triage for #945 · sonnet46 · 26.4 AIC · ⌖ 5.43 AIC · ⊞ 7.5K · ◷

  2. SohamDas2021 commented on Aug 19, 2026

    @SohamDas2021
    ContributorAuthor

    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/both is not rejected on Bubblewrap at 0.8 any more — it genuinely enforces, via iptables programmed inside the sandbox's own network namespace. applyLinuxNetworkPolicy promoting 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_strictly continues to return false for an empty version (src/core/wxc_common/src/config_parser.rs:307-309), so a config that omits version keeps 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.

  3. added
    Priority2High-impact issue affecting key functionality; prioritize for the next appropriate release.
    and removed
    Priority3Moderate or limited impact; address as capacity permits.
    on Sep 21, 2026
  4. kanismohammed commented on Sep 21, 2026

    @kanismohammed
    Collaborator

    This is specific to the version being absent which will be enforced in the parser

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Area-SchemaVersioned configuration contracts, JSON schemas, compatibility, or generated wire types.Issue-BugSomething is not working as intended.OS-LinuxApplies specifically to Linux hosts or Linux behavior.Priority2High-impact issue affecting key functionality; prioritize for the next appropriate release.

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions