Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions .github/workflows/Build.Linux.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,36 @@ jobs:
cargo test --locked --release --target "${{ matrix.target }}" -p mxc_engine -p mxc_ffi --lib state_aware
cargo test --locked --release --target "${{ matrix.target }}" -p mxc-sdk --test state_aware

# These need no LXC installed: the engine tests inject their probes, and
# the dispatch and FFI pins are refused before a container is created.
- name: Test LXC dispatch and discovery
working-directory: src
shell: bash
run: |
# No `-e`: each run's status is checked below so a failure names it.
set -uo pipefail
run_pinned() {
local label="$1"; shift
local log status
log="$(mktemp)"
cargo test --locked --release --target "${{ matrix.target }}" "$@" 2>&1 | tee "$log"

# `tee` succeeds even when cargo does not.
status=${PIPESTATUS[0]}
if [ "$status" -ne 0 ]; then
echo "::error::$label failed (cargo exited $status)."
exit "$status"
fi
if ! grep -qE 'test result: ok\. [1-9][0-9]* passed' "$log"; then
echo "::error::$label ran no test; the filter no longer matches anything."
exit 1
fi
}
run_pinned "LXC discovery matrix" -p mxc_engine --lib linux_support
run_pinned "LXC dispatch arm" -p mxc_engine --lib streaming_lxc_reaches_the_backend_on_linux
run_pinned "LXC FFI routing" -p mxc_ffi --test ffi extern_spawn_request_reaches_the_lxc_backend
run_pinned "SDK platform helpers" -p mxc-sdk --test sdk_helpers

# Runs the Bubblewrap executor characterization tests. lxc-exec was built
# into src/target/<triple>/release above, where find_binary() locates it.
- name: Test executor characterization (wxc_e2e_tests)
Expand Down
57 changes: 57 additions & 0 deletions .github/workflows/SDK.Dotnet.Test.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,63 @@ jobs:
}
Write-Host "$($Matches[1]) tests passed"

# LXC creates, starts, and attaches to a system container, so the suite
# above can only skip it. This installs the substrate and reruns just
# those tests as root.
- name: Install LXC
if: matrix.os_label == 'linux'
shell: bash
run: |
set -euo pipefail
# Runner images can carry package indexes referencing versions the
# mirrors have already dropped, so a single update is a flaky gate.
for attempt in 1 2 3; do
if sudo apt-get -o Acquire::Retries=3 update; then
break
fi
if [ "$attempt" -eq 3 ]; then
echo "::error::Failed to refresh Linux package indexes after $attempt attempts"
exit 1
fi
sleep 5
done
sudo DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::Retries=3 install -y \
lxc lxc-templates lxc-utils iptables debootstrap uidmap bridge-utils

# The policy these tests use permits no network, so the container starts
# with no interface and the runner's Docker-set FORWARD policy cannot
# reach them.
#
# MXC_LXC_TESTS_REQUIRE_EXECUTION turns an honest skip into a failure:
# this step exists to execute them, so a skip means a prerequisite
# disappeared and the gate would go green having tested nothing. The
# count is checked for the same reason: a filter that selects no test
# also exits 0.
- name: Test the LXC backend
if: matrix.os_label == 'linux'
shell: bash
env:
MXC_LXC_TESTS_REQUIRE_EXECUTION: "1"
run: |
set -uo pipefail
log="$RUNNER_TEMP/dotnet-test-lxc.log"
sudo --preserve-env=MXC_LXC_TESTS_REQUIRE_EXECUTION,PATH,HOME,DOTNET_ROOT \
"$(command -v dotnet)" test \
--project Microsoft.Mxc.Sdk.Tests/Microsoft.Mxc.Sdk.Tests.csproj \
--no-build --no-restore -- --filter-method '*MxcSandboxLxcE2ETests*' 2>&1 | tee "$log"
# `tee` succeeds even when the run beneath it fails, and a partially
# passing run still prints a non-zero succeeded count, so the run's
# own status has to be read before the summary is trusted.
status=${PIPESTATUS[0]}
if [ "$status" -ne 0 ]; then
echo "::error::dotnet test exited $status"
exit "$status"
fi
if ! grep -qE 'succeeded: *[1-9]' "$log"; then
echo '::error::The filter selected no LXC test, so this gate verified nothing.'
exit 1
fi

# Exercise the opt-in backends as one complete native unit. The managed
# tests dry-run both backends and verify WSLC's daemon + SDK DLL were
# propagated into the application output.
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/SDK.Integration.Test.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,20 @@ jobs:

# Linux needs root for Bubblewrap unprivileged-userns paths; `sudo -E`
# preserves the env vars above.
#
# `sudo` resets PATH from its own `secure_path`, which picks the image's
# Node ahead of the one setup-node installed, so the suite would run on a
# different major than this matrix selected. `env "PATH=$PATH"` carries
# the chosen toolchain through.
#
# This lane installs LXC and starts lxc-net specifically so the LXC
# backend is covered, so MXC_LXC_TESTS_REQUIRE_EXECUTION turns an LXC
# skip into a failure here rather than a green job that tested nothing.
- name: npm test
shell: bash
run: |
if [ "${{ matrix.os_label }}" = "linux" ]; then
sudo -E npm test
sudo -E env "PATH=$PATH" MXC_LXC_TESTS_REQUIRE_EXECUTION=1 npm test
else
npm test
fi
11 changes: 11 additions & 0 deletions .github/workflows/lxc-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ jobs:
MXC_LXC_TESTS_REQUIRE_EXECUTION: "1"
run: sudo --preserve-env=MXC_LXC_TESTS_REQUIRE_EXECUTION,PATH,HOME "$(command -v cargo)" test -p wxc_e2e_tests --test e2e_lxc_network_capability

# These create and start a container, so they need this lane's root and
# its LXC install. `sdk_helpers` rides along because this is the only lane
# where `platform_support()` sees a host with LXC.
- name: Run LXC streaming SDK tests
working-directory: src
env:
MXC_LXC_TESTS_REQUIRE_EXECUTION: "1"
run: sudo --preserve-env=MXC_LXC_TESTS_REQUIRE_EXECUTION,PATH,HOME "$(command -v cargo)" test -p mxc-sdk --test streaming_lxc --test sdk_helpers

- name: Show leftover firewall state on failure
if: failure()
run: |
Expand All @@ -123,6 +132,8 @@ jobs:
echo "--- MXC chains ---"
sudo iptables -S | grep -E '^-N MXC-' || echo "none"
sudo ip6tables -S | grep -E '^-N MXC-' || echo "none"
echo "--- leftover containers ---"
sudo lxc-ls -f || echo "none"

- name: Upload logs on failure
if: failure() || cancelled()
Expand Down
2 changes: 1 addition & 1 deletion docs/ci-validation-infrastructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ get fixed or wired.
| Process T1 | ✅ Good | Windows 24H2+ only. Runs the primitives suite, tier-gated to `base-container`. Includes the schema 0.8 directional networking phases (capability matrix, model-3 equivalence, explicit egress rules, host loopback, runtime proxy, reject surface) and the legacy 0.7 network lane. Remaining failures are genuine MXC bugs or harness limitations. |
| Process T3 | ✅ Good | Windows 23H2 only. Runs the primitives suite tier-gated to `appcontainer-dacl`, plus `T3-Workloads.ps1` (real programs — pwsh, git, node, python, cmd — on top of the T3 primitives). The 0.8 networking phases assert the documented *rejection* behavior here, since AppContainer cannot carry egress rules, proxy peer identity, or host-loopback configuration. |
| Bubblewrap | ✅ Good | |
| LXC | ✅ Good | Some networking tests fail on distros other than Ubuntu 24.04; seems to be an issue with MXC. |
| LXC | ✅ Good | Some networking tests fail on distros other than Ubuntu 24.04; seems to be an issue with MXC. The in-process streaming handle is covered at PR time by `lxc-e2e.yml` instead, because this matrix runs prebuilt binaries. |
| WSLC | ✅ Good | Might have to retry hung jobs - this is an issue with overzealous agent reclaiming. |
| IsolationSession | ✅ Good | Runs the one-shot and state-aware suites, the Rust SDK in-process and helper tests, the C# end-to-end tests, the Node SDK suite and the COM apartment probe. Fails when the host cannot run isolation sessions, when a suite executes nothing, or when the run changes the set of local accounts. |
| Windows Sandbox | ⛔ Blocked | Images don't support `Containers-DisposableClientVM` opt. feature |
Expand Down
33 changes: 30 additions & 3 deletions docs/lxc-support/lxc-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ import {
} from '@microsoft/mxc-sdk';

const policy: SandboxPolicy = {
version: '0.8.0-alpha',
filesystem: {
Comment on lines 211 to 213
readwritePaths: ['/tmp/output'],
readonlyPaths: ['/opt/tools'],
Expand All @@ -229,6 +230,33 @@ pty.onData((data) => console.log(data));
pty.onExit((e) => console.log('Exit:', e.exitCode));
```

## Streaming

LXC implements `SandboxBackend`, so `mxc_sdk::spawn_sandbox`, `mxc_sdk::run`,
and every SDK built on `mxc_spawn_request` / `mxc_run_request` reach it
in-process. The handle serves live stdin, stdout, and stderr, plus `wait` and
`kill`.

**Pipes, not a pty.** `isatty()` is false inside the container, and
`StdioMode::Inherit` is refused. Run `lxc-exec` for a terminal.

**`kill()` stops the container,** not just the workload: the workload runs under
container init, where nothing aimed at the host `lxc-attach` process reaches it.

**One live sandbox per container name, per process.** A second sandbox naming a
`containerId` this process already holds is refused rather than queued, because
LXC reads a run's network section only when the container starts. Omit
`containerId` for a generated name. The claim is released when the handle drops.

**Teardown is owed on every terminal path,** including a drop without `wait`:
the proxy pin and the network chains come down, and the container is released.
A teardown failure is reported through `Sandbox::warnings`, which a bare drop
leaves no handle to read.

**The container stays up for as long as the handle lives.** `lxc-exec` is
short-lived by comparison. Weigh that against the threat model before embedding
streaming in a long-lived service.

Comment thread
SohamDas2021 marked this conversation as resolved.
## Building

```bash
Expand Down Expand Up @@ -329,6 +357,5 @@ The zone query should answer the zone you assigned.
state-aware request is rejected.
- **Streaming gives pipes, not a terminal.** The `SandboxBackend` path wires
stdin, stdout, and stderr to pipes and refuses `StdioMode::Inherit`, so the
workload sees `isatty() == false`; the `lxc-exec` binary keeps its pty. The
engine does not route to this path yet, so the in-process SDK APIs still
report `UnsupportedContainment` for LXC.
workload sees `isatty() == false`; the `lxc-exec` binary keeps its pty. See
[Streaming](#streaming).
7 changes: 7 additions & 0 deletions docs/pull-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ it fans out to the reusable `Build.Windows.Job.yml`, `Build.Linux.Job.yml`, and
x64/arm64, Linux x64/arm64, and macOS arm64 hosts, then runs the lint,
versioning, and SDK jobs.

### LXC (`lxc-e2e.yml`)

A separate workflow, because the primary Linux lane does not install LXC and
does not run as root. It triggers on PRs targeting `main`, so a PR stacked on
another branch gets no LXC gating until it is retargeted — dispatch it manually
for a stacked head.

## Azure Pipelines (optional on PRs, required on `main`)

The ADO pipeline (`MXC-PR-Build`) is the Azure version of the PR pipeline. The official
Expand Down
42 changes: 42 additions & 0 deletions sdk/dotnet/Microsoft.Mxc.Sdk.Tests/LxcHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Mxc.Sdk;
using Xunit;

namespace Microsoft.Mxc.Sdk.Tests;

/// <summary>
/// The live-host gate the LXC suites share. A live run needs Linux, an
/// installed LXC, and root, because LXC creates, starts, and attaches to a
/// system container.
/// </summary>
internal static class LxcHost
{
// Evaluated once: this answer decides failure versus skip, so it has to be
// the same for every test that consults it.
private static readonly Lazy<bool> Available = new(() =>
OperatingSystem.IsLinux()
&& Environment.IsPrivilegedProcess
&& MxcSandbox.GetAvailableBackends()
.Any(b => b.Backend == ContainmentBackend.Lxc));

// Without this, a run in which everything skipped is indistinguishable from
// one that passed. The same variable the Rust and shell LXC suites honour.
private static bool SkipsAreFailures =>
Environment.GetEnvironmentVariable("MXC_LXC_TESTS_REQUIRE_EXECUTION") is "1" or "true";

/// <summary>Skips the calling test when LXC is unavailable, or fails it when
/// skips have been declared failures.</summary>
internal static void Require()
{
Assert.False(
SkipsAreFailures && !Available.Value,
"MXC_LXC_TESTS_REQUIRE_EXECUTION is set, but this host cannot run LXC. "
+ "That needs Linux, root, and an installed LXC that "
+ "GetAvailableBackends() reports.");
Assert.SkipUnless(
Available.Value,
"this host has no LXC backend, or the test is not running as root");
}
}
106 changes: 106 additions & 0 deletions sdk/dotnet/Microsoft.Mxc.Sdk.Tests/MxcSandboxLxcE2ETests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text;
using Microsoft.Mxc.Sdk;
using Xunit;

namespace Microsoft.Mxc.Sdk.Tests;

/// <summary>
/// Runs LXC sandboxes against a live host through this binding.
/// </summary>
/// <remarks>
/// The engine's own suite covers the backend; these establish that the managed
/// type, the request envelope, the C ABI and the native library agree, which is
/// the part no Rust test can see.
/// </remarks>
[Collection("MxcLiveHost")]
public class MxcSandboxLxcE2ETests
{
// A healthy run finishes in under a second, so this only trips on a wedged attach.
private const int WaitBoundMs = 180_000;
Comment thread
SohamDas2021 marked this conversation as resolved.

private static SandboxRequest Request(string command, string containerName) =>
new(
new SandboxPolicy
{
Version = "0.9.0-alpha",
TimeoutMs = WaitBoundMs,

// A policy naming no network defaults to `enforcementMode:
// 'capabilities'`, which LXC refuses outright.
Network = new NetworkPolicy
{
Egress = new NetworkEgressPolicy { Default = NetworkAction.Deny },
Ingress = new NetworkIngressPolicy
{
Default = NetworkAction.Deny,
HostLoopback = NetworkAction.Deny,
},
},
},
command)
{
ContainerName = containerName,
Containment = new LxcContainment(),
};

[Fact]
public void Run_ReturnsTheWorkloadsOutputAndExitCode()
{
LxcHost.Require();

var result = MxcSandbox.Run(
Request("printf 'mxc_lxc_run_ok\\n'; exit 3", "mxc-dotnet-run"));

Assert.False(result.TimedOut);
Assert.Equal(3, result.ExitCode);
Assert.Contains("mxc_lxc_run_ok", result.Stdout, StringComparison.Ordinal);
}

[Fact]
public void Spawn_StreamsStandardOutputBeforeTheWorkloadExits()
{
LxcHost.Require();

// Blocking on stdin: the workload cannot reach its exit until this test
// lets it.
using var proc = MxcSandbox.Spawn(
Request(
"printf 'mxc_lxc_stream_ok\\n'; IFS= read -r _; printf 'mxc_lxc_done\\n'",
"mxc-dotnet-spawn"));

var stdin = proc.StandardInput;
var stdout = proc.StandardOutput;
Assert.NotNull(stdin);
Assert.NotNull(stdout);

using var reader = new StreamReader(stdout!, Encoding.UTF8);
var first = ReadLineWithin(reader, TimeSpan.FromMinutes(3));
Assert.Contains("mxc_lxc_stream_ok", first, StringComparison.Ordinal);
Assert.False(
proc.TryGetExitCode(out _),
"the first line arrived only after the workload exited, so it was not streamed");

using (var writer = new StreamWriter(stdin!, Encoding.UTF8))
{
writer.Write("continue\n");
}

var result = proc.Wait();
Assert.False(result.TimedOut);
Assert.Equal(0, result.ExitCode);
}

/// <summary>
/// Reads one line on a worker thread, so a stream that never delivers fails
/// here rather than hanging the job.
/// </summary>
private static string ReadLineWithin(StreamReader reader, TimeSpan deadline)
{
var read = Task.Run(reader.ReadLine);
Assert.True(read.Wait(deadline), $"no output within {deadline}");
return read.Result ?? string.Empty;
}
}
4 changes: 0 additions & 4 deletions sdk/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,10 +377,6 @@ request.Containment = new LxcContainment
};
```

The managed SDK can represent LXC settings, but its in-process `Run`,
`RunAsync`, and `Spawn` surfaces reject LXC because the backend does not expose
captured pipe-based execution. Use the standalone `lxc-exec` binary for LXC.

#### WSL Container options

`WslcContainment` selects the WSLC backend and carries its image,
Expand Down
Loading
Loading