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
91 changes: 91 additions & 0 deletions .github/notes/pr53-linux-sandbox-ci-userns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# PR #53 Linux sandbox CI user-namespace blocker

PR #53 adds the `codespace-linux-sandbox` helper and requires the Linux
isolation tests to exercise the real bubblewrap + seccomp path in CI rather
than silently skipping a failed probe.

## Symptom

GitHub Actions CI run #99 (`35383518697`) reached the dedicated
`crates/linux-sandbox` isolation tests, but all six tests failed at their
common helper probe. With probe stderr enabled, bubblewrap reported:

```text
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
```

The helper then exited with status 1, so
`CODESPACE_REQUIRE_LINUX_SANDBOX=1` correctly turned the failed probe into a
hard CI failure.

This is distinct from the earlier dependency-lock, Clippy, and unit-test
environment issues. The linux-sandbox unit suite, including the helper
self-reexec readable-root check, passed before the integration probe ran.

## Cause

The Restricted network profile uses bubblewrap network isolation
(`--unshare-net`). GitHub-hosted Ubuntu images can restrict unprivileged user
namespaces with either `kernel.unprivileged_userns_clone` or Ubuntu 24.04+
AppArmor's `kernel.apparmor_restrict_unprivileged_userns` gate. In that host
configuration bubblewrap can create the sandbox process far enough to report
its loopback setup, but the netlink address operation is rejected with
`EPERM` / `RTM_NEWADDR`.

This is a CI host prerequisite, not a reason to weaken the CodeSpace sandbox
profile. OpenAI's `codex-action` handles the same GitHub-hosted Linux condition
before running bubblewrap-backed Codex sandbox modes:

- https://github.com/openai/codex-action/blob/main/action.yml

## CI resolution

The CodeSpace `ubuntu-latest` Rust job prepares the ephemeral GitHub-hosted
runner immediately after installing bubblewrap:

```bash
current_userns="$(sysctl -n kernel.unprivileged_userns_clone 2>/dev/null || true)"
if [ -n "$current_userns" ] && [ "$current_userns" != "1" ]; then
sudo sysctl -w kernel.unprivileged_userns_clone=1
fi

current_apparmor="$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null || true)"
if [ -n "$current_apparmor" ] && [ "$current_apparmor" != "0" ]; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
```

Both checks are conditional so older Ubuntu images without one of the sysctls
remain valid. The workflow currently runs on GitHub-hosted `ubuntu-latest`;
if a self-hosted runner is introduced, its host policy should be reviewed
explicitly rather than assuming this CI mutation is appropriate there.

## Security / product scope

This change is CI-host preparation only. It does **not**:

- remove `--unshare-net`;
- change Restricted network policy or seccomp enforcement;
- make a failed sandbox probe skippable in Linux CI;
- add an unsandboxed fallback after a successful probe;
- change Runner/MCP wire contracts; or
- change production host sysctls at runtime.

`CODESPACE_REQUIRE_LINUX_SANDBOX=1` remains scoped to the isolation integration
tests, so the CI contract is still: if the GitHub runner has been prepared for
bubblewrap and the real sandbox cannot start, the job fails.

## Related PR #53 fixes

The prior helper-readable-root change remains a separate correctness fix. The
pinned Codex Linux helper re-execs its own executable inside bubblewrap before
applying seccomp, so that infrastructure binary must remain readable in the
Minimal filesystem view. CI run #99 showed that the observed blocker occurs
earlier, during bubblewrap network-namespace setup; therefore the helper-read
fix should not be described as the root cause of the `RTM_NEWADDR` failure.

References:

- PR #53: https://github.com/novelKR/CodeSpace/pull/53
- CI run #99: https://github.com/novelKR/CodeSpace/actions/runs/35383518697
- helper-readable fix: `446929248e7240a5fe867971a5b666ced78dd58a`
191 changes: 171 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@ jobs:
SCAN_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
run: ./scripts/check-no-model-deps.sh

rust:
rust-format:
name: Rust / Format + upstream pin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
components: rustfmt
- name: Upstream pin
run: PIN_ONLY=1 ./scripts/check-upstream-pin.sh
- name: Format
Expand All @@ -36,24 +37,174 @@ jobs:
cargo fmt --check --manifest-path crates/codex-runtime/Cargo.toml
cargo fmt --check --manifest-path crates/pty/Cargo.toml
cargo fmt --check --manifest-path crates/file-system/Cargo.toml
cargo fmt --check --manifest-path crates/linux-sandbox/Cargo.toml

rust-clippy:
name: Rust / Clippy (${{ matrix.name }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: root
command: cargo clippy --locked --all-targets -- -D warnings
- name: adapters
command: |
cargo clippy --locked --manifest-path crates/patch/Cargo.toml --all-targets -- -D warnings
cargo clippy --locked --manifest-path crates/pty/Cargo.toml --all-targets -- -D warnings
cargo clippy --locked --manifest-path crates/file-system/Cargo.toml --all-targets -- -D warnings
- name: codex-adapters
command: |
cargo clippy --locked --manifest-path crates/codex-runtime/Cargo.toml --all-targets -- -D warnings
cargo clippy --locked --manifest-path crates/linux-sandbox/Cargo.toml --all-targets -- -D warnings
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cache Cargo downloads
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Clippy
run: ${{ matrix.command }}

rust-unit:
name: Rust / Unit (${{ matrix.name }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: patch
command: cargo test --locked --manifest-path crates/patch/Cargo.toml
- name: codex-runtime
command: cargo test --locked --manifest-path crates/codex-runtime/Cargo.toml
- name: pty
command: cargo test --locked --manifest-path crates/pty/Cargo.toml
- name: file-system
command: cargo test --locked --manifest-path crates/file-system/Cargo.toml
- name: linux-sandbox
command: cargo test --locked --manifest-path crates/linux-sandbox/Cargo.toml --lib
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo downloads
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Unit tests
run: ${{ matrix.command }}

rust-linux-isolation:
name: Rust / Linux isolation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo downloads
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Install bubblewrap
run: sudo apt-get update && sudo apt-get install -y bubblewrap
- name: Enable unprivileged user namespaces for bubblewrap
run: |
cargo clippy --all-targets -- -D warnings
cargo clippy --manifest-path crates/patch/Cargo.toml --all-targets -- -D warnings
cargo clippy --manifest-path crates/codex-runtime/Cargo.toml --all-targets -- -D warnings
cargo clippy --manifest-path crates/pty/Cargo.toml --all-targets -- -D warnings
cargo clippy --manifest-path crates/file-system/Cargo.toml --all-targets -- -D warnings
- name: Test
set -euo pipefail

# GitHub-hosted Ubuntu may disable unprivileged user namespaces, or
# gate them through AppArmor. Either condition can make bwrap fail
# while bringing up the isolated loopback device for --unshare-net.
current_userns="$(sysctl -n kernel.unprivileged_userns_clone 2>/dev/null || true)"
if [ -n "$current_userns" ] && [ "$current_userns" != "1" ]; then
echo "Enabling kernel.unprivileged_userns_clone for bubblewrap."
sudo sysctl -w kernel.unprivileged_userns_clone=1
fi

current_apparmor="$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null || true)"
if [ -n "$current_apparmor" ] && [ "$current_apparmor" != "0" ]; then
echo "Disabling kernel.apparmor_restrict_unprivileged_userns for bubblewrap."
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
- name: Build Linux sandbox helper
run: cargo build --locked --manifest-path crates/linux-sandbox/Cargo.toml --bin codespace-linux-sandbox
- name: Linux isolation tests
env:
CODESPACE_REQUIRE_LINUX_SANDBOX: "1"
run: cargo test --locked --manifest-path crates/linux-sandbox/Cargo.toml --test isolation

rust-integration:
name: Rust / Integration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo downloads
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Install bubblewrap
run: sudo apt-get update && sudo apt-get install -y bubblewrap
- name: Enable unprivileged user namespaces for bubblewrap
run: |
set -euo pipefail

current_userns="$(sysctl -n kernel.unprivileged_userns_clone 2>/dev/null || true)"
if [ -n "$current_userns" ] && [ "$current_userns" != "1" ]; then
sudo sysctl -w kernel.unprivileged_userns_clone=1
fi

current_apparmor="$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null || true)"
if [ -n "$current_apparmor" ] && [ "$current_apparmor" != "0" ]; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
- name: Build helpers
run: |
cargo build --locked --manifest-path crates/patch/Cargo.toml --bin codespace-patch
cargo build --locked --manifest-path crates/codex-runtime/Cargo.toml --bin codespace-codex-runtime
cargo build --locked --manifest-path crates/linux-sandbox/Cargo.toml --bin codespace-linux-sandbox
- name: Workspace integration tests
env:
CODESPACE_PATCH_BIN: ${{ github.workspace }}/crates/patch/target/debug/codespace-patch
CODESPACE_RUNTIME_BIN: ${{ github.workspace }}/crates/codex-runtime/target/debug/codespace-codex-runtime
CODESPACE_LINUX_SANDBOX_BIN: ${{ github.workspace }}/crates/linux-sandbox/target/debug/codespace-linux-sandbox
run: cargo test --locked --workspace

rust:
name: rust
if: ${{ always() }}
needs:
- rust-format
- rust-clippy
- rust-unit
- rust-linux-isolation
- rust-integration
runs-on: ubuntu-latest
steps:
- name: Require all Rust checks
run: |
cargo test --manifest-path crates/patch/Cargo.toml
cargo build --manifest-path crates/patch/Cargo.toml --bin codespace-patch
cargo test --manifest-path crates/codex-runtime/Cargo.toml
cargo build --manifest-path crates/codex-runtime/Cargo.toml --bin codespace-codex-runtime
cargo test --manifest-path crates/pty/Cargo.toml
cargo test --manifest-path crates/file-system/Cargo.toml
# Worker apply_patch still shells out to the patch helper. Export
# CODESPACE_PATCH_BIN before workspace and UDS tests. RuntimeProcess
# tests spawn the worker when CODESPACE_RUNTIME_BIN is set.
export CODESPACE_PATCH_BIN="${PWD}/crates/patch/target/debug/codespace-patch"
export CODESPACE_RUNTIME_BIN="${PWD}/crates/codex-runtime/target/debug/codespace-codex-runtime"
cargo test --workspace
test "${{ needs.rust-format.result }}" = "success"
test "${{ needs.rust-clippy.result }}" = "success"
test "${{ needs.rust-unit.result }}" = "success"
test "${{ needs.rust-linux-isolation.result }}" = "success"
test "${{ needs.rust-integration.result }}" = "success"
Loading
Loading