Fix various bugs CoPilot put into LXC. - #1041
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Enables bridge netfilter in the Azure Pipelines Linux SDK integration lane so LXC schema 0.8 firewall enforcement can initialize correctly.
Changes:
- Loads
br_netfilter. - Enables IPv4 and IPv6 bridge-to-iptables processing.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
9798424 to
f20b21b
Compare
f20b21b to
de013b4
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/backends/lxc/common/src/network_iptables.rs:4716
- This statement was accidentally joined to the function declaration, so the committed file is not rustfmt-formatted and the repository's
cargo fmt --all -- --checkgate will fail.
fn a_legacy_policy_that_installs_nothing_can_still_need_the_network() {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/backends/lxc/common/src/lxc_runner.rs:234
- This is fail-open when a container is reused while already running.
set_config_itemonly appends startup configuration, but the runner explicitly accepts an existing running container below; in that case the current veth remains active whilepermits_no_networkalso makes both firewall managers skip their chains, so the workload retains the previous run's network access. Handle this transition before skipping enforcement (for example, reject reuse or stop/reconfigure/restart it), and cover adestroyOnExit: falsenetworked-to-default-deny reuse case.
if permits_no_network(&request.policy, uses_directional_schema) {
// `empty` gives the container its own network namespace holding only
// a loopback device; `up` activates it, keeping 127.0.0.1 available
// to a workload that binds it.
for (key, value) in [("lxc.net.0.type", "empty"), ("lxc.net.0.flags", "up")] {
de013b4 to
216e6ba
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/backends/lxc/common/src/lxc_runner.rs:235
- This is unsafe for
destroyOnExit=falsereuse.set_config_itemonly appends to the persistent startup config; if the named container is already running, the lateris_running()branch skips restart, so its existing veth remains active whileinstalls_firewallreturns false. The workload then runs with unfiltered network access. Reconcile the active interface before execution (stop/recreate or reject incompatible reuse), and ensure the persistentemptyentry is removed before a later network-enabled reuse.
for (key, value) in [("lxc.net.0.type", "empty"), ("lxc.net.0.flags", "up")] {
if let Err(e) = container.set_config_item(key, value) {
216e6ba to
41cdfb1
Compare
The workflow gave one test target its own step, carrying a comment that justified the check by what a workload might do to the rules confining it. We write that workload, and it never tries. The target still runs: Build.Linux.Job.yml invokes the whole wxc_e2e_tests crate, and this test is in it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9cc60db3-cf27-4d8f-9cdb-3a23286e1b0b
There was a problem hiding this comment.
🟡 Changes recommended
Unprivileged LXC execution is broken, raw sockets bypass egress enforcement, and legacy unmarked mounts can retain stale filesystem access.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
src/backends/lxc/common/src/lxc_bindings.rs:305
- This cleanup only recognizes the new marker block. Containers reused from the previous implementation contain MXC-created
lxc.mount.entrylines without these markers becauseset_config_itemappended them directly; those entries are now treated as operator-owned and survive every rewrite. After upgrading, a later request that removes a read/write path therefore retains the old host mount and stale filesystem access. Add an explicit migration strategy (for example, record owned entries separately or recreate legacy containers) before preserving all unmarked entries.
if trimmed == MANAGED_MOUNTS_BEGIN {
inside = true;
continue;
}
if trimmed == MANAGED_MOUNTS_END {
src/backends/lxc/common/src/lxc_bindings.rs:117
- Moving egress enforcement into the container namespace makes
CAP_NET_RAWpart of the boundary, but this code drops onlyCAP_NET_ADMIN. The attached workload still runs as container root withCAP_NET_RAW, so it can send AF_PACKET frames below the namespace'sOUTPUThook and bypass deny/allow and proxy-only rules (the updated backend documentation itself notes this bypass). Drop or otherwise mediate raw packet sockets before treating these chains as enforcement.
// `libc` does not export this; the value is from linux/capability.h.
const CAP_NET_ADMIN: libc::c_ulong = 12;
- Files reviewed: 52/53 changed files
- Comments generated: 1
- Review effort level: Balanced
The document explained how the backend implements its policy: iptables first-match ordering, which chain a rule lands in, the locale the commands run under, the chain-name prefix, and a comparison with the Bubblewrap backend. A reader choosing a configuration cannot act on any of it. Every claim that survived was checked against the code. Two were wrong. The port 53 accept was described as unconditional; it is emitted only for a legacy configuration whose effective default policy is Block and whose allowed-hosts list is not empty. The IPv6 paragraph described a single outcome; an unusable ip6tables with active IPv6 fails the run, while a merely unavailable ip6tables logs a warning and continues. Where the program already states a fact in an error message, the document no longer repeats it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9cc60db3-cf27-4d8f-9cdb-3a23286e1b0b
Two files carried roughly 240 lines of comment explaining what the line below already said: doc comments repeating their own function name, inner comments restating the condition they preceded, citations to sibling backends offered as justification, and the history of a bug that a test name already records. Two were not merely redundant but false. A doc comment claimed network capability is dropped "when none is requested"; the function has no such condition and its one caller drops the capability on every run. Another claimed an empty environment list leaves the host environment in place, which stopped being true when the caller gained a flag to force the environment cleared. What stayed is what a reader cannot derive from the tree: the kernel refusing to mount over or through a symlink, a read-only zero-size tmpfs rejecting the directory creation that a nested mount needs, a capability constant that libc does not export, and the shutdown reply an init process in an unprivileged user namespace never sends. Facts already carried by an error message or a test failure message were deleted rather than duplicated, and three moved into assertion messages so they reach whoever hits the failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9cc60db3-cf27-4d8f-9cdb-3a23286e1b0b
There was a problem hiding this comment.
🟡 Changes recommended
The current implementation leaves a raw-socket firewall bypass, breaks legacy SDK-generated LXC policies, and can retain pre-upgrade filesystem grants.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
src/backends/lxc/common/src/lxc_bindings.rs:106
- Dropping only
CAP_NET_ADMINdoes not protect anOUTPUT-based egress boundary. Container root still retainsCAP_NET_RAW, so it can create anAF_PACKETsocket and transmit below the namespace'sOUTPUThook, bypassing these rules. RemoveCAP_NET_RAWas well or block packet sockets before treating the in-namespace chain as containment.
const CAP_NET_ADMIN: libc::c_ulong = 12;
// SAFETY: `pre_exec` runs between fork and exec, where only
// async-signal-safe work is permitted. `prctl` is a bare syscall and this
// closure allocates nothing and captures nothing.
unsafe {
command.pre_exec(|| {
if libc::prctl(libc::PR_CAPBSET_DROP, CAP_NET_ADMIN, 0, 0, 0) != 0 {
src/backends/lxc/common/src/filesystem_mounts.rs:186
- This replacement does not revoke mount entries written by pre-upgrade MXC builds. Those entries were appended as ordinary, unmarked
lxc.mount.entrylines, whileset_filesystem_access_pointsstrips only the new managed block; reusing such a container therefore retains stale read/write access alongside the new policy. Add an explicit migration strategy or recreate legacy containers before reuse.
container.set_filesystem_access_points(&entries)?;
sdk/node/tests/integration/linux-process-container.test.ts:52
- The legacy branch still produces a network section without
enforcementMode.supportedVersionsincludes 0.6 and 0.7, the parser defaults those requests tocapabilities, and the new LXC validation rejects that mode, so all three network tests using this helper fail for both legacy versions. Align SDK-generated LXC configs with the backend before keeping these versions in the loop.
function outboundNetwork(version: (typeof supportedVersions)[number]) {
return version.compare('0.8.0-alpha') >= 0
? { egress: { default: 'allow' as const } }
: { allowOutbound: true };
- Files reviewed: 52/53 changed files
- Comments generated: 8
- Review effort level: Balanced
Second commenting pass, covering the eleven remaining backend and spec files. Comment lines drop from 1,691 to 197. What survives is what a reader cannot get from the tree: iptables matching and deletion semantics, RFC 4648 and RFC 2606, the chain-name byte ceiling, Linux IPv4-mapped packet behavior, /proc/net/if_inet6 format, POSIX signal delivery, and Alpine DHCP timing. Five comments asserted something the code does not do: - A cleanup comment named the FORWARD hook; the code installs and removes OUTPUT hooks. - An ingress comment said a real container namespace is guaranteed because the PID is mandatory; the code takes any u32 and proves nothing about the namespace. - An ingress comment said a permissive request is refused before any rule is built; build_ingress_rules emits an ACCEPT for one. - A doc link pointed at Self::classify_container_ipv6_state; the call goes to NetworkIptablesManager. - A runner comment said the /etc/hosts rewrites use only shell builtins; the generated command runs grep. A sixth claimed an omitted 0.8 network section is handled as a directional deny default. It routes to the legacy planner instead, which reaches the same result: a deny-all posture with no local network plans Isolated, so no chain is installed and the assertion that no firewall appears is correct. Facts that belong somewhere the program checks them moved into ten assertion messages, so a failing digest or classifier now says what it was pinning. The ICMPv6 types RFC 4890 requires are named constants rather than numeric literals with trailing labels, same values and same order. Two local bindings named unpin became clear_stale_pin_command and clear_run_pin_command. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9cc60db3-cf27-4d8f-9cdb-3a23286e1b0b
There was a problem hiding this comment.
🟡 Changes recommended
CAP_NET_RAW still permits firewall bypass, and pre-0.8 Node SDK LXC configurations are rejected before execution.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
sdk/node/tests/integration/linux-process-container.test.ts:52
- The new validation makes the pre-0.8 branches in this suite fail before execution.
createConfigFromPolicyemits a legacynetwork.defaultPolicyfor 0.6/0.7 (even when the caller supplied no network policy), whileapplyLinuxNetworkPolicyaddsenforcementMode: 'firewall'only for host lists. LXC now rejects that omitted mode ascapabilities, so changing only the 0.8 outbound shape leaves every 0.6/0.7 case in this loop—including the non-network tests—broken. The LXC SDK builder needs to emit an accepted mode for synthesized legacy policies, or the backend must distinguish this synthesized default from an explicit capabilities request.
function outboundNetwork(version: (typeof supportedVersions)[number]) {
return version.compare('0.8.0-alpha') >= 0
? { egress: { default: 'allow' as const } }
: { allowOutbound: true };
- Files reviewed: 55/56 changed files
- Comments generated: 1
- Review effort level: Balanced
…licy A run that asks to preserve its policy and then hits a firewall failure partway through leaves iptables chains on the host. The rollback tries to remove them; when that also fails, nothing tries again, and the chains outlive the run. An ingress failure in the same circumstances cleans up after itself. Egress now marks the policy as preserved after the rules are applied rather than before, which is the order ingress already used. Teardown retries the leftover chains, and a preserved policy still survives a successful run. The distinction matters because preservePolicy is a request to keep a policy that is in force, not a request to keep the wreckage of one that never came into force. record_apply_outcome retains ownership of residual state for exactly this reason, and marking the policy preserved first disabled it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9cc60db3-cf27-4d8f-9cdb-3a23286e1b0b
There was a problem hiding this comment.
🔵 Needs a closer look
Filtered LXC workloads retain a CAP_NET_RAW bypass, and legacy reused containers can retain prior filesystem grants.
Review details
Suppressed comments (5)
Previously missed (2) — in code that hasn't changed since the last review.
sdk/node/tests/integration/linux-process-container.test.ts:209
- This test asserts the current implementation mechanism rather than the network contract. A compliant backend may keep an
eth0interface and enforce deny-all with a reachable firewall, soifaces=[lo ]would fail despite correct isolation; conversely, interface enumeration alone does not prove traffic is blocked. Probe reachability with a working allow-case control, as the new shell tests do, and assert loopback separately.
tests/scripts/run_lxc_network_enforcement_test.sh:219 - This failure message cites the 0.8 contract, but
lxc_network_enforcement_deny.jsondeclares schema 0.7.0-alpha. That will misdiagnose a loopback regression in this test; describe the LXC loopback invariant without attributing it to 0.8.
src/testing/wxc_e2e_tests/tests/e2e_lxc_network_capability.rs:59
- The new confinement check covers only
CAP_NET_ADMIN, but the firewall now hangs from the container namespace'sOUTPUTchain. An attached root workload that retainsCAP_NET_RAWcan inject AF_PACKET frames belowOUTPUTand bypass destination/port filtering entirely. Please dropCAP_NET_RAWfrom the bounding set as well and assert all three masks here; using a filtered-network fixture would also exercise the security boundary this test is intended to protect.
for field in ["CapEff:", "CapPrm:", "CapBnd:"] {
assert_eq!(
capability_mask(&status, field) & CAP_NET_ADMIN,
0,
"{field} still carries CAP_NET_ADMIN; the workload can rewrite the firewall confining it\n{status}"
src/backends/lxc/common/src/filesystem_mounts.rs:186
- This replacement mechanism cannot clean mounts written by the previous implementation. Earlier releases appended bare
lxc.mount.entrylines, whileset_filesystem_access_pointsremoves only entries inside the new MXC marker block, so upgrading and reusing an existing container preserves prior read-write grants indefinitely. Add a migration strategy (or recreate legacy containers) before relying on this call for policy tightening.
container.set_filesystem_access_points(&entries)?;
src/testing/wxc_e2e_tests/src/lib.rs:208
Command::output()returningOkonly means the process spawned; it may still exit unsuccessfully. In that case this helper reports the host ready and the E2E test fails later instead of skipping an unavailable LXC installation. Requireoutput.status.success()and report nonzero status as unavailable.
match Command::new("lxc-start").arg("--version").output() {
Ok(_) => true,
Err(_) => {
println!(
"SKIPPED: lxc-start not installed — this host cannot start a system container"
);
false
}
- Files reviewed: 55/56 changed files
- Comments generated: 0 new
- Review effort level: Balanced
The kernel version in the prerequisites was not sourced and was too high, so a reader on an older kernel would conclude LXC could not run there when it can. The floor is now the one LXC publishes, including the higher floor that running unprivileged requires, which is the other choice offered two lines below it. https://linuxcontainers.org/lxc/getting-started/ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9cc60db3-cf27-4d8f-9cdb-3a23286e1b0b
There was a problem hiding this comment.
🟡 Changes recommended
Retained CAP_NET_RAW permits an egress-filter bypass, while legacy SDK behavior, mount migration, and CI coverage remain incomplete.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (4)
Previously missed (2) — in code that hasn't changed since the last review.
src/testing/wxc_e2e_tests/tests/e2e_lxc_network_capability.rs:27
- This test is skipped in the normal Linux Rust-test lane because that lane does not install LXC, while the dedicated
.github/workflows/lxc-e2e.ymllane installs LXC but runs onlyrun_lxc_all_tests.sh, not this Rust test. Consequently the new capability invariant is never exercised on an LXC-capable CI host; add this test binary to that workflow or cover the same masks in the shell suite.
tests/scripts/run_lxc_all_tests.sh:126 - Registering these cases makes the strict LXC CI gate depend on TCP reachability to hard-coded GitHub address
140.82.114.6. The same suite explicitly quarantines its older public-network test as unreliable and recommends a local endpoint (run_lxc_all_tests.sh:92-107); an address rotation or runner egress outage will now fail several unrelated cases. Use the local network-namespace peer pattern fromrun_lxc_network_enforcement_test.shfor the new controls.
src/testing/wxc_e2e_tests/tests/e2e_lxc_network_capability.rs:59
- Checking only
CAP_NET_ADMINleaves the new OUTPUT-based firewall bypassable. LXC workloads run as root, andconfine_network_capabilitiescurrently drops only capability 12; retainedCAP_NET_RAWlets the workload send AF_PACKET frames below the namespace's OUTPUT hook. DropCAP_NET_RAWas well (from the bounding/effective/permitted sets) and assert both bits here, or use an enforcement point that packet sockets cannot bypass.
for field in ["CapEff:", "CapPrm:", "CapBnd:"] {
assert_eq!(
capability_mask(&status, field) & CAP_NET_ADMIN,
0,
"{field} still carries CAP_NET_ADMIN; the workload can rewrite the firewall confining it\n{status}"
sdk/node/tests/integration/linux-process-container.test.ts:52
- For schema 0.6/0.7 this still emits only
allowOutbound, which the SDK serializes as legacydefaultPolicy: allowwithoutenforcementMode. The new LXC validation rejects that request because the omitted mode defaults tocapabilities, so the 0.6 and 0.7 iterations of all three updated network tests fail whenever the network tests are enabled. Make the LXC SDK mapping emitfirewallfor legacy network policies (or expose an equivalent supported API) instead of only changing the 0.8 shape.
function outboundNetwork(version: (typeof supportedVersions)[number]) {
return version.compare('0.8.0-alpha') >= 0
? { egress: { default: 'allow' as const } }
: { allowOutbound: true };
- Files reviewed: 55/56 changed files
- Comments generated: 1
- Review effort level: Balanced
…bridge-netfilter Two files conflicted. `lxc_runner.rs` reads the caller's environment before applying the proxy variables. Main turned `ExecutionRequest::env` into an `Option` and added `env_entries()` for the backends that treat an absent and an empty environment alike, LXC among them, so the read moves to the accessor. The comment main carries above the `apply_proxy_env` call stays deleted, which is what this branch did to it. `lxc-backend.md` conflicted whole because this branch cut it to what a person needs to decide while main added a paragraph on v0.9 directional-only networking. The trimmed document is kept and that paragraph is carried into it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 05a694a4-da12-44bd-b724-fe4dcf773281
Drop CAP_NET_RAW alongside CAP_NET_ADMIN. A workload keeping it can send AF_PACKET frames, which never reach the OUTPUT chain the egress policy is installed on, so the capability test now covers bypass as well as reconfiguration and is named for both. Check the exit status of the `lxc-start --version` probe. Spawning the program said nothing about whether it ran. Recognize the mount entries an MXC from before the managed-mount marker block wrote. Those containers carry them as unmarked lines, and a tightened filesystem policy left them in place, restoring host access the current policy never granted. The five shapes the policy builder emits now have one definition that both the writer and the sweep read, so a new shape cannot be emitted without also being reclaimed. Entries in any other shape are the user's and still survive. Describe the reuse tests as what the runner does. It applies a policy by stopping a surviving container and starting it again, because LXC reads the network section only at start, so reuse of a still-running container is not reachable and the tests never covered it. Each now compares the init PID across the reused run and fails if it did not move, which is what proves the restart happened. Correct the LXC backend document. Its example declared no version and defaulted to the capabilities enforcement mode, which the backend rejects; port 53 is exempt only under a legacy deny with named hosts, not under the directional rules; and preserved egress chains live in the container's network namespace, so they do not outlive it. Rename the 0.7 control fixture to say 0.7. The compatibility test pins it at 0.7 on purpose and fails if it is not, but the file was named v08 after the suite that reads it. Two failures came from main rather than from review. Exact contract versions became authoritative, so two egress specs declaring a bare "0.7.0" no longer parsed; they now use the registered spelling their sibling already used. The corpus inventory also counts the fixtures this branch adds. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 05a694a4-da12-44bd-b724-fe4dcf773281
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 05a694a4-da12-44bd-b724-fe4dcf773281
…bridge-netfilter The corpus inventory conflicted again. Main removed one fixture while this branch adds twelve and removes one, so the count is main's 353 plus this branch's eleven. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 05a694a4-da12-44bd-b724-fe4dcf773281
There was a problem hiding this comment.
🟡 Changes recommended
Namespace enforcement has confirmed bypass and startup-isolation gaps, and its capability regression test is not exercised by LXC CI.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
src/backends/lxc/common/src/lxc_runner.rs:390
- The container is started with its configured interface before the OUTPUT policy is installed, and the code then waits for network readiness. On a reused container, the previous root workload can persist an init service that sends traffic during this unrestricted boot window, so a tightened policy can leak before the attached command runs. Install enforcement before networking/startup becomes active, or recreate the container for policy changes.
let _ = writeln!(logger, "Starting LXC container...");
if let Err(e) = container.start(network) {
if self.destroy_on_exit || container_created {
let _ = container.destroy();
}
return ScriptResponse::error(&format!("Failed to start container: {}", e));
}
let _ = writeln!(logger, "Container started successfully.");
let needs_network = needs_network(&request.policy);
if needs_network {
// Alpine DHCP leases can arrive at about nine seconds; thirty
// seconds leaves margin.
let timeout = Duration::from_secs(30);
if let Some(response) = self.enforce_network_readiness(
src/testing/wxc_e2e_tests/tests/e2e_lxc_network_capability.rs:71
- Checking only CAP_NET_ADMIN is insufficient for an OUTPUT-based firewall. The attached root workload still retains CAP_NET_RAW, so it can send Ethernet frames through an AF_PACKET socket and bypass the namespace's OUTPUT chain. Drop CAP_NET_RAW as well (or otherwise prohibit packet sockets) and extend this regression test accordingly.
// The kernel writes these masks; the workload cannot forge them. Effective
// alone would not settle it: a process raises a permitted capability into
// its effective set whenever it likes, and only a bounding-set drop
// survives execve.
for field in ["CapEff:", "CapPrm:", "CapBnd:"] {
assert_eq!(
capability_mask(&status, field) & CAP_NET_ADMIN,
0,
"{field} still carries CAP_NET_ADMIN; the workload can rewrite the firewall confining it\n{status}"
src/backends/lxc/common/src/lxc_bindings.rs:178
NoInterfaceoverrides onlylxc.net.0; anylxc.net.1(or later) entry in an existing container remains active. Because this runner reuses already-defined containers, a deny-all request can therefore retain a second network interface and bypass the intended isolation. Clear or override every configured network entry, or reject reused multi-interface containers before starting them.
StartNetwork::NoInterface => {
&["-s", "lxc.net.0.type=empty", "-s", "lxc.net.0.flags=up"]
}
- Files reviewed: 57/58 changed files
- Comments generated: 2
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Retained CAP_NET_RAW permits bypassing the new namespace OUTPUT firewall, and the capability E2E test is not executed on an LXC-capable CI host.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
src/testing/wxc_e2e_tests/tests/e2e_lxc_network_capability.rs:30
- This test currently never exercises LXC in CI. The Linux
wxc_e2e_testsjob runs on a host where LXC is intentionally absent, soready()returns false, while the LXC-capable workflow only invokestests/scripts/run_lxc_all_tests.shand never runs this Rust test binary. Wire this test into the LXC E2E workflow (or move the assertion into that shell suite) so regressions cannot pass through this early return.
#[test]
fn workload_cannot_reconfigure_the_network() {
if !ready() {
return;
}
src/backends/lxc/common/src/lxc_bindings.rs:158
- Dropping only
CAP_NET_ADMINdoes not protect a firewall hooked in the container'sOUTPUTchain. The attached workload still runs as root withCAP_NET_RAW, so it can send Ethernet frames through anAF_PACKETsocket; packet sockets bypass the IPOUTPUThook and can escape the allow/deny rules. DropCAP_NET_RAWas well and extend the capability E2E test to assert that bit is absent.
// `libc` does not export this; the value is from linux/capability.h.
const CAP_NET_ADMIN: libc::c_ulong = 12;
// SAFETY: `pre_exec` runs between fork and exec, where only
// async-signal-safe work is permitted. `prctl` is a bare syscall and this
// closure allocates nothing and captures nothing.
unsafe {
command.pre_exec(|| {
if libc::prctl(libc::PR_CAPBSET_DROP, CAP_NET_ADMIN, 0, 0, 0) != 0 {
- Files reviewed: 57/58 changed files
- Comments generated: 1
- Review effort level: Balanced
| "explicit divergence inventory and observed category totals differ" | ||
| ); | ||
| let expected_inventory = (353, 329, 14); | ||
| let expected_inventory = (364, 340, 14); |
There was a problem hiding this comment.
Gudge (@MGudgin) what is this?
…bridge-netfilter Main added two fixtures, so the corpus inventory is its 355 plus this branch's eleven. The migration inventory document keeps main's text. It records the version-specific parser migration, and this branch is not part of that story. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 05a694a4-da12-44bd-b724-fe4dcf773281
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 05a694a4-da12-44bd-b724-fe4dcf773281
There was a problem hiding this comment.
🟡 Changes recommended
LXC policies remain bypassable through retained capabilities, additional configured interfaces, and legacy stale mounts.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
src/testing/wxc_e2e_tests/tests/e2e_lxc_network_capability.rs:9
- Checking only
CAP_NET_ADMINis insufficient for the newOUTPUT-chain design. The attached workload runs as container root, and retainingCAP_NET_RAWlets it emitAF_PACKETframes that bypass the IPOUTPUThook entirely. The production confinement and this E2E assertion must also drop/checkCAP_NET_RAW, or filtered LXC policies remain bypassable.
const CAP_NET_ADMIN: u64 = 1 << 12;
src/backends/lxc/common/src/filesystem_mounts.rs:185
- The replacement only removes entries inside the new managed markers. Containers reused from an older MXC build have MXC-created
lxc.mount.entrylines without those markers, so a first post-upgrade run that removes a prior rw/ro grant still inherits that stale mount. Refuse/recreate legacy containers or provide a migration that distinguishes and removes the old MXC entries before applying the new policy.
container.set_filesystem_access_points(&entries)?;
- Files reviewed: 56/57 changed files
- Comments generated: 1
- Review effort level: Balanced
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 05a694a4-da12-44bd-b724-fe4dcf773281
There was a problem hiding this comment.
🟡 Changes recommended
CAP_NET_RAW still permits an AF_PACKET bypass of the new OUTPUT-chain enforcement, and one SDK test does not exercise its claimed wire shape.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
sdk/node/tests/integration/linux-process-container.test.ts:202
- This does not actually send an omitted-network request.
runLxccallscreateConfigFromPolicy, whose no-network branch emitsnetwork: { defaultPolicy: 'block' }(sdk/node/src/sandbox.ts:500-503), so the assertion exercises the legacy 0.8 shape instead of the omitted-section path described by the test. Build the config here, deleteconfig.network, and then invokespawnFromConfigAsyncso this regression test covers the intended wire shape.
- Files reviewed: 58/59 changed files
- Comments generated: 1
- Review effort level: Balanced
| use serde_json::json; | ||
| use wxc_e2e_tests::{has_lxc_host, has_platform_exec, run_platform_config_value}; | ||
|
|
||
| const CAP_NET_ADMIN: u64 = 1 << 12; |
This PR branches from the traditional PR template because of hoe many times the reason for this PR has changed over the past 2 weeks.
tl;dr copilot added bugs, then did its best to work around those bugs. Additionally the code quality and comment quality is also horrible.
In the Deny-All-Except-Proxy PR, this is when the story starts. CoPilot, in its vanilla fashion, made the smallest, local change to get LXC to deny-al-except proxy. The smallest change was "Get the interface, but then take off all capabilities". This change turned the github pipeline red because MXC would not give the LXC container a network yet LXC needed the network to take away the capabilities.
CoPilots fix? Add a set to the github pipeline to re-add br_netfilter. This made the github pipeline green. However, CoPilot did not change the ADO pipeline. The ADO pipeline stayed red. Which is how this whole bug started. Get the ADO pipeline green.
Since two weeks ago CoPIlot and I have substantially improved LXC in so many areas.
a. This breaks nodeSDK tests because the nodeSDK disallows users setting enforcmentMode and the SDK defaults to capabilities.
This is tested with using the full E2E suite that LXC offers.