The misc Controller

The misc controller is cgroups v2’s catch-all for scalar hardware resources — countable, machine-finite quantities that exist in too small a supply and too narrow a use case to justify a dedicated controller of their own. Rather than write a whole new controller every time a CPU vendor ships a feature gated on a handful of hardware IDs, the kernel offers one generic limiter keyed by resource name. As of Linux 6.12 LTS, the only resources registered are AMD SEV and SEV-ES address-space identifiers (ASIDs) — the encryption-key slots used by confidential-VM features; in Linux 6.18 LTS a third, Intel TDX host key IDs (HKIDs), is added (per v6.12 vs v6.18 misc_cgroup.h). The controller is enabled by CONFIG_CGROUP_MISC and was merged in Linux 5.13 (per LWN and Phoronix). Its interface is deliberately tiny: per resource you get a total misc.capacity, a per-cgroup misc.max limit, a misc.current usage, a misc.peak high-watermark, and a misc.events counter of limit hits.

The mechanism is interesting precisely because of what it isn’t: there is no CPU scheduler integration, no memory reclaim, no I/O throttling. The misc controller is a pure counting allocator with a hierarchical ceiling — atomic charge/uncharge of an integer against a budget. That simplicity is the whole design: it exists so that the next niche scalar resource can be added in a few lines of a vendor’s KVM driver instead of a new subsystem. It belongs to the containers family because, like every other v2 controller, it bounds what a process tree may consume — here, how many confidential-VM encryption slots a container or a tenant may hold.


Mental Model — A Named-Token Budget per Subtree

Think of misc as a bank with a few named token denominations. The machine has a fixed total of each token (misc.capacity, e.g. “509 SEV ASIDs”). Each cgroup may set a ceiling on how many of each token its subtree may hold (misc.max). When the kernel hands out a token (charges), it walks the cgroup up to the root, debiting each ancestor; if any ancestor would exceed its ceiling or the machine-wide capacity, the whole charge fails with -EBUSY and a misc.eventsmax counter ticks.

flowchart TB
  CAP["misc.capacity (root only)<br/>sev 509  sev_es 100<br/>= total tokens on this machine"]
  CAP --> ROOT["root misc cgroup"]
  ROOT -->|"misc.max sev 200"| T1["tenant-A<br/>misc.current sev 137<br/>misc.peak sev 180"]
  ROOT -->|"misc.max sev 200"| T2["tenant-B<br/>misc.current sev 40"]
  T1 -->|"misc.max sev 50"| C1["container-1<br/>charge here walks UP:<br/>container-1 -> tenant-A -> root"]
  CHARGE["misc_cg_try_charge(SEV, cg, 1)"] -.->|"debit cg AND every ancestor;<br/>any over max or capacity => -EBUSY,<br/>misc.events max++"| C1

The misc controller as a hierarchical token budget. What it shows: a charge for one SEV ASID at container-1 is debited not just locally but at every ancestor up to root, and it succeeds only if no ancestor’s misc.max and the machine-wide misc.capacity are both respected. The insight to take: misc enforces limits hierarchically and additively — a child’s usage counts against every parent — and misc.capacity is a hard machine ceiling that even a generous misc.max cannot exceed. The limit is on a count, not bytes or CPU time; this is what “scalar resource” means.


Mechanical Walk-through — Charge, Limit, Account

The entire controller is ~490 lines of kernel/cgroup/misc.c (v6.12), and the data model is the small include/linux/misc_cgroup.h. Walking the real code:

The resource registry

A resource is two things in lockstep: an entry in enum misc_res_type (in the header) and a matching string in misc_res_name[] (in misc.c). In v6.12 both are gated on CONFIG_KVM_AMD_SEV:

/* misc_cgroup.h, v6.12 */
enum misc_res_type {
#ifdef CONFIG_KVM_AMD_SEV
    MISC_CG_RES_SEV,        /* AMD SEV ASIDs */
    MISC_CG_RES_SEV_ES,     /* AMD SEV-ES ASIDs */
#endif
    MISC_CG_RES_TYPES       /* count sentinel */
};

MISC_CG_RES_TYPES is not a real resource — it is the array-size sentinel (the classic C enum-as-count idiom), so struct misc_res res[MISC_CG_RES_TYPES] sizes itself to exactly the number of compiled-in resources. If CONFIG_KVM_AMD_SEV is off and (in 6.12) nothing else registers, the array is empty and the controller is inert. The cgroup-v2.rst documentation (lines 2665–2668) confirms the contract: “A resource can be added to the controller via enum misc_res_type{} … and the corresponding name via misc_res_name[].”

Per-resource state

Each (cgroup, resource) pair holds a struct misc_res (misc_cgroup.h line 38):

struct misc_res {
    u64 max;                 /* the misc.max ceiling */
    atomic64_t watermark;    /* misc.peak: historical high usage */
    atomic64_t usage;        /* misc.current */
    atomic64_t events;       /* misc.events: hierarchical max-hit count */
    atomic64_t events_local; /* misc.events.local: non-hierarchical */
};

usage, watermark, events, and events_local are atomic64_t because charging happens from any context with no cgroup mutex held — the controller relies on lock-free atomics rather than a spinlock, which is part of why it is cheap.

Capacity: the machine ceiling

A provider of the resource — the AMD SEV code in KVM — calls misc_cg_set_capacity(type, capacity) (line 98) at init, recording the machine-wide total in a static misc_res_capacity[] array. Capacity 0 means “this resource does not exist / is not initialised on this host,” and any charge against a zero-capacity resource fails with -EINVAL. Crucially, the root cgroup’s max and the capacity are independent: the in-source comment (lines 36–39) notes root_cg.max can be set higher than the actual capacity — this is the cgroup “Limits” distribution model, where the real hardware ceiling (capacity) is the backstop regardless of what max says.

Charging: walk up, debit, validate, roll back

misc_cg_try_charge(type, cg, amount) (line 164) is the heart of the controller:

  1. Reject if the type is invalid, the cgroup is null, or the resource’s capacity is 0 → -EINVAL. A zero amount is a trivial success.
  2. Walk from cg to the root (for (i = cg; i; i = parent_misc(i))). At each level, atomic64_add_return(amount, &res->usage) charges and reads the new usage in one atomic op.
  3. If the new usage exceeds either res->max (this cgroup’s ceiling) or misc_res_capacity[type] (the machine total), set ret = -EBUSY and jump to error handling.
  4. On success at each level, update the watermark (misc_cg_update_watermark, an atomic compare-exchange loop that only ever raises the recorded peak).
  5. On failure, roll back: misc_cg_event(type, i) fires the events counter at the level that failed and propagates it up; then every level below the failure point that was already charged is uncharged (misc_cg_cancel_charge). The charge is therefore all-or-nothing across the hierarchy.

This is why the limit is genuinely hierarchical: a container deep in the tree cannot allocate a SEV ASID if any ancestor — a per-tenant cgroup, say — is already at its misc.max. The failure is reported at the constraining level.

The events counter

misc_cg_event() (line 137) increments events_local at the failing cgroup and cgroup_file_notifys its misc.events.local file, then walks up incrementing each ancestor’s hierarchical events and notifying their misc.events files. So misc.events’s max field at a parent counts every limit-hit anywhere in its subtree; misc.events.local counts only hits at that exact cgroup. Both are surfaced by __misc_events_show() (line 379), which prints "%s.max %llu\n" — note the key is literally <resname>.max, the only event field defined.

Uncharging and migration

misc_cg_uncharge() (line 208) simply walks up cancelling the charge at every level — symmetric to the charge walk. A subtle but documented property: charges do not follow process migration. cgroup-v2.rst (lines 2742–2745) states a misc resource “is charged to the cgroup in which it is used first, and stays charged to that cgroup until that resource is freed. Migrating a process to a different cgroup does not move the charge.” This makes sense given the resource model — a SEV ASID is bound to a running VM, not to the bookkeeping of which cgroup the controlling thread currently lives in.


Interface Files — All Six

The kernel doc’s prose says the controller “provides 3 interface files,” but the actual misc_cg_files[] cftype array in v6.12 (misc.c line 407) registers six. The full set, with their flags:

FileWhereDirectionMeaning
misc.capacityroot only (CFTYPE_ONLY_ON_ROOT)readMachine-wide total of each resource. Zero-capacity resources are omitted.
misc.currentall cgroupsreadCurrent usage (res.usage) of each resource in this cgroup and its children.
misc.peakall cgroupsreadHistorical maximum usage (res.watermark).
misc.maxnon-root (CFTYPE_NOT_ON_ROOT)read/writeThe ceiling. Default is max (i.e. U64_MAX, set in misc_cg_alloc).
misc.eventsnon-rootreadHierarchical count of max-boundary hits (<res>.max <n>).
misc.events.localnon-rootreadSame, but counting only hits at this cgroup.

Two of these — misc.peak and misc.events.local — are not mentioned in the original three-file documentation paragraph and are easy to miss if you read prose rather than source.

Reading the files

# At the root: what does the machine have?
cat /sys/fs/cgroup/misc.capacity
# sev 509
# sev_es 100              # (numbers illustrative; depend on CPU + firmware)
 
# In a child cgroup: set and inspect a limit.
cd /sys/fs/cgroup/confidential-vms
echo "sev 50"  > misc.max     # cap this subtree at 50 SEV ASIDs
echo "sev max" > misc.max     # remove the cap (back to U64_MAX)
 
cat misc.max
# sev 50
cat misc.current
# sev 37
cat misc.peak
# sev 48
cat misc.events
# sev.max 3                  # this subtree hit its limit 3 times

The write parser

misc_cg_max_write() (line 266) takes input like echo "sev 23" — a strsep on the space splits the resource name from the value; the name is matched against misc_res_name[], an unknown name yields -EINVAL, the literal string max maps to U64_MAX, otherwise kstrtou64 parses the number. A write to a resource whose capacity is 0 (not present on this host) also returns -EINVAL — you cannot limit what does not exist.

Allocation defaults

misc_cg_alloc() (line 451) initialises every new cgroup’s per-resource max to MAX_NUM (U64_MAX) and usage to 0 — so a fresh cgroup imposes no limit until you write one. The root cgroup is a static root_cg; children are kzalloc’d. The controller registers both legacy_cftypes and dfl_cftypes to the same file array (line 488), so the interface is identical on the (rare) v1 hierarchy and the v2 default hierarchy.


The Canonical Consumers — Confidential Computing

The misc controller exists because confidential-VM features anchor each guest to a scarce, hardware-enumerated key slot, and cloud hosts pack many tenants onto one machine.

AMD SEV / SEV-ES ASIDs (v6.12 and later). Secure Encrypted Virtualization (SEV) encrypts a VM’s memory with a key tied to an address-space identifier (ASID); SEV-ES (Encrypted State) additionally encrypts the guest’s CPU register state across VMEXIT. The number of simultaneously usable ASIDs is a small, CPU- and firmware-fixed quantity (commonly in the low hundreds, partitioned between plain-SEV and SEV-ES). Without a limiter, one noisy tenant could exhaust the host’s ASIDs and starve every other tenant of confidential-VM capability. The very first misc.c commit registered SEV and SEV-ES ASIDs to the controller for exactly this reason; the controller was, in fact, originally proposed as a SEV ASID controller and generalised during review (LWN: “The misc control group”).

Intel TDX HKIDs (v6.18, not v6.12). Trust Domain Extensions (TDX) is Intel’s confidential-VM technology; each trust domain uses a host key ID (HKID) to select its memory-encryption key, and HKIDs are likewise a limited per-machine resource. In v6.18 the misc controller gains a third resource, tdx, gated on CONFIG_INTEL_TDX_HOST (verified in v6.18 misc_cgroup.h and misc.c).

Uncertain

Verify: the exact LTS release in which TDX HKIDs first appeared in the misc controller. Reason: I confirmed by direct fetch that the tdx resource is absent in v6.12 and present in v6.18 — that part is source-verified. I did not pin the precise introducing version (it could be any release in the 6.13–6.18 range); the KVM/TDX merge commit (“KVM: TDX: Register TDX host key IDs to cgroup misc controller”) exists but I did not map it to a kernel tag. To resolve: git log --oneline v6.12..v6.18 -- kernel/cgroup/misc.c and read the tag the HKID commit landed in. Do not rely on this note for “TDX support exists in 6.12” — it does not. uncertain

The task brief named both SEV/SEV-ES and TDX as “canonical consumers.” That is accurate for the mainline / 6.18 kernel but wrong for 6.12: pinned to 6.12 LTS, the misc controller knows only sev and sev_es. This note corrects that to match the version it is pinned to. See the Linux Virtualization MOC for the SEV/SEV-ES/TDX confidential-computing mechanisms themselves.


Failure Modes and Gotchas

  • A zero-capacity resource silently rejects everything. If the host CPU lacks SEV (or CONFIG_KVM_AMD_SEV is off), misc.capacity omits it, misc.max writes return -EINVAL, and charges return -EINVAL. The resource is invisible, not “unlimited.”
  • misc.max can exceed misc.capacity and that is intentional. The in-source comment is explicit: root_cg.max may be larger than capacity. The capacity is the real backstop — misc_cg_try_charge checks new_usage > misc_res_capacity[type] independently of max. Setting misc.max sev max does not grant more ASIDs than the silicon has.
  • Charges don’t migrate with the process. Move a process holding a SEV ASID into another cgroup and the charge stays with the original cgroup until the VM is torn down. Accounting tools that assume charge-follows-task will misattribute usage.
  • The doc undercounts the files. Reading only the “3 interface files” paragraph hides misc.peak and misc.events.local. Trust the cftype array.
  • misc.events’ only field is max. There is no “high” or “oom” event like the memory controller; the sole event is the limit-hit (<res>.max). Don’t grep for fields that don’t exist.
  • It is not a reservation. Setting misc.max sev 50 does not pre-allocate 50 ASIDs; it caps usage. Actual ASIDs are charged lazily as VMs start, and a start can fail with -EBUSY at any point up to the cap or capacity.

Alternatives and When to Choose Them

  • A dedicated controller (like cpu, memory, io, pids). Justified when the resource needs scheduling, reclaim, throttling, or pressure feedback — anything beyond counting. SEV ASIDs need none of that; they are bind-on-VM-create, free-on-destroy integers, which is exactly the misc sweet spot. See The cgroup pids Controller for the closest counting sibling (it limits process/thread count, but is dedicated because PID accounting is universal, not niche).
  • No controller — global host limit only. Before misc, ASID exhaustion was a global host failure with no per-tenant attribution. misc adds the per-subtree ceiling and the events counter so a cloud host can fairly partition and observe a scarce hardware resource across tenants.
  • pids controller for the specific case of “limit how many processes.” misc is for non-process scalar hardware tokens; the two are complementary, not substitutes.

The decision rule the LWN coverage settled on: if a resource is countable, scalar, machine-finite, and niche, add it to misc (a few lines); if it needs active management, it earns its own controller.


Production Notes

The misc controller’s reach is, by design, narrow: in v6.12 it is only meaningful on AMD EPYC hosts running confidential VMs with SEV. Container/VM platforms that schedule confidential guests (cloud providers offering AMD SEV-SNP or Intel TDX instances, Kata Containers / Confidential Containers stacks) are the realistic operators of misc.max. For the overwhelming majority of containers — which use no memory-encryption hardware — the controller is present but inert, every resource at zero capacity. This is why a typical kubectl/Kubernetes cgroup hierarchy never touches misc: there is nothing scalar-and-scarce to bound. Its importance is as the extension point: when the next vendor ships a feature gated on a handful of hardware IDs, the kernel community can expose it through misc without the design debate and code of a new subsystem — which is exactly how TDX HKIDs were added later with a one-resource patch.


See Also