Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
155 commits
Select commit Hold shift + click to select a range
fabdfd0
[LXC] Implement StatefulSandboxBackend for LXC (state-aware lifecycle…
dhoehna Jul 10, 2026
0853f9c
[LXC] Address PR #633 review: collision-free container names + narrow…
dhoehna Jul 13, 2026
9764b1a
Add LXC state-aware provision sample config (tests/configs/lxc_state_…
dhoehna Jul 15, 2026
af38cc1
Merge microsoft/mxc main (47a172c) into lxc-state-aware-lifecycle
dhoehna Jul 15, 2026
5a138bc
ci: re-trigger PR build (empty commit)
dhoehna Jul 15, 2026
f84cf2b
Address PR #633 review: LXC state-aware lifecycle fixes
dhoehna Jul 16, 2026
dd29002
Merge upstream/main (d271ac4) into lxc-state-aware-lifecycle
dhoehna Jul 17, 2026
91ebaf7
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-s…
dhoehna Jul 20, 2026
7ad03df
Merge upstream/main into user/dahoehna/lxc-state-aware-lifecycle
dhoehna Jul 27, 2026
287b918
[LXC] Address PR #633 review: egress direction, fail-open start, SDK …
dhoehna Jul 30, 2026
95a197d
[LXC] Share state-aware telemetry orchestration between the wxc and l…
dhoehna Jul 30, 2026
783c197
[LXC] Add the missing state-aware lifecycle test and run it from the …
dhoehna Jul 30, 2026
c453787
[LXC] Do not treat the default network policy as an explicit restriction
dhoehna Jul 30, 2026
c9001a3
Merge upstream/main into lxc-state-aware-lifecycle
dhoehna Jul 30, 2026
d287e49
Remove accidentally committed SDK build output and node_modules
dhoehna Jul 30, 2026
a39e1cc
Restore LF line endings in wxc/src/main.rs
dhoehna Jul 30, 2026
15daa8f
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-s…
dhoehna Aug 4, 2026
1832e43
LXC: clear only MXC-added bind mounts, not baseline entries
dhoehna Aug 4, 2026
cdb8344
LXC: make firewall chain name collision-free via a name hash
dhoehna Aug 4, 2026
5c03c13
LXC: remove FORWARD hook on teardown regardless of veth discovery
dhoehna Aug 4, 2026
d8be903
LXC: install the firewall before the container starts (fail closed)
dhoehna Aug 4, 2026
44929f0
LXC: route exec error envelope to stderr so it can't corrupt the stream
dhoehna Aug 4, 2026
d699f9e
LXC test script: make cleanup idempotent so it runs once per run
dhoehna Aug 4, 2026
cd34a4f
docs(sdk): add LXC to the state-aware backend list, note streaming-ex…
dhoehna Aug 4, 2026
e3e657a
LXC: enforce an explicit default network policy (item 6)
dhoehna Aug 4, 2026
9c055f2
LXC: add chain-name collision-freedom spec tests
dhoehna Aug 4, 2026
6d712e3
LXC: fix three evidence-reviewer findings from e3e657a
dhoehna Aug 4, 2026
cb62636
Fix chain-name collision from 32-bit hash truncation (PR 633 review)
dhoehna Aug 4, 2026
adf8e03
docs: correct the 36^11 modulus value in a comment
dhoehna Aug 4, 2026
dbac069
Correct the chain-hash work-factor claim
Aug 4, 2026
bf78432
Correct MAX_CONTAINER_NAME_LEN comment: chain names are not collision…
dhoehna Aug 5, 2026
204bff4
Address review feedback on the state-aware lifecycle
Aug 6, 2026
d7e3bc9
Correct four defects an independent review found in the previous commit
dhoehna Aug 6, 2026
d9ca600
[LXC] Report a timeout that killed a script, and give back chain owne…
dhoehna Aug 7, 2026
2cbaa25
[LXC] Bound the pty drain on silence, not on the caller's whole budget
dhoehna Aug 7, 2026
f8662d7
[LXC] Stop the container before a signal removes its firewall
dhoehna Aug 7, 2026
7813ae9
Scope the stderr error-envelope fallback to LXC
dhoehna Aug 7, 2026
6a154bf
Make a dry-run start answer the question it was asked
dhoehna Aug 7, 2026
e65f65b
Do not unfilter a container that is still running
dhoehna Aug 7, 2026
5449775
Tear down through the manager that owns the chain
dhoehna Aug 7, 2026
5adda93
Require a firewall enforcementMode when the LXC policy restricts
dhoehna Aug 7, 2026
53ac40e
Merge upstream/main (PR #724 dual-stack IPv6) into lxc-state-aware-li…
dhoehna Aug 7, 2026
51c26e4
Merge upstream/main into the state-aware lifecycle branch
dhoehna Aug 12, 2026
2658d07
Drop the IsolationSessionUserConfig import the merge should not have …
dhoehna Aug 12, 2026
c0bc56f
Normalize line endings to LF on the state-aware lifecycle branch
dhoehna Aug 13, 2026
5dd22f4
Merge upstream/main (#810 WSLc per-phase SDK configs) into the state-…
dhoehna Aug 13, 2026
bb5895a
Address Copilot review: fix docs, trap handling, and drop a dead test…
dhoehna Aug 14, 2026
816851b
Add the lxc arm to the state-aware probe and correct the README
dhoehna Aug 14, 2026
3b8fdf5
Merge upstream/main into user/dahoehna/lxc-lifecycle-current
dhoehna Aug 14, 2026
39480f4
Address suppressed review comments: skip probe, cleanup retry, stale …
dhoehna Aug 14, 2026
a481f3d
Fail closed on an unreadable FORWARD, and rewrite lxc config atomically
dhoehna Aug 14, 2026
fd60d0d
Enforce the inbound half of the network policy in state-aware start
dhoehna Aug 14, 2026
4e5933d
Stop the LXC lifecycle paths from failing open on teardown and on races
dhoehna Aug 14, 2026
40cf877
Trust the right stream for LXC errors, and let empty host lists through
dhoehna Aug 14, 2026
9462cb6
Reap an exec's container processes when its timeout fires
dhoehna Aug 14, 2026
2f05fc5
Do not read a failed existence probe as an absent container
dhoehna Aug 14, 2026
7cc27b5
Close three fail-open paths the previous round opened
dhoehna Aug 14, 2026
48657f7
Lock the whole LXC lifecycle, not just start
dhoehna Aug 14, 2026
86a262a
Keep teardown idempotent when there is no LXC root
dhoehna Aug 14, 2026
15a25b2
Take the lifecycle lock in provision too
dhoehna Aug 14, 2026
f83c5ee
Close the remaining fail-open probes and make the mount set atomic
dhoehna Aug 14, 2026
706ce82
Derive each path once so the probe and the reclaim cannot drift
dhoehna Aug 14, 2026
b6e2480
Make each fact have one answer, and stop claiming more than we enforce
dhoehna Aug 14, 2026
b22589f
Say plainly that the exec reaper is hygiene, not containment
dhoehna Aug 14, 2026
c9b0833
Only a missing tool means absent, and only a stopped container is saf…
dhoehna Aug 14, 2026
24b0bfd
Say exactly which network configs a firewall-enforced start refuses
dhoehna Aug 14, 2026
5b12c72
Send containment only on provision, so the lifecycle script can run a…
dhoehna Aug 14, 2026
a2c55c3
Say that allowLocalNetwork is rejected, not silently ignored
dhoehna Aug 14, 2026
2523b98
Merge upstream/main into the state-aware LXC lifecycle branch
dhoehna Aug 14, 2026
0016410
Refuse a Library exec on LXC before the workload runs
dhoehna Aug 14, 2026
c77937b
Pin the state-aware LXC test to a published Alpine release
dhoehna Aug 14, 2026
a551ce2
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-l…
dhoehna Aug 15, 2026
7d3e2e0
[LXC] Enforce the network mode rule at start instead of in the SDK types
dhoehna Aug 16, 2026
c4a87aa
[LXC] Cover the network presence bit end to end on a real container
dhoehna Aug 16, 2026
1470f15
[LXC] Pin filesystem_specified with a case that can actually fail
dhoehna Aug 16, 2026
8fecff7
[LXC] Ask liblxc for the interface list instead of parsing the config
Aug 17, 2026
579d486
[LXC] Assert the default-deny hook is applied, not just reported
Aug 17, 2026
de90b20
[LXC] Drop the lxc.include refusal from the contract
Aug 17, 2026
79ab7ab
[LXC] Require the physdev hook the bridged veth actually uses
dhoehna Aug 17, 2026
bdd8214
[LXC] Drop the stale lxc.include refusal from the repo guidance
dhoehna Aug 17, 2026
b02e731
[LXC] Pin the netdev type named "empty" against the absence signal
dhoehna Aug 17, 2026
825334a
[LXC] Follow the sole interface's index instead of requiring lxc.net.0
dhoehna Aug 17, 2026
1fbc2a9
[LXC] Refuse a network config that moved while it was being read
dhoehna Aug 17, 2026
e140496
[LXC] Recheck the located index, not just the interface summary
dhoehna Aug 17, 2026
d6984f1
[LXC] Enforce the network policy without depending on the interface i…
dhoehna Aug 17, 2026
56fccba
[LXC] Close three teardown gaps an independent review found
dhoehna Aug 17, 2026
ae24c28
[LXC] Ask the container which name its interface has, and re-read bef…
dhoehna Aug 17, 2026
7c452f4
[LXC] Cover every start-phase policy field with a roadmap E2E case
dhoehna Aug 17, 2026
1ced1fa
[LXC] Gate the filesystem phase on the path lists alone
dhoehna Aug 18, 2026
b78cb95
Enforce LXC network policy from the policy, not from enforcementMode
dhoehna Aug 18, 2026
c54733c
Inline the last-line envelope parse at its only call site
dhoehna Aug 18, 2026
b9a6e81
Put the LXC error envelope on stdout like every other backend
dhoehna Aug 18, 2026
c4deacf
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-l…
dhoehna Aug 18, 2026
f9c9352
Drop enforcementMode from LxcNetworkConfig and fix two stale doc comm…
dhoehna Aug 19, 2026
cfff8cd
Enable bridge netfilter on the SDK integration Linux lanes
dhoehna Aug 19, 2026
0ef10b6
Wire experimental.lxc into the generated config schema
dhoehna Aug 19, 2026
669c80e
Merge upstream/main into lxc-experimental-wire
dhoehna Aug 19, 2026
b5d5cd6
Trim LXC runner comments to one-sentence reasons
dhoehna Aug 20, 2026
712ece5
Install the LXC egress firewall before the container starts
dhoehna Aug 20, 2026
e43e48b
Restore CRLF line endings in lxc_runner.rs
dhoehna Aug 20, 2026
e2419a6
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-l…
dhoehna Aug 20, 2026
e9db1c6
[LXC] Halt a reused container when a post-start step fails
dhoehna Aug 20, 2026
a492906
[LXC] Restore the Linux gate on run_watchdog
dhoehna Aug 20, 2026
0289a92
[LXC] Correct what the pty drain comment claims about the abandoned t…
dhoehna Aug 20, 2026
78907be
Merge main into the LXC state-aware lifecycle branch
dhoehna Aug 21, 2026
82e42a7
Merge main into the LXC state-aware lifecycle branch
dhoehna Aug 21, 2026
c74967c
[LXC] Keep the sandbox ID when deprovision fails in the network test
dhoehna Aug 21, 2026
6387371
[LXC] Move requires_firewall out of the shared policy model
dhoehna Aug 21, 2026
5ea1bc8
Merge main into the LXC state-aware lifecycle branch
dhoehna Aug 21, 2026
1e99ca1
Merge upstream/main into the state-aware LXC branch
dhoehna Aug 25, 2026
40dbde7
Align the state-aware matrix with main's enforcement split
dhoehna Aug 25, 2026
3c2bb9d
Restore LF line endings in two LXC network files
dhoehna Aug 25, 2026
18ff7be
Reject a foreign `experimental.lxc` section instead of dropping it
dhoehna Aug 25, 2026
9321b87
Abort the network matrix when a sandbox fails to deprovision
dhoehna Aug 25, 2026
abfc110
Correct the `LxcNetworkConfig` note on when enforcement happens
dhoehna Aug 25, 2026
476840e
Enforce the network policy LXC's state-aware SDK accepts
dhoehna Aug 25, 2026
d0d059f
Describe state-aware schema defaults as per-backend
dhoehna Aug 25, 2026
85cdbc1
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-l…
dhoehna Aug 26, 2026
2e73408
[LXC] Gate state-aware on schema 0.8.0 and report observable sandbox …
dhoehna Aug 28, 2026
90c8b63
Report a missing LXC runtime as backend_unavailable
dhoehna Aug 28, 2026
9d3f721
Cover the LXC identifier and adopt-or-create contract
dhoehna Aug 28, 2026
bec5bb8
Block the container-to-host path when hostLoopback is denied
dhoehna Aug 28, 2026
b3ae8ff
Cover the timeout, phase-ordering, and provision-field contract cases
dhoehna Aug 28, 2026
485e151
Probe a literal address in the GA egress cases
dhoehna Aug 29, 2026
328fa33
Wait longer for a container to acquire an address
dhoehna Aug 29, 2026
d22acce
Revert "Wait longer for a container to acquire an address"
dhoehna Aug 29, 2026
6baa21a
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-l…
dhoehna Aug 29, 2026
70c4006
Format the merge-resolved call site
Aug 31, 2026
c85ce37
Merge upstream/main into the state-aware lifecycle branch
dhoehna Aug 31, 2026
bac44e6
Recognize LXC as a state-aware containment in the .NET SDK
dhoehna Aug 31, 2026
4576b4d
Let LXC state-aware validation tests run on a host without LXC
dhoehna Aug 31, 2026
47d661c
Refuse a proxy under a non-firewall mode in one place instead of two
dhoehna Aug 31, 2026
7f6edf5
Point the inbound tamper note at the issue that still tracks it
dhoehna Sep 1, 2026
c8cf6ef
Refuse a proxy under capabilities before the container exists
dhoehna Sep 1, 2026
46cb91c
Enforce the state-aware schema floor for every LXC caller
dhoehna Sep 1, 2026
f748778
Keep clearPolicyOnExit out of the LXC start type
dhoehna Sep 1, 2026
805afe2
Let a signal delete the container's return-path rules
dhoehna Sep 1, 2026
de6ae00
Keep filtering on a container that would not die
dhoehna Sep 1, 2026
899de23
Let the reaper report a process it could not kill
dhoehna Sep 1, 2026
27ab29c
Keep the inbound chain when a container will not die
dhoehna Sep 1, 2026
a3f77ab
Clean up the failed create by name instead of by sweep
dhoehna Sep 1, 2026
442639b
Restore the CRLF endings the repo declares for the guidance file
dhoehna Sep 1, 2026
12b7193
Merge upstream/main into the state-aware LXC branch
dhoehna Sep 1, 2026
07e3c10
Format the LXC runner and bindings the way rustfmt wants them
dhoehna Sep 1, 2026
86e5587
Let the version-floor test pass on a host with no LXC installed
dhoehna Sep 1, 2026
553fe21
Give the failed-create control a container name the backend will accept
dhoehna Sep 1, 2026
266765c
Stop the provision-fields cleanup from failing the run it just passed
dhoehna Sep 1, 2026
3db4570
Refuse a mistyped destination or environment entry instead of warning
dhoehna Sep 1, 2026
53df405
Refuse network and environment settings LXC cannot enforce
dhoehna Sep 1, 2026
8c92694
Correct the LXC document where it still describes silent skipping
dhoehna Sep 1, 2026
a24a3be
Store LXC Rust sources with LF so an edit cannot rewrite the whole file
dhoehna Sep 1, 2026
87ad5e3
Refuse a proxy LXC cannot enforce on a dry run too
dhoehna Sep 2, 2026
37b57b9
Gate LXC state-aware behind --experimental
dhoehna Sep 3, 2026
6b99512
Merge remote-tracking branch 'upstream/main' into user/dahoehna/lxc-l…
dhoehna Sep 3, 2026
ec0ba3a
Follow the shared exec-stdio rename in the LXC state-aware backend
dhoehna Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .azure-pipelines/templates/SDK.Integration.Test.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ jobs:
sudo apt-get install -y -qq lxc lxc-utils dnsmasq-base iptables bubblewrap slirp4netns
displayName: Install LXC, Bubblewrap, and slirp4netns

# LXC enforces network policy from the policy itself, and the default is
# deny-all, so every LXC run installs an egress chain even when the
# request carries no network section. A bridged veth only reaches
# FORWARD while br_netfilter delivers bridged packets to iptables, and
# without it the backend refuses to report success for a policy it
# cannot enforce. Tolerated rather than required, because a hosted pool
# may forbid loading modules; when it does, the LXC tests fail exactly
# as they already would.
- script: |
sudo modprobe br_netfilter || echo "br_netfilter unavailable; LXC policy enforcement is unreachable on this pool"
sudo sysctl -w net.bridge.bridge-nf-call-iptables=1 || true
sudo sysctl -w net.bridge.bridge-nf-call-ip6tables=1 || true
displayName: Enable bridge netfilter

- script: sudo MXC_SKIP_LXC_NETWORK_TESTS=1 MXC_DEBUG=${{ parameters.debug }} npm test
workingDirectory: $(integrationDirectory)
displayName: npm test (sudo, with LXC)
Expand Down
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
*.sh text eol=lf
.github/copilot-instructions.md whitespace=cr-at-eol
.github/workflows/*.lock.yml linguist-generated=true
# Rust sources under the LXC backend are stored LF; editors on Windows write CRLF
# and silently re-commit the whole file as a rewrite.
src/backends/lxc/**/*.rs text eol=lf
6 changes: 3 additions & 3 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

93 changes: 79 additions & 14 deletions docs/lxc-support/lxc-backend.md

Large diffs are not rendered by default.

266 changes: 266 additions & 0 deletions docs/lxc-support/state-aware-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
# MXC LXC Backend β€” State-Aware

The per-backend document required by Β§11.6 of the state-aware sandbox API
specification. It declares what the LXC backend does at each lifecycle phase:
the config and metadata shapes, the cross-cutting policy honor matrix,
mode-specific fields, idempotence behavior, the concurrency story, and the
error mapping.

`docs/lxc-support/lxc-backend.md` describes the shipped implementation. This
document declares the contract that implementation meets.

## Scope

### In scope

The five state-aware phases β€” provision, start, exec, stop, and deprovision β€”
as reached through `lxc-exec` with a state-aware request.

### Out of scope

One-shot execution, covered by `docs/lxc-support/lxc-backend.md`. The
cross-backend policy semantics, covered by `docs/sandbox-policy/0.8.0/`.

## Schema version floor

State-aware requires a `schema_version` of at least `0.8.0`. A request below
that floor is refused before any phase runs, and the sandbox is never created.
One-shot execution has no such floor and continues to accept `0.7.0`.

The floor exists because LXC's state-aware support missed the 0.7.0 cutoff.

## Experimental opt-in

The LXC state-aware lifecycle is experimental. Every call to a lifecycle phase must pass `--experimental` to `lxc-exec`. Without it every phase is refused with `backend_unavailable` before any container is touched. One-shot execution has no such requirement.

## Per-phase config and metadata shapes

### Provision

**Config β€” `LxcConfig`.** Both fields are required.

| Field | Type | Meaning |
|---|---|---|
| `distribution` | string | Linux distribution for the container rootfs, for example `alpine` or `ubuntu`. |
| `release` | string | Distribution release version, for example `3.20` or `24.04`. |

**Metadata β€” `LxcProvisionMetadata`.**

| Field | Type | Meaning |
|---|---|---|
| `containerName` | string | The LXC container name backing this sandbox. |
| `created` | bool | True when this call created the container, false when it adopted an existing one. |

#### The `sandboxId` format

Provision returns `lxc:<containerName>`. Every later phase parses that form
and rejects anything else as `malformed_id` β€” a missing or foreign prefix, an
empty name, or a name longer than the LXC limit.

### Start

**Config β€” none.** Start is the only phase that accepts cross-cutting policy;
see the honor matrix below.

**Metadata β€” none.**

### Exec

**Config β€” the process fields of the request**, matching one-shot execution.

**Metadata β€” none.** LXC relays the sandbox's output to this process's own
stdout and stderr rather than handing back pipes. The returned handle carries
null stream handles and a waiter holding the exit code.

An in-process caller that needs the streams back is refused up front, before
the workload runs, because a later refusal would describe output that has
already gone somewhere the caller never asked for.

### Stop

**Config β€” none. Metadata β€” none.**

### Deprovision

**Config β€” none. Metadata β€” none.**

## Cross-cutting policy honor matrix

Per Β§10.3. LXC applies filesystem and network policy at start and refuses it
everywhere else.

| Field | provision | start | exec | stop | deprovision |
|---|---|---|---|---|---|
| `filesystem` | `policy_validation` | applied | `policy_validation` | `policy_validation` | `policy_validation` |
| `network` | `policy_validation` | applied | `policy_validation` | `policy_validation` | `policy_validation` |
| `ui` | ignored | ignored | ignored | ignored | ignored |

The two fields differ on an empty block. A `network` block that is present
but sets nothing is refused β€” the check reads the "was a network section
supplied" bit, not the fields inside it. A `filesystem` block that is present
but lists no paths is **not** refused; the check reads only whether a path
list is non-empty, and an empty block is indistinguishable from an absent one.

A request that carries no policy at all passes every phase.

The rejection is deliberate rather than incidental. A container's filesystem
mounts and firewall rules are installed when it starts, and a caller that
passed them to stop or exec would receive a silent no-op instead of the
isolation they asked for.

## Mode-specific fields

### `containerId`, state-aware provision only

LXC accepts an optional `containerId` at provision, naming the container to
adopt instead of minting a fresh name. This is the exception to the rule in
`mxc-state-aware-sandbox-api-overview.md` that neither request shape carries
a container identifier, and it is called out in the specification at Β§6.1.

When `containerId` names an existing container, provision adopts it and
returns `created: false`. When it names one that does not exist, provision
creates it under that name and returns `created: true`.

## Idempotence per phase

| Phase | Repeated call | Notes |
|---|---|---|
| provision | non-idempotent without `containerId` | Each call mints a fresh container name. With `containerId`, a repeated call adopts the container the first one created and reports `created: false`. |
| start | `already_started` | A second start against a running container is refused. |
| exec | per-call | Each exec runs the command again. There is no deduplication. |
| stop | `already_stopped` | A stop against a container that is not running is refused, whether it was never started or has already been stopped. |
| deprovision | idempotent success | A second deprovision against a destroyed sandbox exits 0. |

### Why stop reports one code for two histories

LXC answers "is this container running" with a single probe, and that probe
cannot distinguish a container that was never started from one that ran and
has since stopped. Reporting one code for one observable state keeps the
answer honest. `not_started` is reserved for exec, the only other phase that
needs a running container. Each code is emitted by exactly one phase.

### Why deprovision is the one idempotent phase

Deprovision runs the authoritative network teardown whether or not the
container still exists. A caller that retries after a failure partway through
teardown has to be able to reach that cleanup a second time. Every other
phase reports `stale_id` against a destroyed sandbox, provided the request
carries nothing that fails earlier β€” see Precedence.

### Stopping a container also tears down its network

A container that exits on its own leaves the current start's iptables rules on
the host. Stop removes them on both paths β€” before returning success for a
container it stopped, and before returning `already_stopped` for one that was
already down. A refusal never leaves host state behind.

## Concurrency

### Multiple sandboxes

Distinct `sandboxId`s name distinct LXC containers, and their lifecycle locks
are independent. Their firewall chains are not guaranteed independent; see
Known issues.

### The same sandbox

Provision, start, stop, and deprovision take a lifecycle lock keyed on the
container name. Concurrent calls against one sandbox serialize rather than
interleave, and the second caller observes the state the first one left.

### Concurrent exec calls

Exec does not take the lifecycle lock. Two execs against one sandbox run
concurrently inside the same container, and MXC does not order them. A caller
that needs them ordered has to sequence them itself.

## Error mapping

LXC emits nine of the twelve codes in the specification's closed union.

| Observable condition | Wire `error.code` | Trigger |
|---|---|---|
| Caller did not pass `--experimental` | `backend_unavailable` | The state-aware lifecycle is experimental. Reported by all five phases before any container is touched. |
| Provision config missing or incomplete | `malformed_request` | No `experimental.lxc.provision` block, or an empty `distribution` or `release`. |
| The `sandboxId` does not parse | `malformed_id` | Missing or foreign prefix, empty name, a name over 20 characters, or a character outside ASCII alphanumerics, `-`, and `_`. Detected before any container is touched. |
| The id parses, the container does not exist | `stale_id` | The sandbox was deprovisioned, or the container was destroyed out of band. Reported by start, exec, and stop. |
| The container exists but is not running, at exec | `not_started` | Exec needs a running container. |
| The container is already running, at start | `already_started` | |
| The container is not running, at stop | `already_stopped` | See the idempotence table. |
| A phase received policy it does not accept | `policy_validation` | See the honor matrix. Also covers a network policy LXC cannot enforce on this host. |
| A container state probe could not be read | `backend_error` | LXC could not answer whether the container exists or is running. An unreadable probe is never treated as "gone" or "stopped". Also returned when an in-process caller asks exec for the sandbox's streams, which LXC cannot hand back. |
| The `lxc-*` tools are absent from the host | `backend_unavailable` | LXC drives every phase by spawning those tools. Reported by all five phases before any container is touched, which separates a machine without LXC installed from a container operation that failed. |

### Precedence

Every phase runs in two stages, and the first stage never touches the
container. Validation checks the provision config shape, parses the
`sandboxId`, and rejects policy the phase does not accept. Only then does the
phase itself acquire the lifecycle lock and probe the container.

What follows from that split is the part callers can rely on:

- `malformed_request`, `malformed_id`, and a *phase-acceptance*
`policy_validation` are reported before any state code. An exec, stop, or
deprovision against a destroyed sandbox that also carries rejected policy
reports `policy_validation`, not `stale_id`.
- Within the second stage there is no single ordering. The lifecycle lock is
taken before the existence probe, and failing to take it reports
`backend_error`. Start applies filesystem policy after its existence and
running probes, and a failure there also reports `policy_validation`.

`policy_validation` and `backend_error` each have two arrival points, one in
each stage. Neither code identifies which stage produced it; the message
does.

### The remaining three codes

None of these is emitted by LXC backend code. Two of the three still reach a
caller of `lxc-exec`, because shared routing decides them before any backend is
chosen.

**`not_provisioned` is unreachable.** The specification reserves it for a
sandbox whose id is recognized but whose resources have not been created. LXC
has no such state β€” `lxc-create` runs during provision, and the container is
defined from the moment an id exists. Every condition that would reach
`not_provisioned` on another backend reaches `stale_id` here.

**`unsupported_containment` answers a sandbox id naming no known backend.**
Shared routing returns it for a prefix with no entry in the lookup table, which
is the outcome the specification assigns to a direct wire call. An id carrying
the `lxc` prefix routes to this backend instead.

**`unsupported_phase` comes from shared dispatch**, above the backend, never
from LXC code. A caller reaches it by naming a backend that has no state-aware
lifecycle on this host.

## Known issues

### Firewall chain names are not guaranteed unique

A container's iptables chain name is derived from its container name through a
non-cryptographic hash. That derivation is not injective, and a caller that
chooses `containerId` can construct two names that land on one chain. When
that happens, one sandbox's stop or deprovision tears down the other's rules,
leaving a running container with no firewall.

The name length limit and character set narrow the input set; they do not make
the derivation injective. The durable fix is persisted chain ownership
verified before any flush or delete. Tracked in AB#62953349.

## References

- `docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md` β€” the
specification this document declares against.
- `docs/lxc-support/lxc-backend.md` β€” the shipped LXC implementation.
- `docs/sandbox-policy/0.8.0/` β€” the cross-backend policy contract.
- `tests/scripts/run_lxc_state_aware_phase_ordering_test.sh` β€” the phase
ordering and error codes above.
- `tests/scripts/run_lxc_state_aware_network_test.sh` β€” the honor matrix.
- `tests/scripts/run_lxc_state_aware_version_floor_test.sh` β€” the schema
version floor.
- `tests/scripts/run_lxc_state_aware_routing_test.sh` β€” the refusals decided
from the sandbox id alone, the missing runtime dependency, and the envelope
discipline stdout has to keep.
- `tests/scripts/run_lxc_state_aware_adopt_test.sh` β€” the identifier form and
adopt-or-create.
52 changes: 51 additions & 1 deletion docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,27 @@ wire format and have different roles:
| Field | Where it appears | Source | Purpose |
|---|---|---|---|
| `sandboxId` | State-aware wire envelope (Β§7); SDK return value from `provisionSandbox` | System-generated by the backend | Opaque routing identifier; must be passed to subsequent state-aware calls |
| `containerId` | One-shot wire envelope (per `docs/schema.md`) | Caller-supplied (or auto-generated random hex) | Human-readable label, used as e.g. AppContainer profile name |
| `containerId` | One-shot wire envelope (per `docs/schema.md`); **LXC** state-aware `provision` | Caller-supplied (or auto-generated random hex) | Human-readable label, used as e.g. AppContainer profile name or LXC container name |

State-aware non-provision calls carry `sandboxId` on the request; provision returns it
on the response. A state-aware request **may** also carry `containerId` β€” the parser
preserves it into the request the backend receives β€” but it is inert for backends that
do not use it as a label, and it is never a routing key on the state-aware path.
One-shot calls carry `containerId` (when present); they do not carry `sandboxId`.

**LXC exception.** LXC is the one backend whose state-aware `provision` also accepts
`containerId`, because its durable state *is* the named container and `lxc-ls` /
`lxc-attach` address it by name. Consequences callers must understand:

- The returned `sandboxId` is `lxc:<containerName>`, so the name is not opaque.
- Provision is **adopt-or-create**: if a container with that name already exists it is
reused rather than created, and `provisionMetadata.created` reports which happened.
- Deprovision **destroys the container unconditionally**, including one it adopted
rather than created. MXC retains no caller-side state between phases, so deprovision
cannot distinguish an adopted container from a created one. Callers that pass a
`containerId` for a pre-existing container therefore hand MXC ownership of it.
- Omit `containerId` to get a generated name and avoid adopting anything.

## 6. TypeScript SDK

The SDK adds five new functions, exported from `@microsoft/mxc-sdk` alongside the existing
Expand Down Expand Up @@ -1687,6 +1700,43 @@ unconditionally by the in-guest agent).
| `network` | rejected | rejected | rejected | rejected | rejected |
| `ui` | rejected | rejected | rejected | rejected | rejected |

For LXC, filesystem path lists and `network` are applied at **start** β€” the container is
created empty at provision and its mounts and iptables chain are installed just before
it runs β€” and rejected at every other phase. Only a *non-empty* path list counts as a
filesystem policy, so a `filesystem` block whose lists are all empty is accepted at any
phase. `ui` is not consulted anywhere in the LXC backend; `LxcStartConfig` does not
expose it, so SDK callers cannot pass it, but a raw-JSON caller that sends it is not
rejected.

| Field | provision | start | exec | stop | deprovision |
|---|---|---|---|---|---|
| `filesystem` (non-empty path lists) | rejected | applied | rejected | rejected | rejected |
| `network` | rejected | applied | rejected | rejected | rejected |
| `ui` | ignored | ignored | ignored | ignored | ignored |

> **LXC `network` constraints.** `network.proxy` and `allowLocalNetwork` are
> rejected at start. LXC enforces network policy through iptables whenever
> `defaultPolicy` is `"block"`, either host list is non-empty, or proxy is
> enabled. The value of `enforcementMode` is accepted but ignored by LXC; it
> does not disable enforcement. The veth is not discovered after start: the name
> is derived from the container name and pinned by a container-global
> `lxc.hook.start-host` installed before start, so the chain is scoped before the
> interface exists. The hook resolves the container's peer interface from
> `$LXC_PID` and renames it to that name; if it cannot find one it exits
> nonzero, which aborts the start rather than leaving the container unfiltered.
> The interface set comes from liblxc rather than from the container's own
> config file, so an `lxc.include` that declares interfaces elsewhere is
> resolved rather than guessed at, and no interface index is read at all β€” a
> container numbering its only interface `lxc.net.3` is enforced exactly as one
> using `lxc.net.0`, as is one at any index liblxc accepts. Start fails instead
> when that pin cannot be trusted to cover the container's traffic β€” when the
> container declares anything other than exactly one interface, or does not
> declare that interface's type as `veth`. An undeclared type is refused
> alongside a wrong one, because absence is not evidence of a veth, and a
> `macvlan` or `phys` interface would take a hook pinned to a veth name that
> never appears while its traffic ran unfiltered. `removeRulesOnExit` is not
> part of the LXC surface (see `LxcNetworkConfig`).

- **Compile-time enforcement at the SDK.** Each per-(backend, phase) Config (Β§6.1)
declares only the cross-cutting fields the matrix marks as `applied` for that phase
*and* that the runtime currently honors. TypeScript rejects callers passing fields
Expand Down
Loading
Loading