CL-0029: Host-availability capabilities (SYS_NICE, IPC_LOCK, LEASE)¶
Severity: HIGH
Derivation (see severity model):
- Baseline: A — the attacker already has code execution in this container, as the workload uid
- Precondition: Direct — each member realises its impact through supported calls alone.
sched_setscheduler(SCHED_FIFO),mlock(),fcntl(F_SETLEASE). No published technique, no second defect, and no other key in the file - Impact: Host — the scheduler, the page cache and the lease-break path are all host-wide; none of the three is namespaced
- Qualifier/modifier: availability-only — the realised loss is denial of service. None of the three reads host data or runs host code, so the tier drops one from CRITICAL
- Derived: Direct × Host + availability-only = HIGH
- Shipped: HIGH
- Scoping assumptions: none.
IPC_LOCK's reach is bounded bydeploy.resources.limits.memory, but that is a key the file has to add, and the model's scoping clause covers reach that needs a sibling key present (see CL-0027'sSYS_PTRACE). Pricing the bounded case would price the rule below its own default behaviour, against "heterogeneous members score at their most dangerous member" - Evidence: three live checks in
scripts/validate_rule_premises.py, each comparing--cap-drop ALLagainst the same run plus the single capability._cl0029_sys_nice—chrt -f 50is refused capless and succeeds withSYS_NICE._cl0029_lease—F_SETLEASE(F_WRLCK)on a file the process has chowned away from itself isEACCEScapless and granted withLEASE;CHOWNis on both legs so the capability is the only difference._t_ipc_lock(registered under CL-0006) —mlockallagainstRLIMIT_MEMLOCK=0is refused capless and succeeds withIPC_LOCK. All three stop at acquiring the primitive: a check that actually starved the runner's CPUs, pinned its RAM or stalled its opens would be a self-inflicted outage. That the host is what degrades therefore follows from the scheduler, the locked-page accounting and the lease-break path not being namespaced — reasoned, not asserted by the suite. One consequence was measured out-of-band on Docker 29.4.3 and is deliberately not in CI: with a container holding a write lease on a bind-mounted host file, a hostopen()of it completed after 21.9s, against 0.00s unleased, through a read-only bind
References: - OWASP Docker Security Rule #3 - CIS Docker Benchmark 5.4 — Ensure that Linux kernel capabilities are restricted within containers
What it detects¶
cap_add entries naming a capability that can degrade the host:
| Capability | Grants | Why it is not CRITICAL |
|---|---|---|
SYS_NICE |
SCHED_FIFO/SCHED_RR and priority raises. The scheduler is host-wide, so the container's threads sit above ordinary host processes |
Buys no read and no execution. RT throttling caps a runaway at sched_rt_runtime_us, so it degrades rather than halts |
IPC_LOCK |
mlock/mlockall past RLIMIT_MEMLOCK, and SHM_LOCK. Locked pages cannot be swapped or reclaimed |
Pins memory, reads none of it. A memory limit bounds it — but only once the file sets one |
LEASE |
F_SETLEASE on files the process does not own. A write lease makes the host's own open() block until the lease is released or the timeout expires |
Delays host processes; it does not read or alter the file's contents |
CAP_-prefixed and lowercase spellings are equivalent and are matched the same
way: Docker treats CAP_SYS_NICE and SYS_NICE as the same capability.
Why it matters¶
A container is not a scheduling, memory or file-locking boundary. All three of these subsystems are host-global, and none of the three grants needs a vulnerability to become impact — they are the capability working as designed, handed to a workload that should not have it.
LEASE is the least obvious and the least contained. A write lease is a
kernel-supported way to make another process wait: while the lease is held, the
host's own open() of that file blocks for up to /proc/sys/fs/lease-break-time
(45 seconds by default), and the lease can be retaken. It needs only a bind
mount to reach a host file, and the bind may be read-only — taking a lease
is not a write. That is why this rule does not defer to
CL-0013 or CL-0025 the way LINUX_IMMUTABLE does:
those rules flag sensitive or writable host paths, and an ordinary
./config:/config bind is neither.
IPC_LOCK is the one whose severity people will argue with, because a
memory limit does bound it. The derivation says why the bounded case is not the
one priced: the limit is a key the file must add, the default is unbounded, and
CL-0026 shows the limit absent on ~90% of real Compose files.
Fix¶
Remove the capability. Each has a bounded alternative that does not hand the container a host-wide lever:
# Before
services:
app:
image: myapp:1.0
cap_add:
- SYS_NICE
- IPC_LOCK
# After — bound the resources instead of granting the capability
services:
app:
image: myapp:1.0
deploy:
resources:
limits:
cpus: '2.0'
memory: 4G
deploy.resources gives a workload a predictable share of CPU without letting
it outrank the host's own processes, and a memory limit is what bounds pinned
pages — so it is worth setting even for a workload that keeps IPC_LOCK.
When to suppress¶
Storage and packet-processing engines are the honest exception: SPDK and DPDK
stacks ask for SYS_NICE and IPC_LOCK together, alongside a huge-page mount,
and there is no configuration that gives them the same behaviour without the
capabilities. Suppress per service with a reason: naming the workload, and
set deploy.resources.limits.memory anyway — it still bounds what the
container can pin.
LEASE has no comparable case. A workload that needs it is coordinating with a
process outside the container over a shared file, which is a design worth
changing rather than suppressing.
ATT&CK coverage¶
Remediating this finding contributes to mitigating the following MITRE ATT&CK techniques (pinned to ATT&CK v18). compose-lint is a static analyser, so this is mitigation coverage — it detects nothing at runtime.
| Technique | Tactic |
|---|---|
| T1499 Endpoint Denial of Service | Impact |
| T1496 Resource Hijacking | Impact |