Skip to content

Migrate virtio-net from semu - #748

Open
Charlie-Tsai1123 wants to merge 14 commits into
sysprog21:masterfrom
Charlie-Tsai1123:research/virtio-net
Open

Charlie-Tsai1123 wants to merge 14 commits into
sysprog21:masterfrom
Charlie-Tsai1123:research/virtio-net

Conversation

@Charlie-Tsai1123

@Charlie-Tsai1123 Charlie-Tsai1123 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR migrates virtio-net support from semu and follows the existing virtio-blk and virtio-rng integration in rv32emu.

The implementation adds a TAP-backed virtio-net device model for system emulation mode, including MMIO register handling, queue setup, feature negotiation, RX/TX virtqueue handling, virtio-net header processing, interrupt delivery through the PLIC, dynamic DTB node creation, and a runtime option for enabling the device.

Implementation notes

  • Add a TAP backend helper for host network I/O.
  • Add a virtio-net device model with RX and TX virtqueue handling.
  • Handle the virtio-net header on both guest TX and RX paths.
  • Route virtio-net MMIO accesses through the system MMIO path.
  • Update virtio-net interrupts through the PLIC.
  • Dynamically create a virtio,mmio DTB node for virtio-net.
  • Ensure virtio-net can coexist with existing virtio-blk and virtio-rng devices without reusing MMIO bases or IRQs.
  • Add a runtime option for enabling virtio-net with a TAP backend.
  • Note that opening a TAP device usually requires elevated privileges, so rv32emu should be executed with sudo or equivalent permissions when using the TAP backend.

Test

If don't have build/linux-image/Image

make build-linux-image

Build:

make system_defconfig
make ENABLE_SYSTEM=1

sudo build/rv32emu \
  -k build/linux-image/Image \
  -i build/linux-image/rootfs.cpio \
  -x vrng \
  -x vnet:tap

After run rv32emu with -x vnet:tap rv32emu would build TAP, so host linux doesn't need to build TAP again.
Host TAP setup:

ip link | grep tap
sudo ip addr add 192.168.100.1/24 dev tap0
sudo ip link set  tap0 up
sudo tcpdump -i tap0 -n -vv

Guest device verification:

readlink /sys/bus/virtio/devices/virtio0/driver
ip link set eth0 up
ip addr add 192.168.100.2/24 dev eth0
ip addr show eth0
ping -c 3 192.168.100.1

Expected Result:

# readlink /sys/bus/virtio/devices/virtio0/driver
../../../../../bus/virtio/drivers/virtio_net
# ip link set eth0 up
# ip addr add 192.168.100.2/24 dev eth0
# ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast qlen 1000
    link/ether aa:e0:56:5b:58:bc brd ff:ff:ff:ff:ff:ff
    inet 192.168.100.2/24 scope global eth0
       valid_lft forever preferred_lft forever
# ping -c 3 192.168.100.1
PING 192.168.100.1 (192.168.100.1): 56 data bytes
64 bytes from 192.168.100.1: seq=0 ttl=64 time=1.200 ms

64 bytes from 192.168.100.1: seq=1 ttl=64 time=1001.085 ms
64 bytes from 192.168.100.1: seq=2 ttl=64 time=1.334 ms

--- 192.168.100.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 1.200/334.539/1001.085 ms
# 

Summary by cubic

Migrates virtio-net support from semu, adding a VirtIO-MMIO network device with build-time selectable host backends and runtime selection via -x vnet:tap, -x vnet:user, or -x vnet:vmnet. The device gets a DTB node and is covered by Linux and macOS boot tests.

New Features

  • Virtio-net device handles feature negotiation, RX/TX queues, virtio-net headers, queue notifies, used rings, and reset; queues poll every 5000 guest cycles and drive SLIRP via non-blocking socketpairs with tracked timers.
  • tap is Linux-only and needs host privileges; user needs no host setup on Linux and macOS; vmnet is macOS/Clang-only in shared mode; Emscripten compiles networking out; only one vnet device is allowed and CLI validation lists only compiled backends.
  • Kconfig adds CONFIG_VIRTIO_NET, CONFIG_VIRTIO_NET_TAP, CONFIG_VIRTIO_NET_USER, and CONFIG_VIRTIO_NET_VMNET; unused net objects are excluded; src/minislirp builds only for user; MMIO/DTB/interrupt wiring compiles only when enabled; macOS links -lresolv.
  • CI/docs run .ci/netdev.sh from .ci/boot-linux.sh, add Linux user/tap and macOS user/conditional vmnet boot jobs, make ARM64 apt-get retry with IPv4 and install expect, and document usage in docs/networking.md.

Bug Fixes

  • Update the PLIC after queue refresh and raise used-ring interrupts only when the used index advances, fixing delayed interrupts and reducing TAP ping latency.
  • Cold reboot reuses the existing virtio-net device and resets guest-visible state, preserving the host backend.
  • Advertise MAC, link status, and MTU features and read device config at byte offsets so the guest uses the configured MAC instead of a random one.
  • Handle virtqueue index wraparound for virtio-net, virtio-blk, and virtio-rng, and drop TX packets on permanent backend errors so queues keep progressing.
  • Make virtio-net.o wait for the SoftFloat submodule to avoid a clean parallel build race.

Written for commit e8dd4eb. Summary will update on new commits.

Review in cubic

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Benchmarks

Details
Benchmark suite Current: e8dd4eb Previous: c3d9c03 Ratio
Dhrystone 1551.333 DMIPS 1575 DMIPS 1.02
CoreMark 1111.051 iterations/sec 1109.809 iterations/sec 1.00

This comment was automatically generated by workflow using github-action-benchmark.

cubic-dev-ai[bot]

This comment was marked as resolved.

@Charlie-Tsai1123
Charlie-Tsai1123 force-pushed the research/virtio-net branch 2 times, most recently from 7658d0a to b775142 Compare July 1, 2026 15:18
@shengwen-tw

Copy link
Copy Markdown
Contributor

Hi @Charlie-Tsai1123,

I’m not the original author of the virtio-net device. Please check the commit history via git log --follow -- virtio-net.c; I believe the original author is @jserv.

@Charlie-Tsai1123

Copy link
Copy Markdown
Contributor Author

Thanks for the clarification, and sorry for the incorrect attribution.
I checked the semu history with git log --follow -- virtio-net.c. The original virtio-net implementation appears to have been introduced by jserv. I will update the commit message accordingly.

jserv

This comment was marked as resolved.

@jserv

jserv commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Thanks for the clarification, and sorry for the incorrect attribution. I checked the semu history with git log --follow -- virtio-net.c. The original virtio-net implementation appears to have been introduced by jserv. I will update the commit message accordingly.

Remove "This implementation is based on semu's virtio-net device, originally introduced by Jserv" as you already append "Co-authored-by: Jim Huang".

@Charlie-Tsai1123
Charlie-Tsai1123 force-pushed the research/virtio-net branch 2 times, most recently from 6dd32bc to 198ea5c Compare July 16, 2026 08:14
cubic-dev-ai[bot]

This comment was marked as resolved.

@jserv
jserv requested a review from ChinYikMing July 16, 2026 09:19
@jserv jserv added this to the release-2026.2 milestone Jul 16, 2026

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rework semu's network infrastructure, as described in networking.md:

  • Linux: TAP (kernel-level) and user-mode (SLIRP) networking
  • macOS: vmnet.framework (kernel-level NAT; bridge mode planned) and user-mode (SLIRP) networking

For this pull request, both TAP and SLIRP should be landed.

Comment thread .ci/test-netdev.sh Outdated
Comment thread src/main.c Outdated
Comment thread src/main.c
Comment thread src/riscv.c Outdated
Comment thread src/riscv.c Outdated
cubic-dev-ai[bot]

This comment was marked as resolved.

@Charlie-Tsai1123
Charlie-Tsai1123 force-pushed the research/virtio-net branch 2 times, most recently from c48ad2a to 103be25 Compare July 25, 2026 10:19
@Charlie-Tsai1123

Copy link
Copy Markdown
Contributor Author

This update mainly covers two parts:

  1. virtio-net interrupt delivery
    Related commit: cf42444

I moved virtio-net refresh and interrupt propagation into the per-step execution path. After queue refresh, the virtio-net interrupt state is pushed to the PLIC. This avoids delaying completed RX/TX work until a later interrupt update point.

I also updated virtio_net_try_rx() and virtio_net_try_tx() so they only raise used-ring interrupts when the used ring index actually advances. This avoids repeated interrupts when no virtqueue progress was made, and fixes the observed TAP ping latency spike.

Before this change, TAP ping could show delayed replies such as:

PING 192.168.100.1 (192.168.100.1): 56 data bytes
64 bytes from 192.168.100.1: seq=0 ttl=64 time=3.831 ms
64 bytes from 192.168.100.1: seq=1 ttl=64 time=1001.088 ms
64 bytes from 192.168.100.1: seq=2 ttl=64 time=1.389 ms

After the change, replies are delivered promptly:

PING 192.168.100.1 (192.168.100.1): 56 data bytes
64 bytes from 192.168.100.1: seq=0 ttl=64 time=1.181 ms
64 bytes from 192.168.100.1: seq=1 ttl=64 time=0.546 ms
64 bytes from 192.168.100.1: seq=2 ttl=64 time=0.565 ms
  1. user-mode SLIRP backend
    Related commit: 103be25

I added a minislirp-based user-mode backend for virtio-net, adapted from semu. The backend connects the virtio-net RX/TX paths with libslirp through non-blocking socketpairs, so guest networking can work without TAP, root rivileges, or host network setup.

The current backend support is:

  • Linux : vnet:tap and vnet:user
  • macOS : vnet:user
  • Emscripten: no tap, no user, no vmnet

Emscripten is handled separately because rv32emu supports emcc builds, unlike semu's original networking setup. For emcc, the networking backends are disabled and unsupported vnet backends are rejected during argument parsing.

CI coverage is arranged as follows:

  • Linux x64 interpreter matrix runs both VNET_BACKEND=user and VNET_BACKEND=tap.
  • macOS runs the user-mode SLIRP boot test.
  • TAP is only tested on Linux because it depends on the Linux TAP/TUN interface.
  • virtio-net tests are not run for every JIT/T2C/MOP-fusion matrix entry, only run in the interpreter matrix.

Future PR may work:

  • vmnet.framework backend:
    I looked at semu's vmnet implementation. It uses Apple's vmnet.framework and Blocks syntax for callbacks. That path is suitable for Clang with Blocks support, but it is not suitable for the current macOS gcc-15 CI job. vmnet also has macOS-specific privilege / entitlement requirements. A follow-up could add vmnet through a clang-only macOS path or a more portable wrapper.

  • Event-driven wakeup:
    The current backend follows rv32emu's existing polling-based device refresh model and uses non-blocking I/O. This keeps the initial migration simple and consistent with the current virtio-net execution path. In a follow-up, this can be improved with an event-driven wakeup mechanism, such as eventfd on Linux or a pipe/condvar-style notification path, so host-side activity can wake the emulator loop instead of relying on frequent polling.

  • Emscripten support for virtio-net

@ChinYikMing ChinYikMing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please help adding backend selection into Kconfig. This can reduce the binary size of rv32emu exe if user might not need the functionality of vnet.

Comment thread src/devices/netdev.h Outdated
Comment thread src/devices/netdev.h Outdated
Comment thread src/devices/netdev.h Outdated
@ChinYikMing

Copy link
Copy Markdown
Collaborator

Hi @Charlie-Tsai1123 , could you document how to test the user mode vnet backend as I only see the tap backend is listed at here? It will be easier for reviewer to quick test . Thanks.

Comment thread src/main.c Outdated
Comment thread src/devices/netdev.c

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 14 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread mk/system.mk Outdated
@Charlie-Tsai1123

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I have added build-time backend selection through Kconfig and documented how to test the user-mode backend.

test user-mode

Build (Host):

make system_defconfig
make ENABLE_SYSTEM=1

build/rv32emu \
  -k build/linux-image/Image \
  -i build/linux-image/rootfs.cpio \
  -x vnet:user

Guest device verification (Guest):

readlink /sys/bus/virtio/devices/virtio0/driver
ip link set eth0 up
ip addr flush dev eth0
ip addr add 10.0.2.15/24 dev eth0
ip route add default via 10.0.2.2
ip addr show eth0
ip route
ping -c 3 -W 5 10.0.2.2

Expected result (Guest):

# readlink /sys/bus/virtio/devices/virtio0/driver
../../../../../bus/virtio/drivers/virtio_net
# ip link set eth0 up
# ip addr flush dev eth0
# ip addr add 10.0.2.15/24 dev eth0
# ip route add default via 10.0.2.2
# ip addr show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast qlen 1000
    link/ether c2:84:a2:01:23:bb brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 scope global eth0
       valid_lft forever preferred_lft forever
# ip route
default via 10.0.2.2 dev eth0 
10.0.2.0/24 dev eth0 scope link  src 10.0.2.15 
# ping -c 3 -W 5 10.0.2.2
PING 10.0.2.2 (10.0.2.2): 56 data bytes
64 bytes from 10.0.2.2: seq=0 ttl=255 time=1.125 ms
64 bytes from 10.0.2.2: seq=1 ttl=255 time=0.526 ms
64 bytes from 10.0.2.2: seq=2 ttl=255 time=0.544 ms

--- 10.0.2.2 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.526/0.731/1.125 ms

or test with the existing automated user-mode network test (Host):

VNET_BACKEND=user bash .ci/boot-linux.sh

Kconfig changes

The following options were added:

CONFIG_VIRTIO_NET
CONFIG_VIRTIO_NET_TAP
CONFIG_VIRTIO_NET_USER

Default system_defconfig still contain virtio-net setting, user could disable it by using make config and cancel it:

make config

select Execution Modes:
image

Under the system-emulation configuration, users can independently enable or disable:
image
Disabling CONFIG_VIRTIO_NET excludes virtio-net.o netdev.o slirp.o from the build:

When CONFIG_VIRTIO_NET is enabled but neither TAP nor USER is selected, virtio-net is also treated as unavailable and the network objects are excluded.

When user cancel virtio-net, the binary size of rv32emu exe decrease. I copy the build file and test

copy file after make for different config setting:

cp build/rv32emu /tmp/rv32emu-no-vnet
cp build/rv32emu /tmp/rv32emu-vnet-full

Then their size:

charlie-tsai:/media/charlie-tsai/DATA/road_to_master/rv32emu$stat -c '%n: %s bytes'     /tmp/rv32emu-vnet-full     /tmp/rv32emu-no-vnet
/tmp/rv32emu-vnet-full: 344416 bytes
/tmp/rv32emu-no-vnet: 229560 bytes

Comment thread .ci/netdev.sh Outdated
Comment thread docs/networking.md Outdated
Charlie-Tsai1123 and others added 13 commits September 11, 2026 23:03
This commit migrates virtio-net support from semu with the
following modifications:

1. Implement virtio-net device model
The virtio-net implementation follows the VirtIO-MMIO flow used by
virtio-blk, including feature negotiation, queue setup, QueueNotify
handling, used ring update, interrupt status, and device status reset.

The device currently supports a TAP-backend network interface and
handles basic RX/TX virtqueue processing for guest network packets.

2. Add TAP backend helper
Introduce netdev.c and netdev.h to provide host-side TAP device access.
Future work may support other host-side backend.

3. Handle virtio-net header processing
For guest TX, the device skips the virtio-net header before writing the
Ethernet frame to the TAP backend.
For guest RX, the device prepends a virtio-net header before copying
the received Ethernet frame into the guest-provided RX buffer.

4. Implement MMIO_VIRTIONET
Add MMIO routing for virtio-net and connect the device interrupt status
to the PLIC, following the existing virtio-blk and virtio-rng interrupt
update model.

5. Introduce new argument '-x vnet:<tap>'
When virtio-net is enabled, rv32emu dynamically creates a virtio-mmio
node in the generated device tree and assigns an MMIO base address and
IRQ for the device.

6. Support coexistence with virtio-blk and virtio-rng
Update the dynamic virtio-mmio device tree allocation path so
virtio-net can coexist with existing virtio-blk and virtio-rng devices
without reusing MMIO base addresses or IRQs.

7. Use virtio-net state
Unlike semu's device integration model, rv32emu stores the virtio-net
state in vm_attr_t so MMIO routing, interrupt routing, and device
cleanup can access the same device instance.

The emulator should be run with sudo when using the virtio-net TAP
backend.

Co-authored-by: Jim Huang <jserv@biilabs.io>
The host-arm64 CI job may fail before running any build or test when
apt cannot update package indexes due to transient network issues on
the Ubuntu ports mirror. Add retry logic and force IPv4 when updating
the apt cache in install-llvm.sh to make LLVM repository setup more
robust on GitHub-hosted ARM runners.

The host-arm64 dependency installation also has a fallback path for
partially failed apt installs. However, the fallback only attempted to
install make, curl, and wget. As a result, the later Linux boot test
could fail with "expect: command not found" even though expect is a
required dependency for .ci/boot-linux.sh.
virtio-net queue refresh can complete RX/TX descriptors and set the
device interrupt status, but PLIC line was not updated immediately
after refresh. This could leave the guest waiting until later interrupt
update point before observing completed network work, causing TAP ping
latency spikes of around one second.

Update the virtio-net interrupt after queue refresh.
This delivers completed network packets to the guest promptly.

Also make virtio_net_try_rx() and virtio_net_try_tx() raise used-ring
interrupts only when used ring index actually advances. TAP is often
reported writable by poll(), so trying TX without completing any
descriptor must not set VIRTIO_INT_USED_RING. Otherwise guest may see
repeated interrupts without corresponding used-ring updates.

Before this change, TAP ping could show delayed replies such as:

PING 192.168.100.1 (192.168.100.1): 56 data bytes
64 bytes from 192.168.100.1: seq=0 ttl=64 time=3.831 ms
64 bytes from 192.168.100.1: seq=1 ttl=64 time=1001.088 ms
64 bytes from 192.168.100.1: seq=2 ttl=64 time=1.389 ms

After the change, replies are delivered promptly:

PING 192.168.100.1 (192.168.100.1): 56 data bytes
64 bytes from 192.168.100.1: seq=0 ttl=64 time=1.181 ms
64 bytes from 192.168.100.1: seq=1 ttl=64 time=0.546 ms
64 bytes from 192.168.100.1: seq=2 ttl=64 time=0.565 ms
Migrate semu's user-mode virtio-net networking support to rv32emu.

Add a minislirp backend that allows virtio-net to operate without TAP,
root privileges, or host network configuration. Connect the guest
RX/TX paths to libslirp through non-blocking socketpairs.

Adapt semu's timer and event integration to rv32emu by using a
CLOCK_MONOTONIC-based timer wrapper and driving SLIRP progress from the
existing virtio-net refresh path.

Support the standard SLIRP guest network configuration with
10.0.2.15/24 as the guest address and 10.0.2.2 as the gateway.

Co-authored-by: Jim Huang <jserv@biilabs.io>
Add Kconfig options for the virtio-net device, Linux TAP backend,
and user-mode SLIRP backend.

Exclude unused network objects at build time, build minislirp only
when the user backend is enabled, and guard the related CLI, runtime,
MMIO, DTB, and interrupt integration.

Also list only compiled backends in the CLI help and consolidate
backend initialization through a shared helper (netdev_setup in
src/devices/netdev.c).
Ensure virtio-net.o waits for the SoftFloat dependency before
compilation. This fixes a clean parallel build race where
virtio-net could be compiled before the SoftFloat submodule
was initialized.
Reuse the existing virtio-net device during a cold reboot and reset
its guest-visible state instead of allocating a new device. Preserve
the host networking backend across reboots.
Compute the number of pending descriptors in uint16_t before comparing
it with the queue size. Direct subtraction promotes the 16-bit queue
indices to int, so a wrapped index can become a negative value and
bypass validation.

For example, last_avail = 100 and new_avail = 50 evaluates to -50
instead of the intended modulo-65536 result 65486. This can make the
processing loop walk tens of thousands of descriptors that were never
posted by the guest.

Apply the same fix to virtio-blk, virtio-rng, and virtio-net.
Distinguish retryable transmit failures from permanent backend errors.
EAGAIN, EWOULDBLOCK, and EINTR leave the current descriptor pending so
the packet can be retried later.

Previously, every writev failure stopped TX processing without
advancing last_avail. A permanent error such as EMSGSIZE therefore
caused the same descriptor to be rebuilt and submitted again on every
queue refresh, repeatedly logging the same error and preventing later
TX descriptors from making progress.

Drop packets that fail with a permanent error and complete their
descriptors so the guest can continue processing the transmit queue.
Advertise the MAC address, link status, and MTU features provided by
the virtio-net device, and preserve byte offsets when accessing the
device-specific configuration space.

Previously, only VIRTIO_F_VERSION_1 was advertised, so Linux ignored
the configured MAC address and generated a random address instead.
After advertising VIRTIO_NET_F_MAC, VIRTIO_NET_F_STATUS, and
VIRTIO_NET_F_MTU, the existing config access exposed another issue:
virtio_net_read shifted every MMIO address by two bits before handling
the device-specific configuration.

This discarded the low address bits used for byte-sized config fields.
For example, accesses to offsets 0, 1, 2, and 3 of the MAC address all
mapped to the same 32-bit index. As a result, the configured address

    52:54:00:12:34:56

was observed by the guest as

    52:52:52:52:34:34

Handle the device-specific configuration using its original byte
address and only convert addresses to 32-bit register indices for the
VirtIO MMIO transport registers. This also allows the guest to read the
configured link status and MTU correctly.
Poll the virtio-net backend only after several thousand guest cycles
instead of after every rv_step invocation.

The default execution loop advances about 100 guest instructions per
rv_step. Previously, virtio_net_refresh_queue was therefore called
after roughly every 100 instructions. The TAP backend issued a poll
system call on every refresh, while the user-mode backend performed
multiple zero-timeout polls and SLIRP pollfd walks.

Use the guest cycle counter to refresh the backend every 5000 cycles.
This substantially reduces host polling overhead while keeping the
network responsive.
Keep every timer created by minislirp instead of storing only the most
recent timer in net_user_options_t.

Link all timers into a list, unlink them when timer_free is invoked,
and scan the list for expired timers. Restart the scan after each timer
callback because minislirp may modify the timer list while servicing an
expiration.

Clean up any timers that remain after the SLIRP instance is destroyed.
Integrate virtio-net validation into the existing Linux boot test
flow.

Exercise both TAP and user-mode SLIRP backends on Linux, and run the
user-mode SLIRP backend on macOS. Reuse the common boot setup and
cleanup path instead of maintaining standalone network boot steps.

Keep network-specific guest setup and connectivity checks in
netdev.sh while invoking them from the modular Linux boot test suite.
@Charlie-Tsai1123
Charlie-Tsai1123 force-pushed the research/virtio-net branch 2 times, most recently from 1957430 to c3d9c03 Compare September 12, 2026 16:20
Migrate the vmnet.framework network backend from semu and integrate it
with the existing virtio-net implementation on macOS.

Implement shared, host-only, and bridged vmnet initialization paths,
while exposing shared mode through the existing vnet backend selection.
Factor the common interface setup, MAC address handling, and packet
callback registration into shared helpers to avoid duplicating the
three mode-specific initialization paths.

Bridge vmnet's asynchronous receive callbacks into rv32emu's polling
model through a non-blocking pipe while preserving Ethernet packet
boundaries. Forward guest TX descriptor chains directly to
vmnet_write() and translate vmnet failures into errno values so the
existing virtio-net retry and drop policy remains applicable.

Use the MAC address assigned by vmnet.framework as the guest-visible
VirtIO network MAC address.

Add build-time configuration for the vmnet backend on macOS Clang,
including Blocks support and vmnet.framework linking. Keep the backend
disabled for GCC builds so unsupported Blocks-based vmnet code is not
compiled.

Extend the existing network boot tests to exercise vmnet shared mode
on macOS while retaining the existing TAP and user-mode SLIRP tests.

Co-authored-by: Jim Huang <jserv@biilabs.io>
Comment thread .ci/boot-linux.sh
(. "${SCRIPT_DIR}/virtio-blk.sh")
RET=$((${RET} + $?))

# Virtio-net user-mode backend test

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The vmnet block below checks .config before running, but the user and tap blocks do not. With CONFIG_VIRTIO_NET_USER=n (or CONFIG_VIRTIO_NET_TAP=n), virtio_net_backend_supported() rejects -x vnet:user during argument parsing, so rv32emu exits immediately and the whole boot suite fails instead of skipping the test. Guard all three the same way, for example with grep -q '^CONFIG_VIRTIO_NET_USER=y$' .config.

Comment thread .ci/netdev.sh
register_cleanup cleanup_emulator

TIMEOUT=${NETDEV_BOOT_TIMEOUT:-${TIMEOUT}}
MESSAGES+=("${COLOR_R}Fail to ping gateway")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The appended entry covers exit code 4, but the expect block also exits 2 when readlink /sys/bus/virtio/devices/virtio0/driver does not report virtio_net, and that indexes the inherited Fail to login message. A guest that boots and logs in fine but never binds the driver gets reported as a login failure. Give that case its own message, or move the exits past the end of the inherited array.

*/
uint32_t pkt_len = (uint32_t) len;

if (write(state->pipe_fds[1], &pkt_len, sizeof(pkt_len)) !=

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The length prefix and the payload are two separate writes, and only pipe_fds[0] is set non-blocking, so the write side blocks: once the guest stops posting RX buffers the pipe fills and this callback stalls the serial vmnet queue while holding state->lock. If the length write lands and the payload write is short or fails, the code only logs and returns, leaving the stream permanently desynchronized so net_vmnet_read consumes payload bytes as the next length. Use a non-blocking SOCK_DGRAM socketpair so each frame is one atomic message and a full buffer drops the packet instead of blocking.

mode_name, state->mac[0], state->mac[1], state->mac[2],
state->mac[3], state->mac[4], state->mac[5]);

vmnet_register_packet_callback(state, iface);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

iface is read here inside the completion block, but it is only assigned when vmnet_start_interface returns on the calling thread. The block runs on state->queue with no ordering against that assignment, so the packet callback can be registered on a NULL interface and no packet ever arrives. Register it after dispatch_semaphore_wait returns and state->iface has been set.

dispatch_semaphore_signal((dispatch_semaphore_t) state->sem);
});

dispatch_semaphore_wait((dispatch_semaphore_t) state->sem,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

vmnet_start_interface returns NULL when it fails outright, and in that case the completion handler that signals state->sem never runs, so this wait blocks forever and rv32emu hangs at startup with no diagnostic. Check the return value before waiting, and give the wait a finite deadline so a framework that accepts the request but never calls back still unwinds.

state->running = false;

if (state->iface) {
vmnet_stop_interface(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

vmnet_stop_interface only schedules its completion handler on state->queue, yet the code below releases that queue, closes pipe_fds, and destroys state->lock right away, and netdev_delete then frees state. A packet callback already queued or still running will touch a destroyed mutex, a closed fd, and freed memory on every shutdown and cold reboot. Clear the event callback with vmnet_interface_set_event_callback(iface, VMNET_INTERFACE_PACKETS_AVAILABLE, NULL, NULL), wait for the stop completion, then drain the queue before releasing anything.

Comment thread src/devices/slirp.c
{
net_user_options_t *usr = (net_user_options_t *) opaque;

if (!usr || usr->guest_to_host_channel[SLIRP_WRITE_SIDE] < 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The guard tests guest_to_host_channel[SLIRP_WRITE_SIDE], but the write below uses host_to_guest_channel[SLIRP_WRITE_SIDE]. The descriptor actually written is never validated; the check only holds today because net_slirp_init creates and net_slirp_cleanup closes both pairs together.

Suggested change
if (!usr || usr->guest_to_host_channel[SLIRP_WRITE_SIDE] < 0)
if (!usr || usr->host_to_guest_channel[SLIRP_WRITE_SIDE] < 0)

Comment thread src/devices/virtio-net.c
if (!vnet_check_word_range(vnet, desc_addr, 4))
return false;

const struct virtq_desc *desc =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

vnet_preprocess only rejects addresses with the low two bits set, so queue_desc is guaranteed 4-byte alignment while struct virtq_desc needs 8 for its uint64_t addr. A guest that programs QueueDescLow to an address that is 4-byte but not 8-byte aligned makes every field access through this pointer undefined, which faults on strict-alignment hosts. virtio_blk_handle_request copies the entry with memcpy instead of casting for exactly this reason, and carries a comment saying so; do the same here.

@jserv
jserv requested a review from ChinYikMing September 13, 2026 13:53

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rebase latest master branch and resolve conflicts.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants