diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c26c0a1..772a3bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,7 @@ jobs: integration: name: integration suite runs-on: ubuntu-24.04 + timeout-minutes: 20 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..c72d828 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,92 @@ +name: pages + +on: + push: + branches: [main] + paths: + - 'site/**' + - '.github/workflows/pages.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Refuse a page that asks for a file it does not have + run: | + missing=0 + for asset in $(grep -oE '(href|src)="[^"]+"' site/index.html \ + | grep -vE 'https?:|#' \ + | sed -E 's/.*="([^"]+)"/\1/' | sort -u); do + if [ ! -f "site/$asset" ]; then + echo "site/index.html references $asset, which is not in site/" + missing=1 + fi + done + exit $missing + + - name: Refuse a link to a section that does not exist + run: | + grep -oE 'href="#[^"]+"' site/index.html | sed -E 's/href="#(.*)"/\1/' | sort -u > /tmp/anchors + grep -oE '
/tmp/sections + if ! comm -23 /tmp/anchors /tmp/sections | grep -q .; then + exit 0 + fi + echo "these links point at sections the page does not have:" + comm -23 /tmp/anchors /tmp/sections + exit 1 + + - name: Refuse a theme token the stylesheet does not define + run: | + grep -oE 'var\(--ms-[a-z0-9-]+\)' site/site.css \ + | sed -E 's/var\((.*)\)/\1/' | sort -u > /tmp/used + grep -oE '\--ms-[a-z0-9-]+:' site/meridian.css | tr -d ':' | sort -u > /tmp/defined + if ! comm -23 /tmp/used /tmp/defined | grep -q .; then + exit 0 + fi + echo "site.css uses tokens meridian.css does not define, so those rules are dropped:" + comm -23 /tmp/used /tmp/defined + exit 1 + + - name: Refuse a theme that has drifted from the other marstack sites + run: | + for asset in meridian.css site.css; do + remote=$(curl -sSL --fail \ + "https://raw.githubusercontent.com/MarStack-Labs/marstack-access/main/site/$asset") || { + echo "cannot read $asset from marstack-access; skipping the drift check" + continue + } + if [ "$(printf '%s' "$remote" | sha256sum | cut -d' ' -f1)" \ + != "$(sha256sum "site/$asset" | cut -d' ' -f1)" ]; then + echo "site/$asset differs from the copy marstack-access ships" + echo "this repository vendors the theme because it has no console to copy it from," + echo "so drift has to be caught here rather than by a build step" + exit 1 + fi + done + + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - id: deploy + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 7af6cdc..1c4070b 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ An OCI-compliant container runtime written from scratch in Rust — the layer that Docker and Kubernetes sit on top of, built to understand it rather than to replace it. +**[Usage guide →](https://marstack-labs.github.io/marstack-container/)** + `mars` implements the [OCI runtime-spec](https://github.com/opencontainers/runtime-spec): it takes a filesystem bundle and a `config.json` and uses Linux namespaces, cgroup v2, OverlayFS, capabilities and seccomp to turn it into an isolated process. diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..069312b --- /dev/null +++ b/site/index.html @@ -0,0 +1,417 @@ + + + + + +marstack-container — an OCI container runtime written from scratch in Rust + + + + + + + + +
+ pre-release · v0.1.0 +

The layer Docker hides, written out and measured.

+

+ An OCI runtime that takes a filesystem bundle and a config.json and turns it into an + isolated process, using nothing but Linux namespaces, cgroup v2, OverlayFS, capabilities and + seccomp. Not a runc competitor. Built because reading about that layer does not build + a model of it. +

+ +
+
+ The smallest privileged surface measured + 1542 distinct kernel functions to start a container, against crun's 2030 and runc's 2361. +
+
+ Failures with evidence + Nine production failure modes reproduced, each read from kernel state rather than from what the runtime claims. +
+
+ Single-threaded until it forks + setns(2) refuses a multi-threaded process. Rust stays single-threaded, so exec needs no C constructor. +
+
+ A drop-in Docker runtime + docker run --runtime=mars, with an OTLP span per startup phase and no background thread. +
+
+
+ +
+ + +
+ +
+

What it is

+

+ mars implements the + OCI runtime-spec. It is the layer + Docker and Kubernetes sit on top of — the one that actually creates the namespaces, writes the + cgroups, assembles the rootfs and drops the capabilities. +

+

+ It is not meant for production, and it is not trying to displace runc. It exists + because container failures in production happen in the layer Docker hides, and the only way to + build a model of that layer is to write it. +

+
+ + + + + + +
OCI validation suite26 passed, against runc 1.5.1's 22 on the same host
Integration suite128 assertions, every one reading kernel state rather than the runtime's own claims
Kernel attack surface1542 distinct functions to start a container, against crun's 2030 and runc's 2361
Docker drop-inrun, run -it, exec, stop, --memory
LanguageRust, deliberately — see Lifecycle
+
+
+ +
+

Install

+

+ Check the host first. Most of what can go wrong is the host, not the build. +

+
./scripts/preflight.sh
+
+

+ The one that stops people: a VPS that is itself a container. OpenVZ, LXC and + most budget plans share the provider's kernel, which blocks pivot_root and cgroup + delegation. If systemd-detect-virt -c names anything other than none, + mars cannot run there and no amount of sudo changes it — you need + KVM, Xen, or bare metal. +

+

+ The other common blocker is a hybrid cgroup hierarchy. There is no v1 driver, so + /sys/fs/cgroup must be cgroup2fs. +

+
+

On a Debian or Ubuntu host that passes preflight:

+
sudo apt-get install -y build-essential pkg-config libseccomp-dev jq attr uidmap
+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+cargo build --release
+sudo install -m 0755 target/release/mars /usr/local/bin/mars
+

+ Linux only: it needs namespaces, cgroups and libseccomp, and does not build on + macOS. A Lima VM definition is included so the environment is + reproducible. +

+
limactl start --name=mars-dev ./lima/mars-dev.yaml
+limactl shell mars-dev
+
+ +
+

First container

+

Generate a bundle, put a rootfs in it, run it.

+
mars spec --bundle /tmp/demo
+./scripts/make-rootfs.sh /tmp/demo/rootfs
+cd /tmp/demo && sudo mars run demo
+

+ That is a memory limit, a PID namespace, a pivoted root and a seccomp filter, all built by + writing to the kernel rather than by asking a library to do it. +

+
$ docker run --rm --runtime=mars --memory=64m alpine:3.20 cat /sys/fs/cgroup/memory.max
+67108864
+

+ Docker, using mars instead of runc, enforcing a limit through a cgroup + that mars created and wrote itself. +

+
+ +
+

Lifecycle

+

+ The full spec surface is implemented: create, start, + state, kill, delete, exec, + list, ps, pause, resume, + events, update, spec, features, all five + lifecycle hooks, and a console socket over SCM_RIGHTS. +

+

+ The three-level fork chain is forced by the kernel, not a style choice. +

+
mars create
+  │  socketpair(AF_UNIX)
+  ├─ fork() ─────────► [intermediate]
+  │                      unshare(NEWUSER|NEWNS|NEWPID|NEWUTS|NEWIPC|NEWNET|NEWTIME)
+  │  ◄── "map me" ────
+  │  write /proc/<pid>/setgroups, uid_map, gid_map
+  │  ─── "mapped" ──►   setresuid(0) — a new user namespace leaves you unmapped
+  │                      fork() ──────────► [container init, PID 1]
+  │                        exit                mounts, pivot_root, devices
+  │  write pid to cgroup.procs                 caps, seccomp, no_new_privs
+  │  ◄── "ready" ─────────────────────────────  block on exec.fifo
+mars create returns; the container stays alive, waiting
+mars start ──── opens the fifo ─────────────────► execve(user process)
+

+ unshare(CLONE_NEWPID) does not move the caller into the new PID + namespace — the next fork() is what becomes PID 1, and the same holds for + CLONE_NEWTIME. uid_map has to be written from outside the user + namespace by a privileged process, because a process cannot map itself, so the two ends need + two-way synchronisation. And create must return while the container stays alive, + so the wait has to be on something a later unrelated process can reach: a fifo, opened + O_PATH by the parent so it does not count as an opener, reopened by the init + through /proc/self/fd/N because the path is gone after pivot_root. +

+

+ Rust instead of Go, deliberately. setns(2) refuses to move a + multi-threaded process into a new mount or user namespace. Go is already multi-threaded when + main starts, which is why runc ships a C constructor that runs before + the Go runtime initialises. Rust stays single-threaded until the fork, so mars exec + calls setns directly — and asserts the property by counting + /proc/self/task rather than assuming it. +

+
+ +
+

cgroup v2

+

+ The driver is hand-written against cgroupfs rather than delegated to a crate — + memory, cpu, pids, cpuset and + io, written directly. Delegating it would delegate away the main thing this + project is for. +

+

+ Writing one pid to cgroup.procs is the most expensive step in starting a + container — 55% to 79% of cold start, measured. Creating the cgroup and writing every + limit takes 279µs; moving one process into it takes 7–15ms, because the first migration into a + fresh cgroup pays for per-cgroup controller setup and an RCU grace period across every CPU. + Every container gets a fresh cgroup, so nothing is ever amortised. +

+
trace 10a2ae966c82f09af3c6d2282991b79c  25 spans  11892us total
+     458us    279us  cgroup                      create it, write every limit
+    1012us    895us  intermediate.unshare.net    a whole network stack
+    2358us   7065us  cgroup.attach               write one pid to one file
+    9438us    492us  init.rootfs.mount
+    9931us    232us  init.pivot_root
+
+ +
+

Layered rootfs

+

+ The overlay rootfs is a documented extension, not a spec feature. The + runtime-spec has no field for image layers — that is the image-spec's job, done by containerd or + Docker before the runtime is called. mars reads three + dev.mars.overlay.* annotations instead, so the config.json stays valid + for any other runtime, which will ignore them. +

+
sudo -E ./scripts/oci-bundle.sh -i alpine:3.20 /tmp/layered
+cd /tmp/layered && sudo mars run demo
+find /tmp/layered/diff -mindepth 1        # everything the container wrote
+

+ The .wh. markers from the image tarballs are converted into real OverlayFS + whiteouts, and process, env and cwd are taken from the + image config. +

+
+

+ A mount option string over 4096 bytes is truncated, not rejected. Enough + OverlayFS layers and the kernel silently cuts the lowerdir= list mid-path, then + reports ENOENT against the mount source — an error naming neither the + truncation nor the layer count. This is what the short symlinks in + /var/lib/docker/overlay2/l/ are for. +

+
+
+ +
+

Hardening

+

+ Capabilities, seccomp, no_new_privs, read-only rootfs, + maskedPaths and readonlyPaths, sysctls, rlimits, + oomScoreAdj, and user namespaces with a newuidmap fallback. +

+

+ readonlyPaths cannot simply remount a path read-only, because a path is not a + mount. It has to be made one first, by bind-mounting it onto itself, and only then remounted + with MS_RDONLY. maskedPaths binds /dev/null over a file + and an empty read-only tmpfs over a directory — which is how + /proc/kcore, a mapping of all physical memory, stops being readable inside a + container. +

+

Run the OCI validation suite against either runtime and compare:

+
sudo -E ./scripts/run-validation.sh
+sudo -E RUNTIME=runc ./scripts/run-validation.sh
+
+ +
+

As a Docker runtime

+
sudo ./scripts/install-docker-runtime.sh          # TRACE=1 also logs how Docker calls it
+docker run --rm -it --runtime=mars alpine:3.20 sh
+

+ TRACE=1 is there because the interesting part is not that it works, but the exact + sequence of calls Docker makes to a runtime it has never seen before. +

+
+ +
+

Startup traces

+

+ Every startup phase emits an OTLP span, accepted by Tempo. No collector needed to look at them: +

+
./scripts/otlp-echo.py 4318 &
+MARS_OTLP_ENDPOINT=127.0.0.1:4318 sudo -E mars run demo
+

+ The exporter is hand-written for a hard reason. The OpenTelemetry SDK runs its + exporter on a background thread. A process that forks must not have one — only + async-signal-safe work is legal in the child — and a process that calls setns must + not either. So the exporter is around 120 lines that build OTLP/HTTP JSON and write one POST: + no threads, nothing running at fork() time. +

+
+ +
+

Attack surface

+

+ A runtime spends its whole life as root. It holds CAP_SYS_ADMIN, it creates the + namespaces and writes the cgroups, and then it exits. Everything dangerous it will ever do, it + does in that window — and published runtime comparisons measure startup latency and per-container + memory, not that. +

+

+ scripts/hap-bench.sh counts the distinct host kernel functions a runtime traverses + while privileged, traced with ftrace, following Bottomley's horizontal attack + profile. Five runs per cell, median, idle subtracted, all three runtimes sharing one unmodified + seccomp profile: +

+
+ + + + + +
runtimerunvolexectty
mars154215424941545
crun 1.14.1203020308891984
runc 1.5.12361235511342370
+
+

+ 24% less kernel than crun and 35% less than runc on a plain start, and + 44% / 56% less on exec — the operation a Kubernetes exec probe repeats for the + lifetime of a pod. +

+

+ It is not that mars skips work. Namespace creation, + pivot_root, cgroup setup and capability handling all appear at parity. Of the 1000 + functions runc reaches and mars does not, only 10 are thread, futex or + scheduler functions — the Go runtime is not the explanation. 301 are file, path and + /proc traversal. +

+
+

+ Counting function entries cannot see control flow inside a function, so a ten-line + function and a five-hundred-line one count the same. Basic-block coverage through + kcov is the honest version, and it needs a kernel built for the purpose. One + architecture, one kernel version, and a guest kernel rather than bare metal: the comparison + between runtimes holds because all three meet identical conditions, but the absolute values do + not travel. +

+
+

+ Widening that benchmark to cover seccomp is what found the one real defect so far: a rule that + restates the filter's default action, which libseccomp refuses as redundant and + mars was treating as fatal. Every standard profile carries dozens of them. +

+
+ +
+

Failure modes

+

+ Nine failures that happen in production, reproduced here with the evidence read out of the + kernel: +

+
    +
  • a pod is OOMKilled with exit 137 — and memory.events says how close it had been, for how long
  • +
  • a container ignores SIGTERM for the full grace period — because PID 1 gets no default handlers
  • +
  • zombies pile up until fork() fails — because PID 1 was never written to be an init
  • +
  • CPU throttles at 10% utilisation — because cpu.max is a quota, not a share
  • +
  • a fix applied with exec survives a restart but not a recreate — because it lived in the OverlayFS upper layer, which is the container
  • +
  • a rootless bind mount is unwritable at mode 0777 — because the uid has no mapping and reads as nobody
  • +
  • EPERM mounting /sys/fs/cgroup — because the bundle asked for cgroup v1 on a v2 host
  • +
+
+

+ An OOM kill does not reliably produce exit 137. The kernel picks its victim by + badness score, usually the allocating process rather than PID 1. Kill a child and PID 1 carries + on: the container exits 0 having lost a process, with oom_kill=1 in + memory.events and nothing else to show for it. Kubernetes only marks a pod + OOMKilled when PID 1 dies of signal 9, so this case restarts nothing and alerts + nobody. +

+
+
+ +
+

Before production

+
+

+ Do not put this in production. That is not modesty about code quality — it is + the stated purpose. This exists to build a model of the runtime layer, and the things a + production runtime needs that this deliberately does not have are listed below. +

+
+

+ Not finished: rootless without any privilege. The user namespace machinery works + and is tested, but mars still expects to be started with privilege. A fully rootless + run also needs a delegated cgroup under user.slice, + fuse-overlayfs or userxattr for whiteouts, and + slirp4netns for networking. +

+

+ Verified environment: Ubuntu 24.04, kernel 6.8, aarch64, pure cgroup v2 with + cpu cpuset io memory pids delegated, unprivileged user namespaces enabled, and + runc 1.5.1 plus Docker 29.7.2 alongside for comparison. +

+
+ +
+

Out of scope

+

Left out on purpose, each because something else already owns it:

+
+ + + + + + + + +
Image pulling from registriescontainerd's job; the runtime is called after the bundle exists
CNI networkingthe network namespace is created; populating it belongs to a plugin
CRIthe kubelet interface sits a layer above an OCI runtime
cgroup v1, systemd cgroup drivera pure v2 hierarchy is the target, and a second driver would double the surface for no insight
Checkpoint and restorea project of its own
SELinux and AppArmor labelsparsed and ignored, rather than silently claimed
SCMP_ACT_NOTIFYneeds a listener process to receive the notification fd
+
+
+ +
+
+ + + + + diff --git a/site/meridian.css b/site/meridian.css new file mode 100644 index 0000000..5cfae7c --- /dev/null +++ b/site/meridian.css @@ -0,0 +1,747 @@ +:root { + --ms-cyan: #22d3ee; + --ms-sky: #7dd3fc; + --ms-indigo: #6366f1; + --ms-violet: #a78bfa; + + --ms-indigo-300: #a5b4fc; + --ms-indigo-400: #818cf8; + --ms-indigo-500: #6366f1; + --ms-indigo-600: #4f46e5; + --ms-indigo-700: #4338ca; + + --ms-gray-0: #ffffff; + --ms-gray-50: #f6f8fa; + --ms-gray-100: #eff2f5; + --ms-gray-200: #d1d9e0; + --ms-gray-300: #b7bfc9; + --ms-gray-400: #818b98; + --ms-gray-500: #59636e; + --ms-gray-600: #3d444d; + --ms-gray-700: #30363d; + --ms-gray-800: #21262d; + --ms-gray-850: #161b22; + --ms-gray-900: #0d1117; + --ms-gray-950: #010409; + + --ms-slate-950: #080c12; + --ms-slate-900: #0f141d; + --ms-slate-850: #161d28; + --ms-slate-800: #1e2735; + --ms-slate-700: #2a3648; + --ms-slate-600: #374763; + + --ms-navy-950: #070c16; + --ms-navy-900: #0d1522; + --ms-navy-850: #141f33; + --ms-navy-800: #1c2942; + --ms-navy-700: #27385a; + --ms-navy-600: #344a73; + + --ms-midnight-950: #04071a; + --ms-midnight-900: #080e26; + --ms-midnight-850: #101838; + --ms-midnight-800: #182149; + --ms-midnight-700: #252f60; + --ms-midnight-600: #33407d; + + --ms-ice-50: #f3f7fc; + --ms-ice-100: #e7edf6; + --ms-ice-200: #ccd8e8; + --ms-ice-300: #aebed4; + + --ms-sp-1: 4px; + --ms-sp-2: 8px; + --ms-sp-3: 12px; + --ms-sp-4: 16px; + --ms-sp-5: 24px; + --ms-sp-6: 32px; + --ms-sp-7: 48px; + + --ms-r-sm: 4px; + --ms-r: 6px; + --ms-r-md: 8px; + --ms-r-lg: 10px; + --ms-r-plate: 14px; + --ms-r-full: 999px; + + --ms-sidebar-w: 252px; + --ms-topbar-h: 56px; + + --ms-fs-xs: 11px; + --ms-fs-sm: 12px; + --ms-fs-base: 13px; + --ms-fs-md: 14px; + --ms-fs-lg: 16px; + --ms-fs-xl: 20px; + --ms-fs-2xl: 26px; + + --ms-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --ms-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + + --ms-track-tight: -0.01em; + --ms-track-eyebrow: 0.14em; + --ms-track-wordmark: 0.1em; + + --ms-brand-gradient: linear-gradient(90deg, var(--ms-cyan) 0%, var(--ms-indigo) 50%, var(--ms-violet) 100%); + --ms-brand-gradient-diag: linear-gradient(135deg, var(--ms-cyan) 0%, var(--ms-indigo) 50%, var(--ms-violet) 100%); + --ms-wordmark-gradient: linear-gradient(90deg, #e6edf3 0%, var(--ms-sky) 60%, var(--ms-violet) 100%); + + --ms-bg: var(--ms-navy-900); + --ms-bg-sunken: var(--ms-navy-950); + --ms-bg-raised: var(--ms-navy-850); + --ms-bg-overlay: var(--ms-navy-800); + --ms-bg-inset: var(--ms-navy-950); + --ms-bg-hover: rgba(190, 215, 255, 0.06); + --ms-bg-active: rgba(190, 215, 255, 0.1); + --ms-term-bg: var(--ms-navy-950); + + --ms-border: var(--ms-navy-700); + --ms-border-strong: var(--ms-navy-600); + --ms-border-subtle: rgba(190, 215, 255, 0.09); + + --ms-fg: #e6edf3; + --ms-fg-muted: #8b949e; + --ms-fg-faint: #6e7681; + --ms-fg-on-accent: #ffffff; + + --ms-accent: var(--ms-indigo-400); + --ms-accent-solid: var(--ms-indigo-600); + --ms-accent-solid-hover: var(--ms-indigo-500); + --ms-accent-tint: rgba(99, 102, 241, 0.16); + --ms-accent-line: rgba(129, 140, 248, 0.4); + + --ms-focus: var(--ms-cyan); + --ms-focus-tint: rgba(34, 211, 238, 0.14); + --ms-focus-ring: 0 0 0 2px var(--ms-bg), 0 0 0 4px var(--ms-focus); + + --ms-ok: #3fb950; + --ms-ok-tint: rgba(63, 185, 80, 0.15); + --ms-warn: #d29922; + --ms-warn-tint: rgba(210, 153, 34, 0.15); + --ms-danger: #f85149; + --ms-danger-tint: rgba(248, 81, 73, 0.15); + --ms-info: #58a6ff; + --ms-info-tint: rgba(88, 166, 255, 0.15); + --ms-pending: #a371f7; + --ms-pending-tint: rgba(163, 113, 247, 0.15); + --ms-idle: #8b949e; + --ms-idle-tint: rgba(139, 148, 158, 0.14); + + --ms-grid-line: var(--ms-navy-850); + --ms-shadow-sm: 0 1px 2px rgba(4, 7, 22, 0.55); + --ms-shadow: 0 6px 24px rgba(4, 7, 22, 0.6); + --ms-shadow-lg: 0 24px 60px rgba(4, 7, 22, 0.7); +} + +:root[data-surface="slate"] { + --ms-bg: var(--ms-slate-900); + --ms-bg-sunken: var(--ms-slate-950); + --ms-bg-raised: var(--ms-slate-850); + --ms-bg-overlay: var(--ms-slate-800); + --ms-bg-inset: var(--ms-slate-950); + --ms-term-bg: var(--ms-slate-950); + --ms-border: var(--ms-slate-700); + --ms-border-strong: var(--ms-slate-600); + --ms-grid-line: var(--ms-slate-850); + --ms-shadow-sm: 0 1px 2px rgba(8, 12, 18, 0.55); + --ms-shadow: 0 6px 24px rgba(8, 12, 18, 0.6); + --ms-shadow-lg: 0 24px 60px rgba(8, 12, 18, 0.7); +} + +:root[data-surface="github"] { + --ms-bg: var(--ms-gray-900); + --ms-bg-sunken: var(--ms-gray-950); + --ms-bg-raised: var(--ms-gray-850); + --ms-bg-overlay: var(--ms-gray-800); + --ms-bg-inset: var(--ms-gray-950); + --ms-term-bg: var(--ms-gray-950); + --ms-border: var(--ms-gray-700); + --ms-border-strong: var(--ms-gray-600); + --ms-grid-line: var(--ms-gray-850); + --ms-bg-hover: rgba(255, 255, 255, 0.045); + --ms-bg-active: rgba(255, 255, 255, 0.075); + --ms-border-subtle: rgba(255, 255, 255, 0.07); +} + +:root[data-surface="midnight"] { + --ms-bg: var(--ms-midnight-900); + --ms-bg-sunken: var(--ms-midnight-950); + --ms-bg-raised: var(--ms-midnight-850); + --ms-bg-overlay: var(--ms-midnight-800); + --ms-bg-inset: var(--ms-midnight-950); + --ms-term-bg: var(--ms-midnight-950); + --ms-border: var(--ms-midnight-700); + --ms-border-strong: var(--ms-midnight-600); + --ms-grid-line: var(--ms-midnight-850); + --ms-accent: var(--ms-indigo-300); + --ms-accent-solid: var(--ms-indigo-500); + --ms-accent-solid-hover: var(--ms-indigo-400); + --ms-accent-tint: rgba(165, 180, 252, 0.16); + --ms-accent-line: rgba(165, 180, 252, 0.42); + --ms-shadow-sm: 0 1px 2px rgba(4, 7, 26, 0.6); + --ms-shadow: 0 6px 24px rgba(4, 7, 26, 0.65); + --ms-shadow-lg: 0 24px 60px rgba(4, 7, 26, 0.75); +} + +@media (prefers-color-scheme: light) { + :root:not([data-theme="dark"]) { + --ms-bg: var(--ms-gray-0); + --ms-bg-sunken: var(--ms-ice-50); + --ms-bg-raised: var(--ms-gray-0); + --ms-bg-overlay: var(--ms-gray-0); + --ms-bg-inset: var(--ms-ice-100); + --ms-bg-hover: rgba(23, 42, 74, 0.05); + --ms-bg-active: rgba(23, 42, 74, 0.085); + + --ms-border: var(--ms-ice-200); + --ms-border-strong: var(--ms-ice-300); + --ms-border-subtle: rgba(23, 42, 74, 0.09); + + --ms-fg: #1f2328; + --ms-fg-muted: var(--ms-gray-500); + --ms-fg-faint: var(--ms-gray-400); + + --ms-accent: var(--ms-indigo-600); + --ms-accent-solid: var(--ms-indigo-600); + --ms-accent-solid-hover: var(--ms-indigo-700); + --ms-accent-tint: rgba(79, 70, 229, 0.1); + --ms-accent-line: rgba(79, 70, 229, 0.35); + + --ms-focus: #0891b2; + --ms-focus-tint: rgba(8, 145, 178, 0.1); + + --ms-ok: #1a7f37; + --ms-ok-tint: rgba(26, 127, 55, 0.11); + --ms-warn: #9a6700; + --ms-warn-tint: rgba(154, 103, 0, 0.11); + --ms-danger: #cf222e; + --ms-danger-tint: rgba(207, 34, 46, 0.1); + --ms-info: #0969da; + --ms-info-tint: rgba(9, 105, 218, 0.1); + --ms-pending: #8250df; + --ms-pending-tint: rgba(130, 80, 223, 0.1); + --ms-idle: var(--ms-gray-500); + --ms-idle-tint: rgba(89, 99, 110, 0.11); + + --ms-grid-line: var(--ms-ice-100); + --ms-shadow-sm: 0 1px 2px rgba(31, 35, 40, 0.08); + --ms-shadow: 0 8px 24px rgba(31, 35, 40, 0.1); + --ms-shadow-lg: 0 24px 60px rgba(31, 35, 40, 0.16); + } +} + +:root[data-theme="light"] { + --ms-bg: var(--ms-gray-0); + --ms-bg-sunken: var(--ms-ice-50); + --ms-bg-raised: var(--ms-gray-0); + --ms-bg-overlay: var(--ms-gray-0); + --ms-bg-inset: var(--ms-ice-100); + --ms-bg-hover: rgba(23, 42, 74, 0.05); + --ms-bg-active: rgba(23, 42, 74, 0.085); + + --ms-border: var(--ms-ice-200); + --ms-border-strong: var(--ms-ice-300); + --ms-border-subtle: rgba(23, 42, 74, 0.09); + + --ms-fg: #1f2328; + --ms-fg-muted: var(--ms-gray-500); + --ms-fg-faint: var(--ms-gray-400); + + --ms-accent: var(--ms-indigo-600); + --ms-accent-solid: var(--ms-indigo-600); + --ms-accent-solid-hover: var(--ms-indigo-700); + --ms-accent-tint: rgba(79, 70, 229, 0.1); + --ms-accent-line: rgba(79, 70, 229, 0.35); + + --ms-focus: #0891b2; + --ms-focus-tint: rgba(8, 145, 178, 0.1); + + --ms-ok: #1a7f37; + --ms-ok-tint: rgba(26, 127, 55, 0.11); + --ms-warn: #9a6700; + --ms-warn-tint: rgba(154, 103, 0, 0.11); + --ms-danger: #cf222e; + --ms-danger-tint: rgba(207, 34, 46, 0.1); + --ms-info: #0969da; + --ms-info-tint: rgba(9, 105, 218, 0.1); + --ms-pending: #8250df; + --ms-pending-tint: rgba(130, 80, 223, 0.1); + --ms-idle: var(--ms-gray-500); + --ms-idle-tint: rgba(89, 99, 110, 0.11); + + --ms-grid-line: var(--ms-ice-100); + --ms-shadow-sm: 0 1px 2px rgba(31, 35, 40, 0.08); + --ms-shadow: 0 8px 24px rgba(31, 35, 40, 0.1); + --ms-shadow-lg: 0 24px 60px rgba(31, 35, 40, 0.16); +} + +.ms-root { + background: var(--ms-bg); + color: var(--ms-fg); + font-family: var(--ms-font); + font-size: var(--ms-fs-base); + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +.ms-mono { + font-family: var(--ms-font-mono); + font-size: 0.94em; + letter-spacing: var(--ms-track-tight); +} + +.ms-eyebrow { + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-xs); + font-weight: 700; + text-transform: uppercase; + letter-spacing: var(--ms-track-eyebrow); + color: var(--ms-fg-muted); +} + +.ms-wordmark { + font-family: var(--ms-font-mono); + font-weight: 700; + letter-spacing: var(--ms-track-wordmark); + background: var(--ms-wordmark-gradient); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.ms-hairline { + height: 1px; + border: 0; + background: var(--ms-brand-gradient); + opacity: 0.85; +} + +.ms-plate { + position: relative; + background: var(--ms-bg-raised); + border: 1px solid var(--ms-border); + border-radius: var(--ms-r-plate); + background-image: linear-gradient(var(--ms-grid-line) 1px, transparent 1px), + linear-gradient(90deg, var(--ms-grid-line) 1px, transparent 1px); + background-size: 26px 26px; +} + +.ms-card { + background: var(--ms-bg-raised); + border: 1px solid var(--ms-border); + border-radius: var(--ms-r-lg); + box-shadow: var(--ms-shadow-sm); +} + +.ms-pill { + display: inline-flex; + align-items: center; + gap: var(--ms-sp-2); + padding: 2px var(--ms-sp-2); + border-radius: var(--ms-r-full); + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-xs); + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + white-space: nowrap; +} + +.ms-pill::before { + content: ""; + width: 6px; + height: 6px; + border-radius: var(--ms-r-full); + background: currentColor; +} + +.ms-pill-ok { color: var(--ms-ok); background: var(--ms-ok-tint); } +.ms-pill-warn { color: var(--ms-warn); background: var(--ms-warn-tint); } +.ms-pill-danger { color: var(--ms-danger); background: var(--ms-danger-tint); } +.ms-pill-info { color: var(--ms-info); background: var(--ms-info-tint); } +.ms-pill-pending { color: var(--ms-pending); background: var(--ms-pending-tint); } +.ms-pill-idle { color: var(--ms-idle); background: var(--ms-idle-tint); } + +.ms-pill-pending::before { + animation: ms-pulse 1.6s ease-in-out infinite; +} + +@keyframes ms-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.25; } +} + +@media (prefers-reduced-motion: reduce) { + .ms-pill-pending::before { animation: none; } +} + +.ms-btn { + display: inline-flex; + align-items: center; + gap: var(--ms-sp-2); + height: 30px; + padding: 0 var(--ms-sp-3); + border: 1px solid var(--ms-border); + border-radius: var(--ms-r); + background: var(--ms-bg-overlay); + color: var(--ms-fg); + font: inherit; + font-size: var(--ms-fs-sm); + cursor: pointer; +} + +.ms-btn:hover { background: var(--ms-bg-active); border-color: var(--ms-border-strong); } + +.ms-btn-primary { + background: var(--ms-accent-solid); + border-color: var(--ms-accent-solid); + color: var(--ms-fg-on-accent); + font-weight: 600; +} + +.ms-btn-primary:hover { background: var(--ms-accent-solid-hover); border-color: var(--ms-accent-solid-hover); } + +.ms-btn-danger { color: var(--ms-danger); border-color: var(--ms-danger-tint); background: var(--ms-danger-tint); } + +.ms-input { + height: 30px; + padding: 0 var(--ms-sp-3); + border: 1px solid var(--ms-border); + border-radius: var(--ms-r); + background: var(--ms-bg-inset); + color: var(--ms-fg); + font: inherit; + font-size: var(--ms-fs-sm); +} + +.ms-input::placeholder { color: var(--ms-fg-faint); } + +.ms-btn:focus-visible, +.ms-input:focus-visible, +.ms-link:focus-visible { + outline: none; + box-shadow: var(--ms-focus-ring); +} + +.ms-input:focus-visible { border-color: var(--ms-focus); } + +.ms-link { + color: var(--ms-accent); + text-decoration: none; + border-radius: var(--ms-r-sm); +} + +.ms-link:hover { text-decoration: underline; } + +.ms-table { + width: 100%; + border-collapse: collapse; + font-size: var(--ms-fs-base); +} + +.ms-table th { + text-align: left; + padding: var(--ms-sp-2) var(--ms-sp-3); + border-bottom: 1px solid var(--ms-border); + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-xs); + font-weight: 700; + text-transform: uppercase; + letter-spacing: var(--ms-track-eyebrow); + color: var(--ms-fg-muted); + background: var(--ms-bg-sunken); +} + +.ms-table td { + padding: var(--ms-sp-3); + border-bottom: 1px solid var(--ms-border-subtle); + vertical-align: middle; +} + +.ms-table tbody tr:hover { background: var(--ms-bg-hover); } + +.ms-table tbody tr[aria-selected="true"] { + background: var(--ms-focus-tint); + box-shadow: inset 2px 0 0 var(--ms-focus); +} + +.ms-nav-item { + display: flex; + align-items: center; + gap: var(--ms-sp-3); + padding: var(--ms-sp-2) var(--ms-sp-3); + border-radius: var(--ms-r); + color: var(--ms-fg-muted); + cursor: pointer; +} + +.ms-nav-item:hover { background: var(--ms-bg-hover); color: var(--ms-fg); } + +.ms-nav-item[aria-current="page"] { + background: var(--ms-bg-active); + color: var(--ms-fg); + box-shadow: inset 2px 0 0 var(--ms-focus); +} + +.ms-term { + padding: var(--ms-sp-4); + border: 1px solid var(--ms-border); + border-radius: var(--ms-r-md); + background: var(--ms-term-bg); + color: #e6edf3; + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-sm); + line-height: 1.7; + overflow-x: auto; +} + +.ms-kbd { + display: inline-flex; + align-items: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + justify-content: center; + border: 1px solid var(--ms-border); + border-bottom-width: 2px; + border-radius: var(--ms-r-sm); + background: var(--ms-bg-overlay); + color: var(--ms-fg-muted); + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-xs); + line-height: 1; +} + +.ms-chip { + display: inline-flex; + align-items: center; + gap: var(--ms-sp-2); + height: 26px; + padding: 0 10px; + border: 1px solid var(--ms-border); + border-radius: var(--ms-r-full); + background: transparent; + color: var(--ms-fg-muted); + font: inherit; + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-xs); + cursor: pointer; +} + +.ms-chip:hover { background: var(--ms-bg-hover); color: var(--ms-fg); } + +.ms-chip[aria-pressed="true"] { + background: var(--ms-accent-tint); + border-color: var(--ms-accent-line); + color: var(--ms-accent); +} + +.ms-chip:focus-visible { outline: none; box-shadow: var(--ms-focus-ring); } + +.ms-stat { + position: relative; + padding: var(--ms-sp-4); + border: 1px solid var(--ms-border); + border-radius: var(--ms-r-lg); + background: var(--ms-bg-raised); + overflow: hidden; +} + +.ms-stat::before { + content: ""; + position: absolute; + inset: 0 auto 0 0; + width: 2px; + background: var(--ms-stat-hue, var(--ms-idle)); +} + +.ms-stat dt { + margin: 0; + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-xs); + font-weight: 700; + text-transform: uppercase; + letter-spacing: var(--ms-track-eyebrow); + color: var(--ms-fg-muted); +} + +.ms-stat dd { + margin: var(--ms-sp-2) 0 0; + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-2xl); + font-weight: 700; + letter-spacing: var(--ms-track-tight); + color: var(--ms-stat-hue, var(--ms-fg)); + font-variant-numeric: tabular-nums; +} + +.ms-skeleton { + display: block; + height: 12px; + border-radius: var(--ms-r-sm); + background: linear-gradient(90deg, var(--ms-bg-overlay) 0%, var(--ms-bg-active) 50%, var(--ms-bg-overlay) 100%); + background-size: 200% 100%; + animation: ms-shimmer 1.4s linear infinite; +} + +@keyframes ms-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.ms-scrim { + position: fixed; + inset: 0; + background: rgba(4, 7, 22, 0.62); + backdrop-filter: blur(2px); + z-index: 40; +} + +.ms-palette { + position: fixed; + top: 14vh; + left: 50%; + transform: translateX(-50%); + width: min(560px, calc(100vw - 32px)); + z-index: 50; + border: 1px solid var(--ms-border-strong); + border-radius: var(--ms-r-lg); + background: var(--ms-bg-raised); + box-shadow: var(--ms-shadow-lg); + overflow: hidden; +} + +.ms-palette-field { + display: flex; + align-items: center; + gap: var(--ms-sp-3); + padding: var(--ms-sp-3) var(--ms-sp-4); + border-bottom: 1px solid var(--ms-border); +} + +.ms-palette-field input { + flex: 1; + border: 0; + background: transparent; + color: var(--ms-fg); + font: inherit; + font-size: var(--ms-fs-md); + outline: none; +} + +.ms-palette-field input::placeholder { color: var(--ms-fg-faint); } + +.ms-palette-list { + max-height: 46vh; + overflow-y: auto; + padding: var(--ms-sp-2); + margin: 0; + list-style: none; +} + +.ms-palette-group { + padding: var(--ms-sp-3) var(--ms-sp-3) var(--ms-sp-1); + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-xs); + font-weight: 700; + text-transform: uppercase; + letter-spacing: var(--ms-track-eyebrow); + color: var(--ms-fg-faint); +} + +.ms-palette-item { + display: flex; + align-items: center; + gap: var(--ms-sp-3); + padding: var(--ms-sp-2) var(--ms-sp-3); + border-radius: var(--ms-r); + color: var(--ms-fg-muted); + cursor: pointer; +} + +.ms-palette-item[aria-selected="true"] { + background: var(--ms-focus-tint); + color: var(--ms-fg); + box-shadow: inset 2px 0 0 var(--ms-focus); +} + +.ms-drawer { + position: fixed; + inset: 0 0 0 auto; + width: min(520px, 100vw); + z-index: 50; + display: flex; + flex-direction: column; + border-left: 1px solid var(--ms-border-strong); + background: var(--ms-bg); + box-shadow: var(--ms-shadow-lg); +} + +.ms-drawer-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--ms-sp-4); + padding: var(--ms-sp-4) var(--ms-sp-5); + border-bottom: 1px solid var(--ms-border); +} + +.ms-drawer-body { + flex: 1; + overflow-y: auto; + padding: var(--ms-sp-5); +} + +.ms-timeline { + margin: 0; + padding: 0 0 0 var(--ms-sp-5); + list-style: none; + border-left: 1px solid var(--ms-border); +} + +.ms-timeline li { + position: relative; + padding-bottom: var(--ms-sp-5); +} + +.ms-timeline li:last-child { padding-bottom: 0; } + +.ms-timeline li::before { + content: ""; + position: absolute; + left: calc(var(--ms-sp-5) * -1 - 4px); + top: 4px; + width: 7px; + height: 7px; + border-radius: var(--ms-r-full); + background: var(--ms-bg); + box-shadow: 0 0 0 2px var(--ms-tl-hue, var(--ms-idle)); +} + +.ms-dl { + display: grid; + grid-template-columns: minmax(120px, auto) 1fr; + gap: var(--ms-sp-2) var(--ms-sp-4); + margin: 0; +} + +.ms-dl dt { + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-xs); + text-transform: uppercase; + letter-spacing: var(--ms-track-eyebrow); + color: var(--ms-fg-muted); + padding-top: 2px; +} + +.ms-dl dd { + margin: 0; + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-sm); + color: var(--ms-fg); +} + +@media (prefers-reduced-motion: reduce) { + .ms-skeleton { animation: none; } +} diff --git a/site/site.css b/site/site.css new file mode 100644 index 0000000..7bf0106 --- /dev/null +++ b/site/site.css @@ -0,0 +1,267 @@ +body { + margin: 0; + background: var(--ms-bg); + color: var(--ms-fg); + font-family: var(--ms-font); + font-size: var(--ms-fs-base); + line-height: 1.65; +} + +a { + color: var(--ms-accent); +} + +code { + font-family: var(--ms-font-mono); + font-size: 0.92em; + padding: 0.1em 0.35em; + border-radius: var(--ms-r-sm); + background: var(--ms-bg-raised); +} + +pre { + overflow-x: auto; + margin: var(--ms-sp-4) 0; + padding: var(--ms-sp-4); + border: 1px solid var(--ms-border-subtle); + border-radius: var(--ms-r-md); + background: var(--ms-term-bg); + font-family: var(--ms-font-mono); + font-size: var(--ms-fs-sm); + line-height: 1.6; +} + +pre code { + padding: 0; + background: none; +} + +.top { + position: sticky; + top: 0; + z-index: 10; + display: flex; + align-items: center; + gap: var(--ms-sp-3); + padding: var(--ms-sp-3) var(--ms-sp-6); + border-bottom: 1px solid var(--ms-border-subtle); + background: var(--ms-bg); +} + +.top .grow { + flex: 1; +} + +.top a { + color: var(--ms-fg-muted); + font-size: var(--ms-fs-sm); + text-decoration: none; +} + +.top a:hover { + color: var(--ms-fg); +} + +.hero { + max-width: 62rem; + margin: 0 auto; + padding: var(--ms-sp-7) var(--ms-sp-6) var(--ms-sp-6); +} + +.hero h1 { + margin: var(--ms-sp-3) 0; + font-size: clamp(1.9rem, 5vw, 2.9rem); + letter-spacing: var(--ms-track-tight); + line-height: 1.15; +} + +.hero p.lede { + max-width: 44rem; + margin: 0; + color: var(--ms-fg-muted); + font-size: var(--ms-fs-lg); +} + +.cards { + display: grid; + gap: var(--ms-sp-4); + grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); + margin-top: var(--ms-sp-6); +} + +.card { + padding: var(--ms-sp-4); + border: 1px solid var(--ms-border-subtle); + border-radius: var(--ms-r-md); + background: var(--ms-bg-raised); +} + +.card b { + display: block; + margin-bottom: var(--ms-sp-2); +} + +.card span { + color: var(--ms-fg-muted); + font-size: var(--ms-fs-sm); +} + +.page { + display: grid; + grid-template-columns: 15rem minmax(0, 1fr); + gap: var(--ms-sp-7); + max-width: 62rem; + margin: 0 auto; + padding: 0 var(--ms-sp-6) var(--ms-sp-7); +} + +.toc { + position: sticky; + top: 4.5rem; + align-self: start; + max-height: calc(100vh - 6rem); + overflow-y: auto; + padding-top: var(--ms-sp-6); + font-size: var(--ms-fs-sm); +} + +.toc ol { + margin: 0; + padding: 0; + list-style: none; +} + +.toc li { + margin-bottom: 2px; +} + +.toc a { + display: block; + padding: var(--ms-sp-1) var(--ms-sp-2); + border-radius: var(--ms-r-sm); + color: var(--ms-fg-muted); + text-decoration: none; +} + +.toc a:hover { + background: var(--ms-bg-hover); + color: var(--ms-fg); +} + +.body { + min-width: 0; + padding-top: var(--ms-sp-6); +} + +.body section { + scroll-margin-top: 4.5rem; + padding-bottom: var(--ms-sp-6); +} + +.body h2 { + margin: var(--ms-sp-6) 0 var(--ms-sp-3); + font-size: var(--ms-fs-xl); + letter-spacing: var(--ms-track-tight); +} + +.body h3 { + margin: var(--ms-sp-5) 0 var(--ms-sp-2); + font-size: var(--ms-fs-lg); +} + +.body p, +.body li { + color: var(--ms-fg-muted); +} + +.body strong { + color: var(--ms-fg); + font-weight: 600; +} + +.body table { + width: 100%; + margin: var(--ms-sp-4) 0; + border-collapse: collapse; + font-size: var(--ms-fs-sm); +} + +.body th, +.body td { + padding: var(--ms-sp-2) var(--ms-sp-3); + border-bottom: 1px solid var(--ms-border-subtle); + text-align: left; + vertical-align: top; +} + +.body th { + color: var(--ms-fg); + font-weight: 600; +} + +.body td { + color: var(--ms-fg-muted); +} + +.scroller { + overflow-x: auto; +} + +.note { + margin: var(--ms-sp-4) 0; + padding: var(--ms-sp-3) var(--ms-sp-4); + border: 1px solid var(--ms-border); + border-left-width: 3px; + border-radius: var(--ms-r-md); + background: var(--ms-bg-raised); + font-size: var(--ms-fs-sm); +} + +.note p { + margin: 0; +} + +.note p + p { + margin-top: var(--ms-sp-2); +} + +.note[data-tone="warn"] { + border-left-color: var(--ms-warn); +} + +.note[data-tone="danger"] { + border-left-color: var(--ms-danger); +} + +.note[data-tone="info"] { + border-left-color: var(--ms-info); +} + +footer.site { + max-width: 62rem; + margin: 0 auto; + padding: var(--ms-sp-6); + border-top: 1px solid var(--ms-border-subtle); + color: var(--ms-fg-faint); + font-size: var(--ms-fs-sm); +} + +@media (max-width: 52rem) { + .page { + grid-template-columns: minmax(0, 1fr); + gap: 0; + } + + .toc { + position: static; + max-height: none; + padding-bottom: var(--ms-sp-4); + border-bottom: 1px solid var(--ms-border-subtle); + } + + .toc ol { + display: flex; + flex-wrap: wrap; + gap: var(--ms-sp-1); + } +} diff --git a/tests/run-integration.sh b/tests/run-integration.sh index 0b23280..5b824e9 100755 --- a/tests/run-integration.sh +++ b/tests/run-integration.sh @@ -8,6 +8,11 @@ if [[ -z "${MARS:-}" ]]; then done fi MARS="${MARS:-target/debug/mars}" +if [[ ! -x "$MARS" ]]; then + echo "error: $MARS is not an executable runtime; build it or set MARS" >&2 + exit 1 +fi +MARS="$(cd "$(dirname "$MARS")" && pwd)/$(basename "$MARS")" WORK="${WORK:-/tmp/mars-it}" IMAGE="${IMAGE:-alpine:3.20}" @@ -283,6 +288,7 @@ check "container sees its own cgroup as the root" "0::/" "$cgns" echo echo "memory.max and OOM kill" bundle "$WORK/oom" '.linux.resources.memory.limit = 33554432 + | .linux.resources.memory.swap = 33554432 | .process.args = ["/usr/bin/awk","BEGIN{s=\"\";while(1){s = s sprintf(\"%1000000s\",\"\")}}"]' oom_log=$(run_in "$WORK/oom" it-oom 2>&1) check "OOM kill is reported as 128+9" "137" "$?"