Skip to content

[Bubblewrap] Network policy follow-ups: incomplete parser/validate() pairs, unusable IPv6 rules, and misleading diagnostics #955

Closed

Description

Relevant area(s)

Linux (Bubblewrap backend)

Brief description of your issue

Five follow-ups on the Bubblewrap network policy path. Items 1 and 2 are
incomplete parser/validate() pairs; items 3-5 are correctness and diagnostic
gaps found in review of the firewall-enforcement change.

None of these is a fail-open, and none weakens the enforced posture. Items
1, 2, 4 and 5 are error-reporting or robustness defects; item 3 fails closed
(traffic that cannot flow) plus an inaccurate doc claim.

Bubblewrap's network gates are deliberately duplicated across two entry points —
the JSON config parser (config_parser.rs) and the runner's validate()
(bwrap_runner.rs) — because a Rust caller can hand mxc_engine a hand-built
ExecutionRequest and never touch the parser. bwrap_runner.rs states this
outright:

The parser rejects both of these too, but only sees JSON configs; a Rust
caller can build an ExecutionRequest and reach the runner directly, so the
checks are repeated here rather than assumed.

1. The D3 hostname rule-address check exists only in the runner.

validate() rejects non-IP/CIDR rule addresses via EgressPlan::for_policy
(src/backends/bubblewrap/common/src/bwrap_runner.rs). There is no
config_parser.rs twin, unlike the neighbouring unenforced-host-rules and
local-network gates, which exist in both.

Both callers are still rejected, so this is purely about which error the user
sees and when: backend_error at validate rather than config_parse at parse
time.

2. The proxy + firewall rejection exists only in the parser.

config_parser.rs rejects network.proxy combined with
network.enforcementMode of firewall or both on Bubblewrap. validate()
has no twin, and cannot reach one as written:
ResolvedNetworkMode::from_request tests proxy_active first
(src/backends/bubblewrap/common/src/bwrap_command.rs) and returns
ProxyOnly, so enforcementMode is never consulted on that path.

A programmatic caller therefore has an explicitly-set enforcementMode
silently discarded. The resulting posture is not weaker — ProxyOnly is a
private netns with egress closed to everything but the proxy endpoint — but the
caller is never told their field was ignored.

3. IPv6 rule addresses are accepted, but the namespace has no IPv6.

RuleAddress::parse accepts IPv6 literals and programs ip6tables rules, but
the supervisor launches slirp4netns without --enable-ipv6
(src/backends/bubblewrap/common/src/proxy_network.rs), and slirp4netns
disables IPv6 by default. An IPv6 entry in allowedHosts therefore validates
and installs successfully but can never carry traffic.

This fails closed, so it is not a bypass. The problem is the silent
impossibility plus an inaccurate doc claim: docs/bwrap-support/bubblewrap-backend.md
says "Both families are covered", which is untrue for the allow direction and
contradicts the same file's statements elsewhere that IPv6 egress is denied
outright.

Fix by either enabling/probing IPv6 in the slirp launch, or rejecting IPv6 rule
addresses and correcting the doc.

4. The shared dependency probe emits proxy-specific advice.

probe_dependencies() reports failures as "network.proxy requires
'slirp4netns'" and advises the caller to "omit network.proxy"
(src/backends/bubblewrap/common/src/proxy_network.rs). Firewall-only requests
now enter the same branch, so a caller who never set network.proxy is told to
remove it.

Generalize the wording to private-network enforcement, or pass the resolved
mode so firewall callers get actionable text.

5. rule_install_timeout has no startup headroom.

The budget is XTABLES_LOCK_WAIT * command_count, and its doc comment says it
covers the phase that "begins once slirp is up". In practice attach() starts
the timer before slirp signals readiness, so slirp startup is charged against
a budget sized only for the aggregate -w lock waits. If every rule waited near
its limit, an otherwise successful installation could exceed the deadline.

Only reachable under pathological lock contention (the floor is 5s x 8 = 40s),
but the fix is to add explicit startup headroom rather than using the exact
lock-wait sum, and to correct the comment.

Two further dimensions, found by a second review pass:

  • The budget has no upper bound. command_count() grows with the caller's
    allowedHosts/blockedHosts, which are unbounded, so a large policy scales
    the deadline arbitrarily.
  • It sits outside the caller's timeout. attach() runs before the
    BwrapChild timeout derived from request.script_timeout is constructed, so
    a large policy plus a stalled xtables lock can block sandbox creation for far
    longer than the caller asked for.

So the fix should also cap the setup budget (or the host-list size) and bound
network setup by the request's overall timeout.

6. The live inbound test asserts only the IPv4 ingress chain.

render_filter_payloads emits the MXC_INGRESS declaration and the
-A INPUT -j MXC_INGRESS hook for both families, and the supervisor
restores the v6 payloads with ip6tables-restore
(src/backends/bubblewrap/common/src/proxy_network.rs).
tests/scripts/run_bwrap_inbound_deny_test.sh inspects only iptables, so no
live test pins the v6 chain's content.

The rendering-to-kernel hop itself is already covered: that ip6tables-restore
loop runs before slirp.ready is written and exits 1 on failure, so a v6
payload the kernel rejects aborts startup and fails every Bubblewrap network
test. What is missing is an explicit assertion that the v6 chain exists with the
expected rules and is hooked to INPUT, guarding against a future change that
silently stops rendering the family.

This should land with item 3: enabling IPv6 requires the RFC 4890 ICMPv6 ingress
exemptions, and the same test would need to cover them.

Steps to reproduce

Item 1 - with a schema 0.8.0-alpha config:

{
  "containment": "bubblewrap",
  "network": {
    "defaultPolicy": "block",
    "enforcementMode": "firewall",
    "allowedHosts": ["example.com"]
  }
}

Run it through lxc-exec --experimental.

Item 2 - build an ExecutionRequest programmatically (via mxc-sdk or
mxc_engine directly, bypassing the parser) with both network.proxy enabled
and network_enforcement_mode = Firewall, then call the Bubblewrap runner.

Item 3 - run a schema 0.8.0-alpha firewall config whose allowedHosts
contains an IPv6 literal, and observe that the destination is unreachable
despite the rule installing successfully.

Item 4 - on a host without slirp4netns, run a firewall-mode config that
sets no network.proxy, and read the error.

Item 5 - inspect rule_install_timeout against the attach() call site.

Item 6 - run sudo tests/scripts/run_bwrap_inbound_deny_test.sh and grep the
script for ip6tables.

Expected behavior

Item 1 - rejected by the parser as config_parse, consistent with the other
Bubblewrap network gates.

Item 2 - rejected by validate() with the parser's message verbatim, so the
JSON and programmatic paths agree.

Item 3 - either the IPv6 destination is reachable, or the rule address is
rejected and the limitation documented. Docs must not claim both families work
if they do not.

Item 4 - the diagnostic names the mechanism the caller actually requested.

Item 5 - the budget covers slirp startup plus rule installation, matching
its documented intent.

Item 6 - the live test asserts ip6tables -S MXC_INGRESS and the v6 INPUT
hook alongside the IPv4 ones, without requiring IPv6 connectivity.

Actual behavior

Item 1 - rejected later by validate() as backend_error.

Item 2 - accepted. enforcementMode is silently dropped and the request
resolves to ProxyOnly.

Item 3 - accepted and installed, but no IPv6 traffic can flow; docs claim
otherwise.

Item 4 - the error advises removing a field the caller never set.

Item 5 - the timer starts before slirp readiness with no headroom for it.

Item 6 - only iptables is inspected, so the v6 chain's content is never
verified against a live kernel.

Additional context

Suggested severity: low for items 1, 4, 5 and 6; low-to-medium for item 2
(an explicit, security-relevant field silently ignored); medium for item 3's
documentation claim, low for its runtime effect.

Each fix is small and should reuse the existing shared message constants so the
parser and runner paths cannot drift.

Items 3-5 were raised in review of the Bubblewrap firewall-enforcement PR and
deliberately deferred: none affects the enforced posture, and the PR was already
validated end-to-end. The two gaps that did affect behaviour in that review -
an external network.proxy combined with host lists being accepted on the
programmatic path, and the missing allowLocalNetwork parser twin - were fixed
in the PR itself rather than deferred here.

Item 6 was raised as a non-blocking test suggestion in review of the Bubblewrap
inbound default-deny PR (#964). It is a defense-in-depth assertion over a hop
that already fails closed, so it is deferred here rather than blocking that PR.

Activity

  1. added
    Needs-TriageHas not yet been reviewed, classified, and routed.
    Issue-BugSomething is not working as intended.
    OS-LinuxApplies specifically to Linux hosts or Linux behavior.
    on Aug 19, 2026
  2. added
    Backend-LXCLinux container isolation implemented with LXC.
    Area-SchemaVersioned configuration contracts, JSON schemas, compatibility, or generated wire types.
    on Aug 19, 2026
  3. github-actions commented on Aug 19, 2026

    @github-actions

    Triage complete.

    Labels applied: Area-Executor-Schema, Area-Executor-LXC, Area-Build-Rust (in addition to the existing OS-Linux and Issue-Bug).

    Owner assigned: Soham Das (@SohamDas2021) (already assigned — Linux / LXC / Bubblewrap area).

    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 #955 · sonnet46 · 24.3 AIC · ⌖ 5.3 AIC · ⊞ 7.5K · ◷

  4. changed the title [-][Bubblewrap] Network policy gates: two incomplete parser/validate() pairs report the wrong error or none at all[/-] [+][Bubblewrap] Network policy follow-ups: incomplete parser/validate() pairs, unusable IPv6 rules, and misleading diagnostics[/+] on Aug 19, 2026
  5. github-actions commented on Aug 19, 2026

    @github-actions

    Triage complete. This issue was already correctly labeled and assigned; no changes were needed.

    • Labels already applied: OS-Linux, Area-Executor-LXC, Area-Executor-Schema
    • Owner already assigned: Soham Das (@SohamDas2021) (Bubblewrap / Linux)

    The five items (incomplete parser/validate() pairs, unusable IPv6 rules, misleading dependency probe message, and timer headroom) are all scoped to the Bubblewrap network policy path on Linux.

    Generated by 🏷️ Issue Triage for #955 · sonnet46 · 22.9 AIC · ⌖ 5.29 AIC · ⊞ 7.5K · ◷

  6. github-actions commented on Aug 19, 2026

    @github-actions

    Triage summary

    This issue was already partially triaged. No new labels were added; the existing labels (OS-Linux, Area-Executor-LXC, Area-Executor-Schema) correctly cover the Bubblewrap network policy follow-ups described.

    SohamDas2021 is already assigned as the Bubblewrap/Linux owner.

    Needs-Triage was not present on this issue, so no removal was needed.

    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 #955 · sonnet46 · 20.9 AIC · ⌖ 3.92 AIC · ⊞ 7.5K · ◷

  7. added
    Area-TestShared test infrastructure, fixtures, harnesses, probes, or end-to-end validation.
    on Aug 21, 2026
  8. github-actions commented on Aug 21, 2026

    @github-actions

    Triage summary

    Labels applied: Area-Test-Executor (added for item 6's missing ip6tables chain assertion).

    The issue already carried OS-Linux, Area-Executor-LXC, and Area-Executor-Schema, which correctly cover the Bubblewrap runner, config parser, and slirp/iptables enforcement path. SohamDas2021 was already assigned as the Linux/Bubblewrap owner.

    Needs-Triage was not present on the issue.

    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 #955 · sonnet46 · 25.4 AIC · ⌖ 4.1 AIC · ⊞ 7.6K · ◷

  9. SohamDas2021 commented on Aug 26, 2026

    @SohamDas2021
    ContributorAuthor

    Item 1 premise does not hold today.
    It assumes the neighboring unenforced-host-rules and local-network gates "exist in both" layers; they do not — config_parser.rs has only four Bubblewrap network gates, all proxy-related, and both bwrap_command.rs and bwrap_runner.rs state that the runner is "the only layer that checks what Bubblewrap can enforce," with external-proxy grandfathered as the sole exception. Adding the twin would invert that convention and force RuleAddress out of the Linux-gated, crate-private network_rules (or a hand-duplicate in wxc_common) to buy only a different error code on a request that is already rejected.

    Item 3: the documentation half is already fixed on main — four places now state the namespace has no IPv6, and the_supervisor_leaves_ipv6_disabled_in_the_namespace pins that slirp gets no --enable-ipv6 — leaving only the low-severity, fail-closed runtime effect. The linked PR instead removes the silence that was the real complaint — spawn now warns, naming the IPv6 destinations a plan allows — so proposing item 3 be closed on that basis.

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.Area-TestShared test infrastructure, fixtures, harnesses, probes, or end-to-end validation.Backend-LXCLinux container isolation implemented with LXC.Issue-BugSomething is not working as intended.OS-LinuxApplies specifically to Linux hosts or Linux behavior.Priority3Moderate or limited impact; address as capacity permits.

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions