Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dcd16df
feat(security): confine measure/sim subprocess exec (sandbox incremen…
brianlball Jun 6, 2026
ceafc91
feat(security): Landlock FS + seccomp net-deny (sandbox increment 3)
brianlball Jun 6, 2026
1b6a62b
fix(security): keep test_measure temp off the run dir (Docker bind mo…
brianlball Jun 6, 2026
3d9de87
feat(security): secure-by-default + local/non-root protection (sandbo…
brianlball Jun 6, 2026
e75d0b8
chore: stop tracking docs/plans/measure-exec-sandbox.md
brianlball Jun 6, 2026
90e89c6
refactor(security): add reject_escaping_symlinks() staging guard
brianlball Jun 6, 2026
838b6c3
fix(security): reject sandbox uid/gid <= 0 (Codex H5)
brianlball Jun 6, 2026
a27f41f
fix(security): seccomp KILL unexpected arch + x32, deny io_uring_setu…
brianlball Jun 6, 2026
cce1673
fix(security): Landlock IOCTL_DEV (ABI>=5) + fail-closed add_rule (Co…
brianlball Jun 6, 2026
60373e8
fix(security): fail-closed when Landlock/seccomp cannot engage (Codex…
brianlball Jun 6, 2026
b51d237
fix(security): respect OSMCP_SANDBOX_NET, symlink-safe chown, tighten…
brianlball Jun 6, 2026
cca9681
fix(security): apply_measure rejects escaping symlinks, stages symlin…
brianlball Jun 6, 2026
37cd3b8
fix(security): validate test_measure paths; escape/validate generated…
brianlball Jun 6, 2026
bfe7d21
feat(security): enforce OSMCP_SIM_TIMEOUT_SECONDS; validate run_simul…
brianlball Jun 6, 2026
be16d83
Merge remote-tracking branch 'origin/develop' into feat/measure-exec-…
brianlball Jun 6, 2026
ae9a01c
test(security): publish sandbox confinement suite + run it in CI
brianlball Jun 6, 2026
6efac2e
ci(security): run integration suite under OSMCP_SANDBOX=auto explicit…
brianlball Jun 6, 2026
a38a280
ci(security): run sandbox suite on arm64 too (matrix amd64+arm64)
brianlball Jun 6, 2026
e565a95
ci: add arch-sensitive test set to arm64 (shard 2)
brianlball Jun 6, 2026
5da03f4
fix(ci): set RUBYLIB on arm64 image so measure tests can require open…
brianlball Jun 6, 2026
d2860f4
fix(security): batch 1 — Ruby interpolation RCE, fail-closed mode, en…
brianlball Jun 7, 2026
3f942b2
fix(security): batch 2 — per-file /dev Landlock rules + process-group…
brianlball Jun 7, 2026
ab0d4b8
fix(security): batch 3 — run_osw access gate, test_measure private st…
brianlball Jun 7, 2026
eb82af8
Harden post-test measure metadata refresh
brianlball Jun 7, 2026
9633194
Document simulation sandbox security
brianlball Jun 7, 2026
6d803f1
fix(security): address PR #64 review (Copilot + manual)
brianlball Jun 7, 2026
05e27fd
Merge pull request #64 from NatLabRockies/codex/measure-sandbox-harde…
brianlball Jun 7, 2026
3afb1e0
fix(security): per-tenant sandbox uids + drop /proc from Landlock (H1…
brianlball Jun 7, 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
26 changes: 21 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,15 @@ jobs:
retention-days: 1

arm64-test:
# Start of arm64 test matrix: shard 1 only (real EnergyPlus sim via SEB4 OSW,
# asserts EUI ≈ 1.875 ± 2%). Expand to [1..5] once arm64 sim parity is proven.
# arm64 test matrix: shard 1 = real EnergyPlus sim via SEB4 OSW (asserts EUI
# ≈ 1.875 ± 2%) + weather/loops; shard 2 = the arch-sensitive set (SWIG/stdout
# deb-vs-wheel, measure exec/bundler, an HVAC sim). Both run confined (auto).
needs: arm64-build
runs-on: ubuntu-24.04-arm
strategy:
fail-fast: false
matrix:
shard: [1]
shard: [1, 2]
steps:
- uses: actions/checkout@v4

Expand All @@ -115,6 +116,9 @@ jobs:
env:
RUN_OPENSTUDIO_INTEGRATION: "1"
MCP_SERVER_CMD: "openstudio-mcp"
# Run the integration suite confined (explicit, not via the code default).
# Measures/sims drop to the unprivileged sandbox uid + Landlock + seccomp.
OSMCP_SANDBOX: "auto"
run: |
mkdir -p runs
case ${{ matrix.shard }} in
Expand All @@ -123,10 +127,19 @@ jobs:
FILES="tests/test_example_workflows.py tests/test_component_properties.py tests/test_comstock.py tests/test_weather.py tests/test_weather_files.py tests/test_mcp_seb4.py tests/test_create_constructions.py tests/test_loop_operations.py tests/test_plant_loop_demand.py tests/test_sizing_properties.py tests/test_skill_retrofit.py tests/test_integration.py"
EXTRA_ENV="-e MCP_OSW_PATH=tests/assets/SEB_model/SEB4_baseboard/workflow.osw -e EXPECTED_EUI=1.8750760248144998 -e EXPECTED_EUI_RTOL=0.02 -e EXPECTED_EUI_ATOL=0.0"
;;
2)
# arm64 arch-sensitive coverage (the deb-vs-wheel / native-ISA risks):
# - SWIG memleak + stdout suppression (wheel-vs-deb behaviour)
# - measure apply + authoring (Ruby/Python exec + bundler on arm64)
# - an HVAC EnergyPlus sim (arm64 E+ build correctness)
# (The sandbox/security PoC runs on arm64 via the security workflow.)
FILES="tests/test_swig_memleak_cleanup.py tests/test_stdout_logger_silence.py tests/test_measures.py tests/test_measure_authoring.py tests/test_hvac_supply_sim.py"
EXTRA_ENV=""
;;
esac
docker run --rm \
-v "$PWD:/repo" -v "$PWD/runs:/runs" \
-e RUN_OPENSTUDIO_INTEGRATION -e MCP_SERVER_CMD \
-e RUN_OPENSTUDIO_INTEGRATION -e MCP_SERVER_CMD -e OSMCP_SANDBOX \
$EXTRA_ENV \
openstudio-mcp:arm64 bash -lc "cd /repo && pytest -vv -s $FILES"

Expand Down Expand Up @@ -182,6 +195,9 @@ jobs:
env:
RUN_OPENSTUDIO_INTEGRATION: "1"
MCP_SERVER_CMD: "openstudio-mcp"
# Run the integration suite confined (explicit, not via the code default).
# Measures/sims drop to the unprivileged sandbox uid + Landlock + seccomp.
OSMCP_SANDBOX: "auto"
run: |
mkdir -p runs
case ${{ matrix.shard }} in
Expand Down Expand Up @@ -215,6 +231,6 @@ jobs:
esac
docker run --rm \
-v "$PWD:/repo" -v "$PWD/runs:/runs" \
-e RUN_OPENSTUDIO_INTEGRATION -e MCP_SERVER_CMD \
-e RUN_OPENSTUDIO_INTEGRATION -e MCP_SERVER_CMD -e OSMCP_SANDBOX \
$EXTRA_ENV \
openstudio-mcp:dev bash -lc "cd /repo && pytest -vv -s $FILES"
51 changes: 51 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: security

# Dedicated workflow for the security / sandbox-confinement suite
# (tests/test_sandbox.py), kept separate from the main `ci` shards so the
# security tests run and report on their own. Runs the full sandbox tier
# (OSMCP_SANDBOX=auto) on BOTH amd64 and arm64 — the seccomp BPF and Landlock
# use arch-specific syscall numbers, so arm64 must be exercised explicitly.
# Individual tests still pin their own mode per fixture (off/posix/auto).

on:
push:
branches: [feat/measure-exec-sandbox]
pull_request:
workflow_dispatch:

jobs:
security-tests:
name: security-tests (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
dockerfile: docker/Dockerfile
- arch: arm64
runner: ubuntu-24.04-arm
dockerfile: docker/Dockerfile.arm64
steps:
- uses: actions/checkout@v4

- name: Build image (${{ matrix.arch }})
run: docker build -f ${{ matrix.dockerfile }} -t openstudio-mcp:dev .

- name: Run security suite (${{ matrix.arch }})
env:
RUN_OPENSTUDIO_INTEGRATION: "1"
MCP_SERVER_CMD: "openstudio-mcp"
OSMCP_SANDBOX: "auto"
run: |
mkdir -p runs
FILES=$(ls tests/test_sandbox.py tests/test_security_*.py 2>/dev/null || true)
if [ -z "$FILES" ]; then
echo "No security tests present in this checkout — nothing to run."
exit 0
fi
echo "[${{ matrix.arch }}] security tests under OSMCP_SANDBOX=$OSMCP_SANDBOX: $FILES"
docker run --rm -v "$PWD:/repo" -v "$PWD/runs:/runs" \
-e RUN_OPENSTUDIO_INTEGRATION -e MCP_SERVER_CMD -e OSMCP_SANDBOX \
openstudio-mcp:dev bash -lc "cd /repo && pytest -vv $FILES"
72 changes: 71 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "./tests/assets:/inputs",
"-v", "./tests/assets:/inputs:ro",
"-v", "./runs:/runs",
"-v", "./.claude/skills:/skills:ro",
"-e", "OPENSTUDIO_MCP_MODE=prod",
Expand Down Expand Up @@ -95,6 +95,76 @@ See **[docs/remote-multi-user.md](docs/remote-multi-user.md)** for setup, auth,

---

## Security and simulation sandbox

OpenStudio measures are Ruby or Python programs and must be treated as untrusted
code. EnergyPlus workflows can also invoke measure code. Docker isolates the
container from the host, while openstudio-mcp adds a second sandbox around the
child processes used by `apply_measure`, `test_measure`, measure metadata
refresh, and simulations.

The default `OSMCP_SANDBOX=auto` mode provides the following controls on Linux,
including Docker Desktop's Linux VM:

- **Privilege drop:** child processes run as the image's unprivileged
`sandbox` user (UID/GID 1001), while the MCP server retains only the
privileges needed to prepare run directories.
- **Filesystem policy:** Landlock denies filesystem access by default. The
current run or staged measure directory is writable; required system and
OpenStudio directories are read-only. `/repo`, `/inputs`, and other users'
run directories are not exposed to measure code.
- **Network policy:** seccomp denies outbound IP networking by default.
- **Secret isolation:** child processes receive an allowlisted environment
instead of inheriting API keys, tokens, and other server environment
variables.
- **Process hardening:** `no_new_privs` prevents privilege recovery through
setuid executables. Resource limits constrain generated file size and process
count, and simulations have a wall-clock timeout.
- **Staging:** input models, weather files, measures, and OSWs are copied into a
private run directory before execution. Escaping symlinks are rejected.

On Linux, `auto` fails closed if Landlock or the seccomp network filter cannot
be installed: the untrusted child process does not run. On native macOS or
Windows without Docker, kernel confinement is unavailable and the server warns
that only environment filtering is active. Use the Docker image when running
untrusted measures.

### Sandbox options

| Variable | Default | Behavior |
|----------|---------|----------|
| `OSMCP_SANDBOX` | `auto` | `auto`, `full`, and `landlock` request environment filtering, UID/GID drop, resource limits, Landlock filesystem confinement, and seccomp network denial. `posix` omits Landlock and seccomp. `off` disables child-process confinement and is only appropriate for trusted local code. Unknown values fall back to `auto`. |
| `OSMCP_SANDBOX_NET` | `deny` | `deny` blocks outbound IP networking. `allow` permits it for trusted measures that must fetch external resources. |
| `OSMCP_SANDBOX_UID` / `OSMCP_SANDBOX_GID` | `1001` | Account used for confined child processes. Values less than 1 are rejected. The default matches the `sandbox` user built into the image. |
| `OSMCP_SANDBOX_RLIMIT_FSIZE` | `10737418240` | Maximum size in bytes of one file created by a child process. `0` disables this limit. |
| `OSMCP_SANDBOX_RLIMIT_NPROC` | `1024` | Maximum processes/threads for the sandbox UID. `0` disables this limit. |
| `OSMCP_SANDBOX_RLIMIT_NOFILE` | `0` | Optional open-file descriptor limit. |
| `OSMCP_SANDBOX_RLIMIT_CPU` | `0` | Optional CPU-seconds limit. Disabled by default because annual simulations can be long-running. |
| `OSMCP_SANDBOX_RLIMIT_AS` | `0` | Optional virtual-memory limit. Prefer Docker memory limits because restrictive address-space limits can break EnergyPlus. |
| `OSMCP_SIM_TIMEOUT_SECONDS` | `7200` | Wall-clock timeout for a simulation. `0` disables the timeout. |

### Secure deployment guidance

- Mount `/inputs` read-only: `-v /host/inputs:/inputs:ro`.
- Mount only the output directory at `/runs`; any process allowed to write
`/runs` can modify that host directory by design.
- Mount workflow guides read-only at `/skills`, or use the copy already baked
into the image: `-v /host/.claude/skills:/skills:ro`.
- Do not mount the repository, home directory, Docker socket, credentials, or
broad host paths into production containers. The `/repo` source mount in the
testing commands is for development only.
- Keep `OSMCP_SANDBOX=auto` and `OSMCP_SANDBOX_NET=deny` for untrusted measure
authoring. Docker's `--network none` can provide an additional
container-wide network boundary when remote access is not required.
- Use Docker CPU, memory, PID, and disk quotas as outer limits. The in-process
resource limits are defense in depth, not replacements for container limits.

The sandbox protects the host and other run directories, but it intentionally
allows measure code to modify its own staged run directory. Treat resulting
OSM, SQL, report, and log files as untrusted outputs.

---

## Workflow skills

In Claude Code, 12 bundled skills add workflow automation and domain knowledge:
Expand Down
5 changes: 5 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*

# Unprivileged account that confined measure/simulation subprocesses drop to
# (see mcp_server/_sandbox_exec.py). The server stays root; only the exec'd
# measure/EnergyPlus child runs as this uid when OSMCP_SANDBOX is enabled.
RUN useradd -r -u 1001 -s /usr/sbin/nologin sandbox

# ComStock measures (openstudio-standards-based templates for space types,
# constructions, HVAC, schedules). Only the measures/ directory is kept (~50 MB).
ARG COMSTOCK_TAG=2025-3
Expand Down
15 changes: 15 additions & 0 deletions docker/Dockerfile.arm64
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
ARG OPENSTUDIO_SHA=241b8abb4d
ARG OS_BUNDLER_VERSION=2.4.10

FROM --platform=linux/arm64 ubuntu:24.04

Check warning on line 11 in docker/Dockerfile.arm64

View workflow job for this annotation

GitHub Actions / arm64-build

FROM --platform flag should not use a constant value

FromPlatformFlagConstDisallowed: FROM --platform flag should not use constant value "linux/arm64" More info: https://docs.docker.com/go/dockerfile/rule/from-platform-flag-const-disallowed/

Check warning on line 11 in docker/Dockerfile.arm64

View workflow job for this annotation

GitHub Actions / arm64-build

FROM --platform flag should not use a constant value

FromPlatformFlagConstDisallowed: FROM --platform flag should not use constant value "linux/arm64" More info: https://docs.docker.com/go/dockerfile/rule/from-platform-flag-const-disallowed/
ARG OPENSTUDIO_VERSION
ARG OPENSTUDIO_SHA
ARG OS_BUNDLER_VERSION
Expand All @@ -26,6 +26,10 @@
ruby-full \
&& rm -rf /var/lib/apt/lists/*

# Unprivileged account that confined measure/simulation subprocesses drop to
# (parity with docker/Dockerfile; see mcp_server/_sandbox_exec.py).
RUN useradd -r -u 1001 -s /usr/sbin/nologin sandbox

# OpenStudio arm64 — official NREL .deb. Pulls in libstdc++ etc. as deps.
RUN curl -fSL \
"https://github.com/NREL/OpenStudio/releases/download/v${OPENSTUDIO_VERSION}/OpenStudio-${OPENSTUDIO_VERSION}+${OPENSTUDIO_SHA}-Ubuntu-24.04-arm64.deb" \
Expand All @@ -40,9 +44,14 @@
# The nrel/openstudio base image does this; the .deb does not. Without it,
# every measure-based MCP tool fails at runtime (ComStock, common-measures,
# apply_measure). Mirrors NREL/docker-openstudio's /var/oscli setup.
# A stable symlink (/usr/local/openstudio-Ruby) is created in the RUN below so
# RUBYLIB can be a fixed ENV — the install dir name carries a build-SHA suffix.
# Both link and target live under /usr/local, which the sandbox Landlock policy
# grants read+exec.
RUN OPENSTUDIO_FOLDER=$(find /usr -maxdepth 2 -name "openstudio-${OPENSTUDIO_VERSION}*" -type d | head -n1) \
&& test -n "$OPENSTUDIO_FOLDER" || { echo "openstudio install dir not found"; exit 1; } \
&& echo "OPENSTUDIO_FOLDER=$OPENSTUDIO_FOLDER" \
&& ln -sfn "$OPENSTUDIO_FOLDER/Ruby" /usr/local/openstudio-Ruby \
&& gem install bundler -v ${OS_BUNDLER_VERSION} --no-document \
&& gem install zip --no-document \
&& mkdir -p /var/oscli \
Expand All @@ -53,6 +62,12 @@
&& bundle _${OS_BUNDLER_VERSION}_ install --path=gems --without=native_ext --jobs=4 --retry=3 \
&& test -d /var/oscli/gems || { echo "/var/oscli/gems missing after bundle install"; exit 1; }

# Let the system `ruby` resolve `require 'openstudio'` (parity with the amd64
# nrel/openstudio base, which sets RUBYLIB to the install's Ruby dir). test_measure
# runs Ruby unit tests via `ruby -I .` directly — `openstudio measure -r` does not
# run minitest — so without this the measure tests fail with a LoadError.
ENV RUBYLIB=/usr/local/openstudio-Ruby

# ComStock measures (openstudio-standards-based templates for space types,
# constructions, HVAC, schedules). Only the measures/ directory is kept (~50 MB).
ARG COMSTOCK_TAG=2025-3
Expand Down
130 changes: 130 additions & 0 deletions mcp_server/_landlock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Minimal Landlock (LSM) filesystem confinement via raw syscalls (ctypes).

Three syscalls, no external dependency (house style: grepable, owned). Applies a
read-deny-by-default policy to the current process and all its children: only the
enumerated read-only paths are readable/executable, and only the run dir is
writable. Requires no_new_privs (set by the caller) for unprivileged use; works
inside default-seccomp Docker >= 23.0 (the landlock_* syscalls are allowlisted
there). Returns the applied ABI, or 0 when Landlock is unavailable — the caller
degrades loudly rather than failing the run.
"""
from __future__ import annotations

import ctypes
import os
import stat

# Generic syscall table numbers (identical on x86_64 and aarch64).
_NR_CREATE_RULESET = 444
_NR_ADD_RULE = 445
_NR_RESTRICT_SELF = 446

_CREATE_RULESET_VERSION = 1 # query the ABI instead of creating a ruleset
_RULE_PATH_BENEATH = 1

# access_fs rights (bit positions). 0..12 = ABI 1, REFER = ABI 2, TRUNCATE = ABI 3.
_A_EXECUTE = 1 << 0
_A_WRITE_FILE = 1 << 1
_A_READ_FILE = 1 << 2
_A_READ_DIR = 1 << 3
_A_REMOVE_DIR = 1 << 4
_A_REMOVE_FILE = 1 << 5
_A_MAKE_CHAR = 1 << 6
_A_MAKE_DIR = 1 << 7
_A_MAKE_REG = 1 << 8
_A_MAKE_SOCK = 1 << 9
_A_MAKE_FIFO = 1 << 10
_A_MAKE_BLOCK = 1 << 11
_A_MAKE_SYM = 1 << 12
_A_REFER = 1 << 13 # ABI >= 2
_A_TRUNCATE = 1 << 14 # ABI >= 3
_A_IOCTL_DEV = 1 << 15 # ABI >= 5 — governs ioctl() on device files

_RO = _A_EXECUTE | _A_READ_FILE | _A_READ_DIR
_RW_BASE = (
_A_EXECUTE | _A_WRITE_FILE | _A_READ_FILE | _A_READ_DIR
| _A_REMOVE_DIR | _A_REMOVE_FILE | _A_MAKE_CHAR | _A_MAKE_DIR | _A_MAKE_REG
| _A_MAKE_SOCK | _A_MAKE_FIFO | _A_MAKE_BLOCK | _A_MAKE_SYM
)
# Rights that are meaningful on a non-directory. Landlock's add_rule returns
# EINVAL if a rule on a FILE carries directory-only rights (READ_DIR, MAKE_*,
# REMOVE_*), so a per-file rule (e.g. /dev/null) MUST be masked to these.
_FILE_ONLY = _A_EXECUTE | _A_WRITE_FILE | _A_READ_FILE | _A_TRUNCATE | _A_IOCTL_DEV


class _RulesetAttr(ctypes.Structure):
# Only handled_access_fs — 8 bytes, valid on every ABID (kernel reads the
# field set implied by `size`); avoids the ABI>=4 net field tripping abi3.
_fields_ = [("handled_access_fs", ctypes.c_uint64)]


class _PathBeneathAttr(ctypes.Structure):
# MUST be packed (12 bytes). The kernel UAPI declares
# struct landlock_path_beneath_attr { __u64 allowed_access; __s32 parent_fd; }
# __attribute__((packed));
# so _pack_=1 matches the kernel exactly. Do NOT "fix" this to natural
# alignment (16 bytes) — that would mismatch the kernel struct. Verified by
# the integration tests, which only pass if add_rule parsed this correctly.
_pack_ = 1
_fields_ = [("allowed_access", ctypes.c_uint64), ("parent_fd", ctypes.c_int32)]

Comment on lines +61 to +70

def restrict(ro_paths: list[str], rw_paths: list[str]) -> int:
"""Confine the filesystem to ro_paths (read+exec) and rw_paths (read+write).

Returns the applied Landlock ABI (>=1), or 0 if Landlock is unavailable or
the ruleset could not be enforced. Missing paths are skipped silently.
"""
libc = ctypes.CDLL("libc.so.6", use_errno=True)

abi = libc.syscall(_NR_CREATE_RULESET, None, 0, _CREATE_RULESET_VERSION)
if abi <= 0:
return 0

handled = _RW_BASE
if abi >= 2:
handled |= _A_REFER
if abi >= 3:
handled |= _A_TRUNCATE
if abi >= 5:
handled |= _A_IOCTL_DEV # govern device ioctls (esp. with /dev writable)

attr = _RulesetAttr(handled_access_fs=handled)
rs_fd = libc.syscall(_NR_CREATE_RULESET, ctypes.byref(attr), ctypes.sizeof(attr), 0)
if rs_fd < 0:
return 0

def _add(path: str, access: int) -> bool:
"""True if the path is absent (optional, skip) or its rule was added;
False only if a PRESENT path's rule failed (a broken ruleset)."""
try:
fd = os.open(path, os.O_PATH | os.O_CLOEXEC)
except OSError:
return True # absent path — optional, not a failure
try:
acc = access & handled
# A rule on a non-directory (e.g. /dev/null) must carry only file
# rights — dir-only bits make add_rule fail with EINVAL.
if not stat.S_ISDIR(os.fstat(fd).st_mode):
acc &= _FILE_ONLY
pba = _PathBeneathAttr(allowed_access=acc, parent_fd=fd)
return libc.syscall(_NR_ADD_RULE, rs_fd, _RULE_PATH_BENEATH, ctypes.byref(pba), 0) == 0
finally:
os.close(fd)

ok = True
for p in ro_paths:
ok = _add(p, _RO) and ok
for p in rw_paths:
ok = _add(p, handled) and ok # full set for the writable run dir
if not ok:
# A present path's rule was rejected → enforce nothing rather than a
# half-applied policy; the caller (auto tier) fails closed on a 0 return.
os.close(rs_fd)
return 0

# no_new_privs is set by the caller; restrict_self enforces the ruleset on
# this process and everything it execs/forks.
rc = libc.syscall(_NR_RESTRICT_SELF, rs_fd, 0)
os.close(rs_fd)
return abi if rc == 0 else 0
Loading
Loading