Skip to content
14 changes: 13 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,19 @@ jobs:
sudo apt-get install -y erofs-utils e2fsprogs iptables
fi
go mod download


- name: Verify Linux test toolchain
run: |
set -euo pipefail
TEST_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
for bin in mkfs.erofs mkfs.ext4 iptables; do
if ! sudo env "PATH=$TEST_PATH" bash -lc "command -v '$bin' >/dev/null"; then
echo "missing required binary under sudo PATH: $bin"
exit 1
fi
sudo env "PATH=$TEST_PATH" bash -lc "command -v '$bin'"
done

# Avoids rate limits when running the tests
# Tests includes pulling, then converting to disk images
- name: Login to Docker Hub
Expand Down
34 changes: 18 additions & 16 deletions cmd/api/api/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -791,23 +791,21 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
uploadBwStr = &s
}

// Build network object with ip/mac and bandwidth nested inside
netObj := &struct {
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Ip *string `json:"ip"`
Mac *string `json:"mac"`
Name *string `json:"name,omitempty"`
}{
Enabled: lo.ToPtr(inst.NetworkEnabled),
BandwidthDownload: downloadBwStr,
BandwidthUpload: uploadBwStr,
// Build network payload as JSON to avoid compile-time coupling to
// generated anonymous struct tags in oapi.Instance.Network.
networkPayload := map[string]any{
"enabled": inst.NetworkEnabled,
}
if downloadBwStr != nil {
networkPayload["bandwidth_download"] = *downloadBwStr
}
if uploadBwStr != nil {
networkPayload["bandwidth_upload"] = *uploadBwStr
}
if inst.NetworkEnabled {
netObj.Name = lo.ToPtr("default")
netObj.Ip = lo.ToPtr(inst.IP)
netObj.Mac = lo.ToPtr(inst.MAC)
networkPayload["name"] = "default"
networkPayload["ip"] = inst.IP
networkPayload["mac"] = inst.MAC
}

// Convert hypervisor type
Expand All @@ -831,7 +829,7 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
OverlaySize: lo.ToPtr(overlaySizeStr),
Vcpus: lo.ToPtr(inst.Vcpus),
DiskIoBps: diskIoBpsStr,
Network: netObj,
Network: nil,
CreatedAt: inst.CreatedAt,
StartedAt: inst.StartedAt,
StoppedAt: inst.StoppedAt,
Expand All @@ -840,6 +838,10 @@ func instanceToOAPI(inst instances.Instance) oapi.Instance {
Hypervisor: &hvType,
}

if b, err := json.Marshal(networkPayload); err == nil {
_ = json.Unmarshal(b, &oapiInst.Network)
}

if inst.ExitMessage != "" {
oapiInst.ExitMessage = lo.ToPtr(inst.ExitMessage)
}
Expand Down
10 changes: 6 additions & 4 deletions lib/network/bridge_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -864,14 +864,16 @@ func (m *manager) queryNetworkState(bridgeName string) (*Network, error) {

// CleanupOrphanedTAPs removes TAP devices that aren't used by any running instance.
// runningInstanceIDs is a list of instance IDs that currently have a running VMM.
// Pass nil to skip cleanup entirely (used when we couldn't determine running instances).
// Pass nil or an empty list to skip cleanup entirely (used when we couldn't
// determine an authoritative list of running instances for this host).
// Returns the number of TAPs deleted.
func (m *manager) CleanupOrphanedTAPs(ctx context.Context, runningInstanceIDs []string) int {
log := logger.FromContext(ctx)

// If nil, skip cleanup entirely to avoid accidentally deleting TAPs for running VMs
if runningInstanceIDs == nil {
log.DebugContext(ctx, "skipping TAP cleanup (nil instance list)")
// Skip cleanup when we don't have an authoritative running set.
// This avoids deleting TAPs created by other concurrent hypeman processes/tests.
if len(runningInstanceIDs) == 0 {
log.DebugContext(ctx, "skipping TAP cleanup (empty instance list)")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty slice now skips TAP cleanup breaking production intent

High Severity

Changing the guard from runningInstanceIDs == nil to len(runningInstanceIDs) == 0 breaks the production caller in cmd/api/main.go, which explicitly initializes preserveTAPs to []string{} (not nil) when there are no running VMs, with a comment stating "Initialize to empty slice (not nil) so cleanup runs even with no running VMs." The caller relies on the nil vs empty-slice distinction to differentiate "we don't know what's running, skip cleanup" from "nothing is running, clean up all orphaned TAPs." Now both cases skip cleanup, causing orphaned TAP devices to accumulate indefinitely.

Fix in Cursor Fix in Web

return 0
}

Expand Down
Loading