Summary
While reviewing the container teardown lifecycle in pkg/unikontainers/unikontainers.go, I noticed that the Kill() method calls network.CleanupAllUruncTaps() - a function that unconditionally deletes every TAP device on the host whose name matches the pattern ^tap\d+_urunc$, regardless of which container owns it.
This means that when a single unikernel container is killed, all other concurrently running unikernel containers on the same host immediately lose their network connectivity, because their TAP devices are silently destroyed as a side effect.
Affected Code
File: pkg/unikontainers/unikontainers.go (inside Kill())
func (u *Unikontainer) Kill() error {
err := u.joinSandboxNetNs()
if err != nil {
// ...
}
vmmType := u.State.Annotations[annotHypervisor]
vmm, err := hypervisors.NewVMM(hypervisors.VmmType(vmmType), u.UruncCfg.Monitors)
if err != nil {
return err
}
err = vmm.Stop(u.State.Pid)
if err != nil {
return err
}
err = network.CleanupAllUruncTaps() // ← deletes ALL urunc TAP devices, not just this container's
if err != nil {
uniklog.Errorf("failed to cleanup tap devices: %v", err)
}
return nil
}
File: pkg/network/network.go (inside CleanupAllUruncTaps())
func CleanupAllUruncTaps() error {
handle, err := netlink.NewHandle()
// ...
links, err := handle.LinkList()
// ...
tapRe := regexp.MustCompile(`^tap\d+_urunc$`) // matches every urunc TAP on the host
for _, link := range links {
name := attrs.Name
if !tapRe.MatchString(name) {
continue
}
// No ownership check - deletes unconditionally for every match
deleteAllTCFilters(link)
deleteAllQDiscs(link)
deleteTapDevice(link)
}
return retErr
}
There is no association stored between a TAP device name and the container that created it. CleanupAllUruncTaps() simply destroys every interface that looks like a urunc TAP, making it fundamentally unsafe to call on individual container teardown when other containers are still running.
Impact
Silent network outage for surviving containers: If containers A, B, and C are running simultaneously with TAP devices tap0_urunc, tap1_urunc, and tap2_urunc respectively, killing container A results in all three TAP devices being deleted. Containers B and C continue to have their VMM processes running, but their network backend has been torn down silently - from the guest's perspective this looks like a physical cable unplug with no error or log on either the host or the guest side.
No warning or recovery path: Because Kill() only logs a warning if CleanupAllUruncTaps() itself errors (which it won't - deleting other containers' devices succeeds), the operator has no way to detect that surviving containers have been affected.
Reproducible at any scale: This affects any deployment running more than one networked unikernel container at a time, including Kubernetes nodes using urunc as a runtime.
Proposed Solution
Kill() should only remove the TAP device(s) belonging to the container being killed. Two viable approaches:
Option A - Store the TAP device name in monitor resources during setup.
When a TAP device is created for a container in networkSetup(), persist its name into the monitor resources file (alongside the existing block and mount data). In Kill(), load the stored name and call a scoped CleanupUruncTap(tapName string) that targets only that one device, while keeping CleanupAllUruncTaps() available for full-host recovery scenarios (e.g., daemon restart).
Option B - Derive the TAP name deterministically from the container ID.
Replace the incremental counter-based naming (tap0_urunc, tap1_urunc, ...) with a name derived from the container ID (e.g., a short hash prefix: tap_<8chars>_urunc). Kill() can then reconstruct the exact TAP name for the container being killed without needing to enumerate or match all TAP interfaces on the host.
Summary
While reviewing the container teardown lifecycle in
pkg/unikontainers/unikontainers.go, I noticed that theKill()method callsnetwork.CleanupAllUruncTaps()- a function that unconditionally deletes every TAP device on the host whose name matches the pattern^tap\d+_urunc$, regardless of which container owns it.This means that when a single unikernel container is killed, all other concurrently running unikernel containers on the same host immediately lose their network connectivity, because their TAP devices are silently destroyed as a side effect.
Affected Code
File:
pkg/unikontainers/unikontainers.go(insideKill())File:
pkg/network/network.go(insideCleanupAllUruncTaps())There is no association stored between a TAP device name and the container that created it.
CleanupAllUruncTaps()simply destroys every interface that looks like a urunc TAP, making it fundamentally unsafe to call on individual container teardown when other containers are still running.Impact
Silent network outage for surviving containers: If containers A, B, and C are running simultaneously with TAP devices
tap0_urunc,tap1_urunc, andtap2_uruncrespectively, killing container A results in all three TAP devices being deleted. Containers B and C continue to have their VMM processes running, but their network backend has been torn down silently - from the guest's perspective this looks like a physical cable unplug with no error or log on either the host or the guest side.No warning or recovery path: Because
Kill()only logs a warning ifCleanupAllUruncTaps()itself errors (which it won't - deleting other containers' devices succeeds), the operator has no way to detect that surviving containers have been affected.Reproducible at any scale: This affects any deployment running more than one networked unikernel container at a time, including Kubernetes nodes using urunc as a runtime.
Proposed Solution
Kill()should only remove the TAP device(s) belonging to the container being killed. Two viable approaches:Option A - Store the TAP device name in monitor resources during setup.
When a TAP device is created for a container in
networkSetup(), persist its name into the monitor resources file (alongside the existing block and mount data). InKill(), load the stored name and call a scopedCleanupUruncTap(tapName string)that targets only that one device, while keepingCleanupAllUruncTaps()available for full-host recovery scenarios (e.g., daemon restart).Option B - Derive the TAP name deterministically from the container ID.
Replace the incremental counter-based naming (
tap0_urunc,tap1_urunc, ...) with a name derived from the container ID (e.g., a short hash prefix:tap_<8chars>_urunc).Kill()can then reconstruct the exact TAP name for the container being killed without needing to enumerate or match all TAP interfaces on the host.