Skip to content

Make networking work in SO3 under QEMU virt - #322

Merged
daniel-rossier merged 8 commits into
mainfrom
feat/so3-networking-qemu-virt
Aug 23, 2026
Merged

Make networking work in SO3 under QEMU virt#322
daniel-rossier merged 8 commits into
mainfrom
feat/so3-networking-qemu-virt

Conversation

@daniel-rossier

@daniel-rossier daniel-rossier commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

SO3 networking has never worked on QEMU: CONFIG_NET is off in every defconfig,
and for good reason — QEMU's virt machine offers only virtio-net, which SO3 has
no driver for. Its single Ethernet driver is smc911x, so the machine has to
provide an SMSC LAN9118.

QEMU

An SMSC LAN9118 is added to the machine model next to the PL111/PL050/absmouse
devices the so3 patch already contributes, at 0x08804000 (VIRT_ETH) with
SPI 15. Two things the obvious version gets wrong:

  • 0x1a000000 does not work. That is the address the SO3 device trees
    inherited from vexpress-a15, and it falls inside VIRT_PCIE_MMIO, whose gpex
    alias covers the whole window and shadows anything mapped underneath it
    (info mtree -f shows 10000000-2fffffff as one gpex_mmio_window extent).
    The guest reads back nothing and never detects the chip. Hence an address in
    the block the so3 patch already owns.
  • Ordering matters. The device must be created before create_pcie(),
    which hands every nd_table entry no on-board device has claimed to
    pci_nic_init_nofail() — that one exits with Unsupported NIC model: lan9118. Slots an on-board NIC has taken are now skipped in that loop.

The MAC is only instantiated when the command line fills the legacy on-board NIC
slot, so a run without -nic/-net leaves the machine exactly as it was.
st.sh passes -nic user,model=lan9118 on its own slirp stack, alongside the
virtio-net device the Linux agency uses (which keeps the 2222 → 22 host
forward). Each guest ignores the NIC it cannot drive.

Adding a third patch to the QEMU set also exposed a .gitignore trap: the
qemu entry meant for the source tree build.sh downloads is unanchored, so it
matches the directories of that name inside the layers
(meta-qemu/recipes-qemu/qemu/) as well. The two existing patches survive only
because they were already tracked; the new one was silently swallowed by git add. The entry is now anchored as /qemu.

SO3

  • device tree nodes moved to the new address/IRQ (the stale xvisor/vexpress
    reference links go with them);
  • CONFIG_SMC911X was unreachable on 64-bit (depends on VIRT32);
  • CONFIG_NET + CONFIG_SMC911X on in the four QEMU virt defconfigs
    (virt32/virt64, plain and _fb) — rpi4 and verdin stay off, they have no
    driver for their own MAC;
  • the driver no longer built: DBG() used to come from soo/debug.h, which is
    only on the include path under CONFIG_SOO. It gets a local definition (the
    call sites also passed a status argument the format string had no conversion
    for);
  • recvfrom() dropped the peer address lwIP had filled in — every reply looked
    like it came from 0.0.0.0. accept() copied it back with a memcpy of the
    lwIP layout, whose leading sa_len byte shifts sin_family. Both now go
    through the reverse of user_to_lwip_sockadd().

Testing

virt64 (virt64_fb_defconfig) and virt32 (virt32_fb_defconfig), each with
a full build.sh bsp-so3 + deploy.sh bsp-so3 and booted with st.sh — 0%
loss on both, and four consecutive runs in one session on each:

Network Interface Controller (NIC) found LAN9118
smc911x: detected LAN9118 controller
IP Network up and running with address 10.0.2.15
/ % ping -c 10 10.0.2.2
64 bytes from 10.0.2.2: icmp_seq=1 ttl=255 time=6.497070 ms
64 bytes from 10.0.2.2: icmp_seq=2 ttl=255 time=0.601074 ms
...
10 packets transmitted, 10 received, 0.000000% packet loss
rtt min/avg/max = 0.557861/1.227100/6.415039 ms

The QEMU patch set was regenerated with updiff.sh qemu and validated by a
clean build.sh -c qemu (fetch → patch → build), after which the working tree
matches the hand-edited sources byte for byte. scripts/check-format.sh is
clean.

The lost second packet

The first version of this branch lost exactly one packet per ping run — the
reply to the second echo request — and reported implausible sub-0.2 ms
round-trip times for every reply after it. Root cause: the driver programmed an
RX Status Level of 1 into FIFO_INT. The controller raises RSFL when the RX
status FIFO holds more entries than that level, so the interrupt only came
once a second frame had arrived, and reception was permanently one frame
behind:

  • the ARP reply sat in the FIFO until echo reply 1 arrived and released both
    (hence the 300 ms first round-trip);
  • echo reply 2 then sat there alone with nothing to trigger the interrupt, and
    recvfrom() timed out;
  • echo request 3's reply released it, and from then on every recvfrom()
    returned the previous reply, already queued — hence the near-zero timings.

Level 0 (the chip's reset value) makes a single waiting frame raise RSFL.

Socket lifetime, and what ping reports

Testing the error paths turned up two more things.

The third ping run of a session lost replies for good. The VFS calls
fops->read/write/close with the global descriptor, while the socket
syscalls get the process-local one — read_sock(), write_sock() and
close_sock() ran the local→global translation anyway. For close_sock() that
landed on -1 (sys_close() releases fd_array[fd] before calling the
operation), indexing lwip_fds[] out of bounds, so lwip_close() never reached
the socket. Every run leaked its raw pcb and the packets queued on it, and with
MEMP_NUM_NETBUF at 4 the third run started timing out. The fops now have their
own accessor, close_sock() clears the slot, and lwip_fds[] is initialised to
-1 (0 being a valid lwIP socket id, a zeroed table made every descriptor look
like socket 0). Six consecutive ping -c 3 runs: 0% loss each.

ping misreported failures. A failed recvfrom() was only reported from the
second request onwards, so the first timeout of a run fell through to the reply
parsing and printed Truncated reply of -1 bytes; and ICMP errors came out as
raw numbers. Now:

/ % ping -c 3 10.0.0.1
Packet receive failed!!            (x3, 100% loss)
/ % ping -c 3 192.168.1.1
From 192.168.1.1 icmp_seq=1 Destination Port Unreachable
/ % ping -c 4 10.0.2.2
4 packets transmitted, 4 received, 0.000000% packet loss

That port-unreachable is host-side, not SO3: net.ipv4.ping_group_range
defaults to the empty range 1 0 on several distributions, so slirp cannot open
an ICMP socket and emulates the echo over UDP. Documented in the lwIP chapter.

QEMU's 'virt' machine only offers virtio-net, and SO3 has no driver for
it: its single Ethernet driver is smc911x. So networking under QEMU has
never worked, which is why CONFIG_NET was off in every defconfig.

Add an SMSC LAN9118 to the machine model, next to the PL111/PL050/
absmouse devices the so3 patch already adds, at 0x08804000 (VIRT_ETH)
with SPI 15.

Two things the obvious version gets wrong:

  - 0x1a000000, the address the SO3 device trees inherited from
    vexpress-a15, does not work here. It is inside VIRT_PCIE_MMIO, and
    the gpex alias covers the whole window and shadows anything mapped
    underneath it (visible in "info mtree -f"): the guest reads back
    nothing and never detects the chip. Hence an address in the block
    the so3 patch already owns.

  - the device has to be created BEFORE create_pcie(), which hands every
    nd_table entry no on-board device has claimed to
    pci_nic_init_nofail(), and that one exits with "Unsupported NIC
    model: lan9118". Slots an on-board NIC has taken are now skipped
    there too.

It is only instantiated when the command line fills the legacy on-board
NIC slot, so a run without -nic/-net leaves the machine exactly as it
was. st.sh passes "-nic user,model=lan9118" on its own slirp stack,
alongside the virtio-net device the Linux agency uses (which keeps the
2222 -> 22 host forward). Each guest ignores the NIC it cannot drive.

The new Kconfig patch of the set exposed a .gitignore trap: the "qemu"
entry meant for the source tree build.sh downloads is unanchored, so it
also matches the directories of that name inside the layers
(meta-qemu/recipes-qemu/qemu/). The two existing patches survive because
they were already tracked, but the third was silently swallowed by
"git add". Anchor the entry as "/qemu".
Point the ethernet nodes of the QEMU virt device trees at the LAN9118
the so3 QEMU patch now instantiates (0x08804000, SPI 15) instead of the
vexpress-a15 0x1a000000 they carried, which is shadowed by the PCIe MMIO
window. The stale xvisor/vexpress reference links go with them.

CONFIG_SMC911X was unreachable on 64-bit ("depends on VIRT32"), so drop
that restriction, and turn CONFIG_NET + CONFIG_SMC911X on in
virt64_defconfig and virt64_fb_defconfig. virt32 keeps them off: the
device tree is ready but nothing has been tested there.

The driver had not been compiled in a long time and no longer built:
DBG() used to come from soo/debug.h, which is only on the include path
under CONFIG_SOO. Give it a local definition (the call sites also passed
a status argument the format string had no conversion for).
recvfrom() converted the caller's sockaddr into an lwIP one, let
lwip_recvfrom() fill it in, and then dropped it: the source address
never left the kernel, so ping reported every reply as coming from
0.0.0.0.

accept() did copy back, but with a memcpy of the lwIP layout, whose
leading sa_len byte shifts sin_family.

Add the reverse of user_to_lwip_sockadd() and use it in both.
The chapter still said QEMU's virt machine provides no NIC SO3 can
drive and that a different driver would be needed. Replace that with
what the machine now has, where the address and IRQ are defined, why the
MAC is not at the vexpress address, and what a working run looks like —
including the one known rough edge, the second echo request of a ping
run being lost.
Every ping run lost exactly one packet — the reply to the second echo
request — and every reply after that was reported with an implausible
sub-0.2 ms round-trip time.

The driver programmed an RX Status Level of 1 into FIFO_INT. The
controller raises RSFL when the RX status FIFO holds MORE entries than
that level, so the interrupt only came once a SECOND frame had arrived.
Reception was therefore permanently one frame behind: the ARP reply sat
in the FIFO until echo reply 1 arrived and released both, echo reply 2
sat there alone with nothing to trigger the interrupt until echo
request 3 was answered, and from then on every recvfrom() returned the
PREVIOUS reply, already queued, hence the near-zero round-trip times.

Write level 0 (also the chip's reset value) so a single waiting frame
raises RSFL. ping now reports 0% loss and honest timings — 6.5 ms for
the first request, ~0.5 ms afterwards, instead of 300 ms then 0.1 ms.
The VFS calls fops->read/write/close with the GLOBAL file descriptor
(fs/vfs.c), while the socket syscalls get the process-local one.
read_sock(), write_sock() and close_sock() ran the local-to-global
translation anyway, so they looked up an unrelated entry of the process
fd table — and in close_sock()'s case usually -1, since sys_close()
releases fd_array[fd] before calling the operation, which then indexed
lwip_fds[] out of bounds.

lwip_close() therefore never reached the socket. Every ping run leaked
its raw pcb and the packets queued on it, and with MEMP_NUM_NETBUF at 4
the third run in a session started losing replies for good.

Give the fops their own accessor, clear the slot on close so a recycled
gfd cannot be taken for the old socket, and initialise lwip_fds[] to -1:
0 is a valid lwip socket id, so a zeroed table made every descriptor
look like socket 0.
Two things ping got wrong once there was a network to test it on.

A failed recvfrom() was only reported from the second request onwards
("&& msg_count > 1"), so the first timeout of a run fell through to the
reply parsing and came out as "Truncated reply of -1 bytes". Report it
whatever the sequence number.

An ICMP error was printed as raw numbers: pinging an address slirp
cannot reach gave "Error... Packet received with ICMP type 3 code 3".
Decode the types a ping is likely to meet and print the usual form,
"From <ip> icmp_seq=<n> Destination Port Unreachable", falling back to
the numbers for anything unexpected.

The lwIP chapter gains the host-side reason for that particular one:
net.ipv4.ping_group_range is empty by default on several distributions,
so slirp cannot open an ICMP socket and emulates the echo over UDP.
The 32-bit QEMU virt machine gets the same LAN9118 as the 64-bit one —
it is the same machine model — and virt32.dts already points at it, so
only the defconfigs were missing. Turn CONFIG_NET + CONFIG_SMC911X on in
virt32_defconfig and virt32_fb_defconfig.

While here, move CONFIG_SMC911X after the input symbols in
virt64_fb_defconfig: devices/Kconfig sources input/ before net/, so that
is where savedefconfig puts it.
@daniel-rossier
daniel-rossier force-pushed the feat/so3-networking-qemu-virt branch from ef4aa2c to 2a91937 Compare August 23, 2026 13:28
@daniel-rossier
daniel-rossier changed the base branch from fix/usr-ping-reply-parsing to main August 23, 2026 13:29
@daniel-rossier
daniel-rossier merged commit e37b1c2 into main Aug 23, 2026
6 of 7 checks passed
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.

1 participant