Skip to content

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 by deploy.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's SYS_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 ALL against the same run plus the single capability. _cl0029_sys_nicechrt -f 50 is refused capless and succeeds with SYS_NICE. _cl0029_leaseF_SETLEASE(F_WRLCK) on a file the process has chowned away from itself is EACCES capless and granted with LEASE; CHOWN is on both legs so the capability is the only difference. _t_ipc_lock (registered under CL-0006) — mlockall against RLIMIT_MEMLOCK=0 is refused capless and succeeds with IPC_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 host open() 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

See also

  • CL-0028 — the same Direct × Host cell, spending its qualifier on integrity-only instead
  • CL-0026 — missing resource limits, which is what bounds IPC_LOCK
  • CL-0006 — dropping capabilities wholesale, the control this rule's findings evade