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
77 changes: 77 additions & 0 deletions .github/workflows/linux-exec-boundary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Linux execution boundary

on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
native:
name: Execution boundary (${{ matrix.compiler }})
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
compiler: [g++, clang++]
defaults:
run:
working-directory: projects/linux-exec-boundary
shell: bash
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false

- name: Verify nonprivileged runner and existing toolchain
env:
CXX: ${{ matrix.compiler }}
run: |
test "$(id -u)" -ne 0
command -v "$CXX"
"$CXX" --version
cmake --version
ctest --version
python3 --version
uname -r
git rev-parse HEAD

- name: Configure and build approved local fixtures
env:
CXX: ${{ matrix.compiler }}
run: |
cmake -S . -B build -DCMAKE_CXX_COMPILER="$CXX" -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallel 2
python3 tools/check_style.py

- name: Probe capabilities without host configuration changes
run: timeout 10s build/capability_probe > build/runner-capabilities.json

# Reviewed fixed fixtures only: watchdog/resource/alarm bounds, exact owned
# child targeting, no detached fixture, no arbitrary executable input.
# Existing CTest per-test timeouts are 8s and 50s. Sanitizers remain a
# separately qualified local run, not an assumed hosted-runner capability.
- name: Execute bounded native lifecycle and qualification tests
run: ctest --test-dir build --output-on-failure

- name: Record actual capability and conditional outcomes
if: always()
run: |
python3 - <<'PY'
import json
from pathlib import Path
for name in ("runner-capabilities.json", "evidence/capabilities.json",
"evidence/results.json", "evidence/negative-controls.json"):
path = Path("build") / name
if path.is_file():
print(name)
print(json.dumps(json.loads(path.read_text()), indent=2, sort_keys=True))
else:
print(name + ": NOT_PRODUCED; inspect earlier step failure")
print("CTest success does not mean every optional scenario executed.")
print("E09 native remains NOT_RUN; no arbitrary-tree containment claim.")
print("Hosted sanitizer qualification: NOT_RUN; local ASan/UBSan evidence is separate.")
PY
115 changes: 115 additions & 0 deletions projects/linux-exec-boundary/CAPABILITY-MATRIX.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"schema": "exec-boundary.capability-matrix.v1",
"baseline_commit": "2c14763864b3d26bc7a67774e22f9ba78f9329c6",
"environment": {
"kernel": "6.18.54.1-microsoft-standard-WSL2",
"arch": "x86_64",
"non_root": true,
"native_fs": "ext4",
"gcc": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0",
"clang": "Ubuntu clang version 18.1.3 (1ubuntu1)",
"cmake": "cmake version 3.28.3",
"ctest": "ctest version 3.28.3",
"python": "3.12.3",
"own_policy_status": {
"CapEff": "0000000000000000",
"NoNewPrivs": "0",
"Seccomp": "2",
"Seccomp_filters": "1"
}
},
"source_of_authority": "unprivileged native probes and actual fixture operations",
"features": {
"pidfd_open_and_signal": {
"status": "SUPPORTED_AND_TESTED",
"evidence": "owned child; signal 0; no numeric retarget"
},
"waitid_P_PIDFD": {
"status": "SUPPORTED_AND_TESTED",
"evidence": "exact child exit and native EINTR/retry"
},
"no_new_privs": {
"status": "SUPPORTED_AND_TESTED",
"evidence": "child set/get plus post-exec witness"
},
"landlock": {
"status": "SUPPORTED_AND_TESTED",
"abi": 7,
"enforced_access_classes": [
"READ_FILE",
"READ_DIR",
"WRITE_FILE",
"MAKE_REG",
"REMOVE_FILE",
"TRUNCATE"
],
"evidence": "successful setup before exec; six allowed/denied operation classes after exec"
},
"seccomp": {
"status": "SUPPORTED_AND_TESTED",
"evidence": "getppid allowed in control; EPERM under installed filter across exec",
"inherited_mode_is_not_request_installation": true
},
"rlimits": {
"status": "SUPPORTED_AND_TESTED",
"controls": [
"CPU",
"AS",
"FSIZE",
"NOFILE",
"CORE"
],
"evidence": "set/get and post-exec values; SIGXCPU, ENOMEM, SIGXFSZ, EMFILE and disabled cores"
},
"close_range": {
"status": "SUPPORTED_AND_TESTED",
"evidence": "unexpected inherited FD 57 removed; injected FD 9 detected"
},
"subreaper": {
"status": "SUPPORTED_AND_TESTED",
"evidence": "cooperating descendant adopted/reaped after leader exit"
},
"dedicated_session_process_group": {
"status": "SUPPORTED_AND_TESTED",
"whole_tree_guarantee": false
},
"cgroup_v2_presence": {
"status": "SUPPORTED_AND_TESTED",
"present": true
},
"cgroup_delegation": {
"status": "UNAVAILABLE",
"errno": 13,
"evidence": "own current subgroup mkdir denied; current cgroup/procs not writable"
},
"cgroup_membership_and_kill": {
"status": "NOT_PROBED",
"reason": "no writable delegation; no root/host changes"
},
"cgroup_backend": {
"status": "UNAVAILABLE",
"strict_requests": "refused before fork"
},
"atomic_clone3_pidfd": {
"status": "NOT_PROBED",
"reason": "gated fork plus exclusive unreaped-child ownership chosen"
},
"detached_descendant_native": {
"status": "NOT_PROBED",
"execution": "NOT_RUN",
"reason": "verified secondary containment absent"
},
"clang_format": {
"status": "UNAVAILABLE",
"fallback": "checked LF/final-newline/tab/NUL/trailing-whitespace conventions"
},
"clang_tidy": {
"status": "UNAVAILABLE",
"fallback": "Clang built-in static analyzer executed"
},
"privileged_provisioning": {
"status": "DENIED_BY_POLICY",
"reason": "no sudo or host-wide settings"
}
}
}
46 changes: 46 additions & 0 deletions projects/linux-exec-boundary/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
cmake_minimum_required(VERSION 3.20)
project(linux_exec_boundary VERSION 0.1.0 LANGUAGES CXX)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(FATAL_ERROR "Native Linux is required; do not substitute Windows process semantics")
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
option(BOUNDARY_SANITIZERS "Instrument the supervisor with ASan/UBSan" OFF)
add_library(boundary_core src/supervisor.cpp src/manifest.cpp)
target_include_directories(boundary_core PUBLIC include)
add_executable(boundary_fixture fixtures/fixture.cpp)
target_include_directories(boundary_fixture PRIVATE include)
add_executable(exec_boundary src/main.cpp)
target_link_libraries(exec_boundary PRIVATE boundary_core)
target_compile_definitions(exec_boundary PRIVATE APPROVED_FIXTURE_PATH="$<TARGET_FILE:boundary_fixture>")
add_dependencies(exec_boundary boundary_fixture)
add_executable(capability_probe tools/capability_probe.cpp)
foreach(target boundary_core boundary_fixture exec_boundary capability_probe)
target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic -Werror)
endforeach()
if(BOUNDARY_SANITIZERS)
target_compile_options(boundary_core PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
target_compile_options(exec_boundary PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
target_link_options(exec_boundary PRIVATE -fsanitize=address,undefined)
endif()
add_executable(lifecycle_test tests/lifecycle.cpp)
target_link_libraries(lifecycle_test PRIVATE boundary_core)
target_compile_definitions(lifecycle_test PRIVATE APPROVED_FIXTURE_PATH="$<TARGET_FILE:boundary_fixture>")
target_compile_options(lifecycle_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
add_dependencies(lifecycle_test boundary_fixture)
if(BOUNDARY_SANITIZERS)
target_link_options(lifecycle_test PRIVATE -fsanitize=address,undefined)
endif()
enable_testing()
find_package(Python3 3.11 REQUIRED COMPONENTS Interpreter)
set(QUALIFY_EXTRA)
if(BOUNDARY_SANITIZERS)
list(APPEND QUALIFY_EXTRA --sanitized)
endif()
add_test(NAME lifecycle COMMAND lifecycle_test)
set_tests_properties(lifecycle PROPERTIES TIMEOUT 8)
add_test(NAME qualification COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/qualify.py
--supervisor $<TARGET_FILE:exec_boundary> --probe $<TARGET_FILE:capability_probe>
--output ${CMAKE_CURRENT_BINARY_DIR}/evidence ${QUALIFY_EXTRA})
set_tests_properties(qualification PROPERTIES TIMEOUT 50)
108 changes: 108 additions & 0 deletions projects/linux-exec-boundary/EXECUTION-CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Execution contract

## Problem and invariant

A successful fork does not prove exec succeeded, a numeric PID is not a lasting
target, and a successful group kill does not establish whole-tree containment.
Reports separate attempted setup, acknowledged controls and observed outcome.
A strict unsupported profile is refused before launching a fixture.

This is a single-request, dedicated-supervisor-process API. The caller must not
run concurrent requests, create unrelated children concurrently, run another
reaper or install SIGCHLD ignore/SA_NOCLDWAIT. Existing direct children and an
already active subreaper cause refusal. The CLI owns the complete process and
uses fixed fixture arguments; the C++ header is not a general embedding SDK.

## ExecutionRequest

- request_id and run_id: required 1-64 character alphanumeric, hyphen or
underscore labels. They are run labels, not durable OS identities.
- approved_executable: CMake's local fixture, opened O_PATH/CLOEXEC before fork
and executed through that descriptor using execveat.
- arguments: exactly boundary_fixture and one allowlisted synthetic scenario.
- environment: exactly LANG=C and LC_ALL=C; stdin is /dev/null.
- working_directory: a newly owned mkdtemp directory under caller TMPDIR.
Only this invocation's directory is removed.
- timeout_ms: 1-3000, measured from execute entry with steady_clock.
- output_limit: 0-16384 retained bytes per stream. Total observed bytes remain
distinct from retained bytes.
- limits: CPU soft/hard 1/2 seconds, AS 32-256 MiB, FSIZE at most 1 MiB,
NOFILE 16-64, CORE zero. CLI defaults: AS 128 MiB, FSIZE 4096 (1024 for the
file-size scenario), NOFILE 32.
- profile: direct, landlock or seccomp. strict-tree and unknown profiles refuse
execution. cancel_after_ms supplies a bounded synthetic cancellation
schedule; SIGTERM/SIGINT provide external cancellation.
- injected_fault: a transparent, allowlisted lab control recorded in results.

## Design decisions

The child closes unexpected descriptors with close_range. Standard I/O, the
CLOEXEC executable/error descriptors, the launch gate and an intentional
fixture-witness descriptor are the only setup descriptors. The fixture closes
its witness descriptor before the scenario and reports unexpected descriptors.

Fork does not atomically return a pidfd. The child waits on the gate while the
parent opens its pidfd and retains exclusive unreaped-child ownership. A child
that exits early remains a zombie until this parent reaps it, preventing PID
reuse under the stated SIGCHLD/reaper preconditions. Acquisition failure aborts
launch and recovery targets only the still-owned unreaped direct child. There
is no silent numeric-target execution fallback. Atomic clone3/CLONE_PIDFD is
not claimed or required.

The child creates its own session. The parent confirms PGID=SID=child PID before
opening the gate, and validates both values again before group signals. Other
existing-session processes cannot join this dedicated session. The leader
remains unreaped during group cleanup, pinning its identity. A descendant can
still create another session; this is not whole-tree containment.

Child setup sets limits, no_new_privs and requested restrictions, then
acknowledges success. A separate post-exec fixture witness reports PID/PGID/SID,
observed no_new_privs/seccomp mode, resource values and descriptor census.
Neither an ack nor EOF alone proves exec. The witness is trusted synthetic
instrumentation, not authentication against a malicious replacement binary.

## Result and classification

Schema: exec-boundary.execution.v1. Request/run identity, stage/errno,
request-local features, inherited/observed policy, raw waitid status, stop
decisions, bounded capture, cleanup evidence and unsupported controls remain
separate.

| State | CLI exit | Condition |
| --- | ---: | --- |
| COMPLETED | 0 | Exec witnessed, normal zero exit, no stop decision |
| EXITED_NONZERO | 1 | Exec witnessed, normal nonzero exit |
| SIGNALED | 2 | Exec witnessed, kernel signal termination observed |
| TIMED_OUT | 3 | Deadline stop decision preceded observed termination |
| CANCELLED | 4 | Cancellation stop decision preceded observed termination |
| LAUNCH_FAILED | 5 | Spawn/exec/setup failed or no valid exec witness |
| POLICY_UNAVAILABLE | 6 | Requested control refused before fork |
| CONTAINMENT_UNCONFIRMED | 7 | Cleanup or inherited-FD boundary unconfirmed |
| SUPERVISOR_ERROR | 8 | Supervisor precondition/accounting/artifact failure |

Unconfirmed containment takes precedence over nominal completion. A timeout or
cancellation decision is retained even if later setup/exit evidence arrives;
an exit/stop race may report that decision with exit zero. This does not assert
that a signal caused the exit. Raw child exit/signal is not the CLI status.

Inherited seccomp mode 2 does not establish request-local installation.
features.seccomp means this request successfully installed the filter;
inherited and post-exec modes are separate. Landlock has no general policy
introspection API: successful setup and controlled post-exec allowed/denied
operations support the narrow claim.

Cleanup waits are bounded observations, not hard real-time guarantees for
uninterruptible kernel tasks. Missing reaping within the window is
CONTAINMENT_UNCONFIRMED. Parent SIGTERM requests cancellation. SIGKILL, machine
failure and hostile supervisor modification are outside the guarantee.

## Compatibility and validation

No saved-evidence schema, source label, fixture, golden, adapter or root workflow
changes. The independent checker consumes manifests and watchdog observations;
it never calls this classifier to derive expected answers. It rejects false
termination, capability, cleanup, resource, identity and truncation claims.

Matrices retain conditional results and NOT_RUN. Repeated schedules are race
controls, not independent semantic coverage. See REPRODUCTION.md and the
independent audit for actual execution evidence.
Loading