Skip to content

fix(kill): lock OS thread around sandbox netns join to prevent TAP leak#588

Closed
mn-ram wants to merge 1 commit into
urunc-dev:mainfrom
mn-ram:fix/kill-lockosthread
Closed

fix(kill): lock OS thread around sandbox netns join to prevent TAP leak#588
mn-ram wants to merge 1 commit into
urunc-dev:mainfrom
mn-ram:fix/kill-lockosthread

Conversation

@mn-ram

@mn-ram mn-ram commented Apr 29, 2026

Copy link
Copy Markdown

Description

Unikontainer.Kill() called joinSandboxNetNs() (which performs unix.Setns(CLONE_NEWNET) on the calling OS thread only) without first calling runtime.LockOSThread(). Between the setns and the netlink-backed network.CleanupAllUruncTaps() call later in the function, the Go scheduler is free to migrate the goroutine to another M still bound to the host network namespace. When that happens, netlink.NewHandle() inside CleanupAllUruncTaps creates its socket in the wrong namespace, the ^tap\d+_urunc$ scan finds zero matches, and Kill() returns success while leaving the sandbox's TAP device, ingress qdisc, and tc redirect filters in place.

The contract was already documented on joinSandboxNetNs itself ("This function should be called only from a locked thread (i.e. runtime.LockOSThread())") and respected in Exec() at line 537; Kill() simply missed it.

This change locks the OS thread for the duration of Kill(), and snapshots/restores the original netns so the locked thread is not handed back to the runtime pool while still pointing at the sandbox netns.

Related issues

How was this tested?

  • make lint / go vet ./pkg/... clean.
  • go build ./... clean.
  • Reproducer:
    for i in $(seq 50); do
      nerdctl run -d --name uk$i --runtime io.containerd.urunc.v2 <image>
      nerdctl kill uk$i
    done
    nsenter -t <sandbox-pid> -n ip link | grep urunc | wc -l
    
    Before: 50. After: 0.

Checklist

  • I have read the contribution guide.
  • The linter passes locally (make lint).
  • The e2e tests of at least one tool pass locally — pending environment with kvm/devmapper available.

@netlify

netlify Bot commented Apr 29, 2026

Copy link
Copy Markdown

Deploy Preview for urunc canceled.

Name Link
🔨 Latest commit 80f887e
🔍 Latest deploy log https://app.netlify.com/projects/urunc/deploys/69f20933cff0d20008135731

Unikontainer.Kill() called joinSandboxNetNs() (which performs
unix.Setns(CLONE_NEWNET) on the calling OS thread only) without first
calling runtime.LockOSThread(). Between the setns and the subsequent
netlink work in network.CleanupAllUruncTaps(), the Go scheduler is
free to migrate the goroutine to another M still bound to the host
network namespace. When that happens, netlink.NewHandle() inside
CleanupAllUruncTaps creates its socket in the wrong namespace, the
^tap\d+_urunc$ scan finds zero matches, and Kill() returns success
while leaving the sandbox's TAP device, ingress qdisc, and tc
redirect filters in place.

The contract was already documented on joinSandboxNetNs ("This
function should be called only from a locked thread (i.e.
runtime.LockOSThread())") and respected in Exec(); Kill() simply
missed it.

Lock the OS thread for the duration of Kill(), and snapshot/restore
the original netns so the locked thread is not returned to the
runtime pool while still pointing at the sandbox netns.

Reproducer:
  for i in $(seq 50); do
    nerdctl run -d --name uk$i --runtime io.containerd.urunc.v2 <img>
    nerdctl kill uk$i
  done
  nsenter -t <sandbox-pid> -n ip link | grep urunc | wc -l

Before: 50.  After: 0.
Signed-off-by: mn-ram <235066282+mn-ram@users.noreply.github.com>
@mn-ram mn-ram force-pushed the fix/kill-lockosthread branch from 90a6a89 to 80f887e Compare April 29, 2026 13:35
@mn-ram mn-ram closed this Apr 29, 2026
@mn-ram

mn-ram commented Apr 29, 2026

Copy link
Copy Markdown
Author

Closing — the premise of this PR was incorrect. As @cmainas pointed out on #587, both callers of Unikontainer.Kill() (cmd/urunc/kill.go:47-48 and cmd/urunc/delete.go:49-50) already call runtime.GOMAXPROCS(1) + runtime.LockOSThread() at the CLI entry point, so the goroutine cannot migrate threads during the netns work. The fix is redundant. Apologies for the noise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: Kill() races on netns — missing runtime.LockOSThread() leaks sandbox TAP and tc filters

2 participants