Skip to content

runsc does not honor OCI LinuxResources.Memory.Swap when joining the pod cgroup, leaving K8s LimitedSwap unenforced for gVisor pods #13267

Description

@matthew22apps

Description

Title: runsc does not honor OCI LinuxResources.Memory.Swap when joining the pod cgroup, leaving K8s LimitedSwap unenforced for gVisor pods

Repro environment

  • gVisor: release-20260413.0
  • Kubernetes: v1.35.5 (via k3s)
  • runtime: containerd-shim-runsc-v1
  • cgroup v2, cgroupfs driver (also tested with systemd driver — same behavior)
  • kubelet config:
    apiVersion: kubelet.config.k8s.io/v1beta1
    kind: KubeletConfiguration
    failSwapOn: false
    memorySwap:
      swapBehavior: LimitedSwap
  • Node has 128 GiB encrypted swap configured (vm.swappiness=10, zswap enabled).

What I expected

Per the K8s LimitedSwap documentation,
each Burstable container should be given a proportional swap budget:

swap.max = (container.memory.request / node.memory.capacity) * node.swap.total

For a Burstable Pod with requests.memory: 384Mi, limits.memory: 1Gi
on a 251 GiB / 128 GiB node, the expected memory.swap.max is ~195 MiB.

What I observed

For a Pod with runtimeClassName: gvisor, no memory.swap.max is
applied anywhere the runsc sandbox can see:

$ cat /sys/fs/cgroup/kubepods/burstable/pod<uid>/memory.swap.max
max

$ cat /sys/fs/cgroup/kubepods/burstable/pod<uid>/<container-id>/memory.swap.max
max

For a Pod scheduled with the default runc runtime, the same spec
correctly receives the calculated swap budget at the container cgroup:

$ cat /sys/fs/cgroup/kubepods/burstable/pod<uid>/<container-id>/memory.swap.max
204472320   # ~195 MiB, matches LimitedSwap formula

Root cause (proposed)

This appears to be a natural consequence of the design in
google/gvisor#6007,
which made the runsc sandbox join the pod-level cgroup rather than
the per-container cgroup. The kubelet (and containerd's runc shim) writes
memory.swap.max to the container cgroup — but for gVisor, the
sandbox process never actually enters that cgroup, so the kernel never
enforces the LimitedSwap budget for the workload.

runsc itself already has code to write memory.swap.max
(runsc/cgroup/cgroup_v2.go memory2.set())
— it just isn't invoked here because the pod-level cgroup is set up by
the kubelet/CRI, and the swap field of OCI LinuxResources.Memory isn't
propagated to it.

Impact

memory.max at the pod level still enforces the memory ceiling, so a
single Pod cannot exceed its configured limit. But because
memory.swap.max = max, a gVisor Pod under memory pressure can consume
an unbounded share of the node's swap pool, starving its neighbors —
defeating the noisy-neighbor isolation that LimitedSwap was added to
provide.

Suggested fix paths

  1. When runsc joins the pod-level cgroup, also apply the swap field
    from the OCI LinuxResources.Memory it received, by extending
    memory2.set()'s caller into the pod-cgroup setup path.

  2. Alternatively (simpler interim), have runsc emit a config knob
    (e.g. --apply-pod-cgroup-swap-max) that, when enabled, writes the
    per-pod swap budget computed from container requests.

Workaround

For anyone hitting this today: a small Kubernetes DaemonSet that watches
gVisor Pods on its node and writes the LimitedSwap-formula value
(memory.swap.max = container.requests.memory / node.memory * node.swap)
directly to the pod cgroup using the standard K8s informer pattern. The
DaemonSet needs pods: [get, list, watch] RBAC plus privileged hostPath
access to /sys/fs/cgroup. Same math kubelet uses for runc, just applied
at the pod-level cgroup where the runsc sandbox actually lives. Happy
to share the implementation if useful — let me know.

Once upstream applies the swap field from the OCI spec when joining the
pod cgroup, the workaround can be removed cleanly.

Steps to reproduce

No response

runsc version

docker version (if using docker)

uname

No response

kubectl (if using Kubernetes)

repo state (if built from source)

No response

runsc debug logs (if available)

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions