|WARNING| [2/2] net/tap: fix use after free on interrupt handle

dpdklab at iol.unh.edu dpdklab at iol.unh.edu
Tue Nov 3 19:21:36 CET 2026


Test-Label: ai-code-review
Test-Status: WARNING
https://dpdk.org/patch/172605

_AI Code Review_

=== Patch Review: series.patch (via Claude) ===
Review date: 2026-11-03

# DPDK Patch Review

## Patch 1/2: net/tap: fix FD leak on netns change

### Errors

**Use-after-free risk in alarm callback registration**

The patch registers an alarm callback (`tap_lsc_register_alarm`) that will be invoked asynchronously after `tap_netns_change()` returns. If the device is closed or freed between alarm registration and alarm execution, the callback will access freed memory.

```c
/* In tap_netns_change() */
rte_eal_alarm_set(1, tap_lsc_register_alarm, dev);
tap_dev_ns_restore(orig_netns_fd);
/* Function returns - dev could be freed before alarm fires */
```

The alarm callback dereferences `dev` and `dev->data->dev_private` without any validation that the device is still valid. There is no reference counting or state check to prevent this race.

Suggested fix: Either increment a device reference count before registering the alarm and decrement it in the callback, or check `dev->data->dev_started` / device state before dereferencing in the callback. The alarm must also be cancelled in `tap_dev_close()` before freeing resources.

**Missing error check on `tap_nl_init()` return value**

```c
/* Recreate netlink socket in new namespace */
new_nlsk_fd = tap_nl_init(0);
```

If `tap_nl_init()` fails and returns -1, the code does not check this and continues. The old netlink FD may have been closed, leaving the driver in a broken state with no valid netlink socket.

Suggested fix:
```c
new_nlsk_fd = tap_nl_init(0);
if (new_nlsk_fd < 0) {
	TAP_LOG(ERR, "%s: failed to recreate netlink socket", pmd->name);
	tap_dev_ns_restore(orig_netns_fd);
	return -1;
}
```

**Resource leak if `setns()` fails in `tap_dev_ns_restore()`**

```c
static void
tap_dev_ns_restore(int orig_netns_fd)
{
	setns(orig_netns_fd, CLONE_NEWNET);
	close(orig_netns_fd);
}
```

If `setns()` fails, the error is silently ignored and `orig_netns_fd` is closed, but the process remains in the wrong namespace. While the FD is correctly closed (not a leak), the silent failure to restore the namespace could leave the driver in an inconsistent state for subsequent operations.

Suggested fix: Log an error if `setns()` fails:
```c
if (setns(orig_netns_fd, CLONE_NEWNET) < 0)
	TAP_LOG(ERR, "failed to restore original netns: %s", strerror(errno));
close(orig_netns_fd);
```

### Warnings

**Alarm set with hardcoded 1 microsecond delay**

```c
rte_eal_alarm_set(1, tap_lsc_register_alarm, dev);
```

The delay of 1 microsecond is extremely short and may not be sufficient for the interrupt thread to finish processing the current callback context. A longer delay (e.g., 1000 us = 1 ms) would be safer. However, this is a judgement call about timing, not a correctness bug.

**Inconsistent error handling between `tap_netns_change()` and `tap_lsc_register_alarm()`**

In `tap_netns_change()`, failure to query the interface name logs a warning but continues. In `tap_lsc_register_alarm()`, failure to switch namespace silently returns without logging. The latter should log an error for consistency and debuggability:

```c
if (tap_dev_ns_switch(pmd, &orig_netns_fd) < 0) {
	TAP_LOG(ERR, "%s: failed to switch namespace in alarm", pmd->name);
	return;
}
```

## Patch 2/2: net/tap: fix use after free on interrupt handle

### Errors

**Logic error: `rte_intr_fd_get() < 0` check is backwards**

```c
clean:
	rte_eal_alarm_cancel(tap_lsc_register_alarm, dev);
	if (rte_intr_fd_get(pmd->intr_handle) < 0)
		return 0;

	do {
		ret = rte_intr_callback_unregister(pmd->intr_handle,
			tap_dev_intr_handler, dev);
```

The code returns 0 (success) if the interrupt FD is invalid, skipping the unregister loop. This is backwards: if the FD is invalid, the interrupt handle was never set up, so there is nothing to unregister and returning early is correct. However, this check is placed *after* the label where the teardown path jumps to (`clean:`), meaning it will also skip unregistering when called from the error path of the setup (`if (set)`) block.

The intent appears to be: skip unregister if the handle was never initialized. But the placement means it also skips cleanup on the error path where the handle *was* partially initialized. This could leave stale callbacks registered.

Suggested fix: Move the check to apply only when called from the `!set` path, or restructure the function so that the early return does not also affect the error cleanup path:

```c
	if (!set) {
		if (rte_intr_fd_get(pmd->intr_handle) < 0)
			return 0;
		goto clean;
	}

	/* setup code */
	if (error) {
		goto clean;  /* Must still run cleanup even if fd < 0 */
	}
	return 0;

clean:
	rte_eal_alarm_cancel(tap_lsc_register_alarm, dev);
	do {
		ret = rte_intr_callback_unregister(...);
	} while (ret == -EAGAIN);
	...
```

### Warnings

**Missing alarm cancellation in `tap_dev_close()`**

The patch adds `tap_intr_handle_set(dev, 0)` to `tap_dev_close()`, which cancels the alarm in `tap_lsc_intr_handle_set()`. However, the alarm callback (`tap_lsc_register_alarm`) may have already fired and be holding references. A direct `rte_eal_alarm_cancel(tap_lsc_register_alarm, dev)` in `tap_dev_close()` before calling `tap_intr_handle_set(dev, 0)` would be clearer and more defensive:

```c
tap_dev_close(struct rte_eth_dev *dev)
{
	/* ... */
	rte_eal_alarm_cancel(tap_lsc_register_alarm, dev);
	tap_intr_handle_set(dev, 0);
	/* ... */
}
```

This addresses the race condition mentioned in Patch 1 more directly.

---

Review-Result: ERROR


More information about the test-report mailing list