Make networking work in SO3 under QEMU virt - #322
Merged
Conversation
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
force-pushed
the
feat/so3-networking-qemu-virt
branch
from
August 23, 2026 13:28
ef4aa2c to
2a91937
Compare
daniel-rossier
changed the base branch from
fix/usr-ping-reply-parsing
to
main
August 23, 2026 13:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SO3 networking has never worked on QEMU:
CONFIG_NETis off in every defconfig,and for good reason — QEMU's
virtmachine offers only virtio-net, which SO3 hasno driver for. Its single Ethernet driver is
smc911x, so the machine has toprovide 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) withSPI 15. Two things the obvious version gets wrong:
inherited from vexpress-a15, and it falls inside
VIRT_PCIE_MMIO, whose gpexalias covers the whole window and shadows anything mapped underneath it
(
info mtree -fshows10000000-2fffffffas onegpex_mmio_windowextent).The guest reads back nothing and never detects the chip. Hence an address in
the block the so3 patch already owns.
create_pcie(),which hands every
nd_tableentry no on-board device has claimed topci_nic_init_nofail()— that one exits withUnsupported 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/-netleaves the machine exactly as it was.st.shpasses-nic user,model=lan9118on its own slirp stack, alongside thevirtio-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
.gitignoretrap: theqemuentry meant for the source treebuild.shdownloads is unanchored, so itmatches the directories of that name inside the layers
(
meta-qemu/recipes-qemu/qemu/) as well. The two existing patches survive onlybecause they were already tracked; the new one was silently swallowed by
git add. The entry is now anchored as/qemu.SO3
reference links go with them);
CONFIG_SMC911Xwas unreachable on 64-bit (depends on VIRT32);CONFIG_NET+CONFIG_SMC911Xon in the four QEMU virt defconfigs(
virt32/virt64, plain and_fb) — rpi4 and verdin stay off, they have nodriver for their own MAC;
DBG()used to come fromsoo/debug.h, which isonly on the include path under
CONFIG_SOO. It gets a local definition (thecall sites also passed a
statusargument the format string had no conversionfor);
recvfrom()dropped the peer address lwIP had filled in — every reply lookedlike it came from
0.0.0.0.accept()copied it back with a memcpy of thelwIP layout, whose leading
sa_lenbyte shiftssin_family. Both now gothrough the reverse of
user_to_lwip_sockadd().Testing
virt64 (
virt64_fb_defconfig) and virt32 (virt32_fb_defconfig), each witha full
build.sh bsp-so3+deploy.sh bsp-so3and booted withst.sh— 0%loss on both, and four consecutive runs in one session on each:
The QEMU patch set was regenerated with
updiff.sh qemuand validated by aclean
build.sh -c qemu(fetch → patch → build), after which the working treematches the hand-edited sources byte for byte.
scripts/check-format.shisclean.
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 RXstatus 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:
(hence the 300 ms first round-trip);
recvfrom()timed out;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/closewith the global descriptor, while the socketsyscalls get the process-local one —
read_sock(),write_sock()andclose_sock()ran the local→global translation anyway. Forclose_sock()thatlanded on -1 (
sys_close()releasesfd_array[fd]before calling theoperation), indexing
lwip_fds[]out of bounds, solwip_close()never reachedthe socket. Every run leaked its raw pcb and the packets queued on it, and with
MEMP_NUM_NETBUFat 4 the third run started timing out. The fops now have theirown accessor,
close_sock()clears the slot, andlwip_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 3runs: 0% loss each.ping misreported failures. A failed
recvfrom()was only reported from thesecond 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 asraw numbers. Now:
That port-unreachable is host-side, not SO3:
net.ipv4.ping_group_rangedefaults to the empty range
1 0on several distributions, so slirp cannot openan ICMP socket and emulates the echo over UDP. Documented in the lwIP chapter.