ExSandbox.Capability (ExSandbox v1.0.0)

Copy Markdown View Source

What this library needs from its host, and whether it is actually there (012 T013, T022, FR-016).

Determined, never assumed

FR-016 requires a library state what it needs and report at runtime when it is unavailable, rather than assuming it. The distinction is not academic: 005 R9 found that six of ten isolation criteria require Linux, and 013 Finding V2 found a capability that is absent under some container configurations. A library that assumed them would provide no isolation on those hosts while claiming to.

Availability is evidence, not configuration

A capability is reported available only on FR-012a's evidence standard — observed behaviour, not the presence of a mechanism. 005 R9b measured the failure this guards against: taskpolicy -m 100 sandbox-exec ... ./hog 300 allocates 300 MB under a nominal 100 MB cap and exits 0, because the limit is silently lost across the intervening exec. Every check short of trigger a breach and watch it stop reports that composition as working.

So a check here answers "could this host enforce the cap at all", and it errs toward false. Whether a particular mechanism actually enforces it is ExSandbox.Conformance's question, answered by breaching it.

The coarse name and its decomposition (014-FR-013a, T004)

:resource_limits remains the coarse, Linux-facing name, and it stays. ExSandbox.Mechanism.Beam's required_capabilities/0 callback names it, and its comment records why one name suffices there: the memory and CPU caps "both come from the same cgroup scope, and a host with one has the other". On Linux that is true, and a single gate over a single mechanism is the honest shape.

It stops being true off Linux, which is what 014-FR-013a is about. 005 R9b measured a macOS host where the caps come apart: RLIMIT_CPU is honoured by the kernel while RLIMIT_AS/DATA/RSS fail setrlimit with EINVAL, and the memory cap survives or is lost depending on where taskpolicy sits in the process tree. One summary level cannot report that host without lying in one direction or the other.

So :process_separation, :memory_cap, :cpu_cap and :time_budget are :resource_limits' per-capability decomposition — an additional, finer vocabulary, not a replacement. Two rules keep the two from drifting apart, and both exist because a split verdict on one underlying fact is a defect this file has already carried more than once (005 T060a5c, T060c):

  • On Linux the three host-enforced names are derived from the existing cgroup v2 probe rather than re-probing it. Two probes of one fact are two things that must stay equal forever, and the symptom of their drifting is a cap reported enforced on a host that does not enforce it.

  • :time_budget is derived from nothing, because it is not a host fact at all. 014-FR-014b places its enforcement in the supervising BEAM, so no probe here can observe it and no cgroup verdict says anything about it. See time_budget_not_a_host_capability/1.

Three Darwin names now report available, and what that cost (014 T020)

Every Darwin clause below used to report unavailable, on the correct grounds that no backend existed to watch stopping a breach. 014 Phase 3 built one, and Phase 4 observed the breaches: :memory_cap, :cpu_cap and :process_separation are derived from ExSandbox.Hardening.Darwin.capabilities/0, whose probe runs the real composition rather than looking for binaries on the PATH.

The claim rests on ExSandbox.Hardening.DarwinOrderingTest (SC-003), which runs R9b's misordered composition and the backend's own composition in one run and requires them to differ — 0 with 300 MB allocated against 137. Without that pair, FR-014a's standard is not met and these names go back.

Everything else on Darwin still reports unavailable, and each detail names what is missing rather than merely asserting absence:

  • :time_budget — not a host fact anywhere, see time_budget_not_a_host_capability/1.
  • :privilege_separation — a deny-list sandbox-exec profile is not default-deny confinement (T021).
  • :filesystem_confinement, :network_restriction, :disk_quota, :resource_limits — each for the reason its clause states.

Summary

Functions

Checks one capability against the running host.

Checks every known capability.

The capabilities a mechanism is gated on when it declares none.

Every capability this library knows how to check.

The capabilities in required that this host cannot provide.

True when every capability in required is available.

Types

name()

@type name() ::
  :resource_limits
  | :filesystem_confinement
  | :privilege_separation
  | :network_restriction
  | :disk_quota
  | :process_separation
  | :memory_cap
  | :cpu_cap
  | :time_budget

t()

@type t() :: %ExSandbox.Capability{
  available?: boolean(),
  detail: String.t() | nil,
  name: name()
}

Functions

check(name)

@spec check(name() | atom()) :: t()

Checks one capability against the running host.

Returns a report; it never raises, because "cannot determine" is a legitimate answer that must be reported rather than thrown (FR-012b).

check_all()

@spec check_all() :: [t()]

Checks every known capability.

gating_defaults()

@spec gating_defaults() :: [name()]

The capabilities a mechanism is gated on when it declares none.

Deliberately narrower than known/0 -- see the note above it. A name that is unavailable on every host belongs in the report and never in the gate.

known()

@spec known() :: [name()]

Every capability this library knows how to check.

missing(required)

@spec missing([name()]) :: [t()]

The capabilities in required that this host cannot provide.

Returned rather than raised so a caller can report which one is missing — "unavailable" with no detail is the kind of message that gets ignored.

satisfied?(required)

@spec satisfied?([name()]) :: boolean()

True when every capability in required is available.

This is what an entry point calls before starting a sandbox: a mechanism whose required capability is missing must refuse rather than start unconfined (spec Edge Cases; 005 R9's macOS rule).