ExSandbox.Conformance.ResourceLimits (ExSandbox v1.0.1)

Copy Markdown View Source

Conformance group: resource limits (012 T034a, T034b; FR-012a, SC-008).

Every check breaches the cap

005 R9b is the reason this group is shaped the way it is. Measured:

taskpolicy -m 100 sandbox-exec -f profile.sb ./hog 300

allocates 300 MB under a nominal 100 MB cap and exits 0. The limiter was invoked, with the right flag, with the right number. The cap is silently lost across the intervening exec, and nothing in the invocation, the exit status, or the process tree distinguishes it from a working one.

Which means every one of these passes it:

  • asserting taskpolicy was called with -m 100
  • asserting the wrapper appears in the process tree
  • asserting configuration records a 100 MB cap
  • asserting the process exited without error

All four check invocation. Only allocate 300 MB and observe the process stopped checks enforcement, and the gap between them is where a tenant exhausts a host while every dashboard reads green.

So each check here allocates past the memory cap, spins past the CPU cap, or blocks past the time budget, and requires that the mechanism stopped it.

An undemonstrable cap is unavailable, never satisfied (FR-012b, T034b)

If a breach can be neither completed nor observed being stopped, the check reports the third outcome — ExSandbox.Conformance.CapabilityUnavailable — rather than passing. A cap that is configured but never breached in this suite must not report as passing; nothing was demonstrated, and "nothing was demonstrated" is not evidence of a guarantee.

The outcomes are distinguishable from each other and from a crash

A breach stopped by the mechanism, a breach that succeeded, and a host that could not attempt one produce three different results. An ordinary crash — the sandbox dying for an unrelated reason — is reported as inconclusive rather than as a satisfied cap, because a process that died before reaching the cap has not shown the cap works.

Summary

Functions

Runs one hostile command inside a sandbox and classifies what happened.

Decides whether a breach attempt was stopped, succeeded, or showed nothing.

A shell command performing a fixed amount of work, printing its marker only if that work completed within a wall-clock deadline.

A shell command allocating mb megabytes and touching every page of it.

A shell command blocking for seconds, far past the budget.

Emits the resource-limit checks into the calling test module.

Functions

breach(mechanism, sandbox, command, dimension)

Runs one hostile command inside a sandbox and classifies what happened.

Returns {:stopped, evidence}, {:breached, evidence}, or anything else — which ExSandbox.Conformance.Helpers.demonstrate_breach/3 routes to the third outcome.

classify(other, dimension, cap)

@spec classify(term(), :memory | :cpu | :time, term()) ::
  {:stopped, String.t()} | {:breached, String.t()} | {:inconclusive, String.t()}

Decides whether a breach attempt was stopped, succeeded, or showed nothing.

Public because the group self-checks it: this is the single place the fail-open mistake would live, so it is exercised directly rather than only through a live mechanism.

cpu_hog_command()

@spec cpu_hog_command() :: String.t()

A shell command performing a fixed amount of work, printing its marker only if that work completed within a wall-clock deadline.

⚠️ Fixed work, not fixed duration, and the distinction makes this check falsifiable at all. An earlier version spun while [ $(date +%s) -lt $end ] for ten seconds and printed the marker unconditionally. That loop finishes in ten seconds whatever the CPU quota — throttling grants less CPU per second, it does not extend wall time — so the check reported "the breach was not stopped" on every correct implementation, including one enforcing a 1% quota.

Measured on a cgroup v2 host under CPUQuota=1%: the old wall-clock loop finished in 5s and printed its marker; this fixed-work loop took 54s. Only the second distinguishes a throttled sandbox from an unthrottled one.

Why a deadline rather than a duration

A CPU cap throttles; it does not kill. So "stopped" here can only mean "could not finish this work in the time an unthrottled process would need", which is what the timeout expresses. A sandbox with no cap prints the marker well inside the deadline; a throttled one is still working when timeout fires and prints nothing.

memory_hog_command(mb)

@spec memory_hog_command(pos_integer()) :: String.t()

A shell command allocating mb megabytes and touching every page of it.

The touching is the part that matters: an allocation the kernel can leave unbacked is not an allocation any cap will notice, so a hog that only calls malloc would report a working cap on a host with none.

sleep_command(seconds)

@spec sleep_command(pos_integer()) :: String.t()

A shell command blocking for seconds, far past the budget.

tests()

(macro)

Emits the resource-limit checks into the calling test module.