net: recvfrom says who sent it, and a datagram is delivered whole

The second of the two things the stack owed. recvfrom accepted the
address arguments and ignored them, so every caller was told nothing
about where a datagram came from -- which for UDP is most of the point:
a server that cannot see the sender cannot reply to it.

Fixing it properly meant fixing something worse that was sitting
underneath. UDP payloads went into the same byte ring TCP uses, with no
record of where one datagram ended and the next began, so two datagrams
that arrived together came back from one read as one. That is a bug
whose appearance depends on timing and whose blame lands on the sender.

Datagrams now carry a header alongside the bytes: source address, source
port, length. Sixteen of them per socket, in a ring beside the existing
one. Whole buffers would have been 16 sockets x 16 datagrams x an MTU of
static memory for something almost always empty; only the boundaries need
keeping, and those are eight bytes each. A datagram too big for the
buffer it is read into is truncated and the remainder discarded, rather
than left in the ring to be delivered as the head of the next one. One
that arrives with the queue full is dropped whole -- that is UDP's own
contract, and half a datagram in the ring would corrupt every one after.

An empty queue and a zero-length datagram are kept apart, because a
sender really can send zero bytes and the right answer to that is 0
rather than "nothing has arrived yet".

sendto grew the mirror image: a destination address on a datagram
socket, which is what a server replying to whoever just wrote to it
needs. On TCP a destination is ignored rather than refused -- the
connection decides where the bytes go, and programs pass one out of
habit. For a stream, recvfrom's source is read from the pcb: it is the
peer, and it is the same answer for every byte.

Verified: bootcheck -Net now covers UDP too, 45 [ok] and no failures.
The address check is real rather than cosmetic -- nettest deliberately
leaves its sockaddr uninitialised, so a kernel that filled in nothing
would print stack rubbish instead of 10.0.2.2 and the port the responder
is actually on. The boundaries are checked by byte count: the responder
sends 20 bytes and then 16, and the guest reports reading 20 and then
16. Treated as a stream the first read would have taken all 36.

Left for later, and deliberately: accept() still does not report the
peer's address, and getsockname/getpeername do not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-15 16:23:06 +02:00
commit a9ff1166b5
6 changed files with 433 additions and 24 deletions

View file

@ -248,11 +248,17 @@ if ($Net) {
$l = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
$l.Start(); $script:netPort = $l.LocalEndpoint.Port; $l.Stop()
# A second port for UDP. Bound the same way, and separately: a free TCP
# port says nothing about the UDP port of the same number.
$u = [System.Net.Sockets.UdpClient]::new(0)
$script:netUdpPort = $u.Client.LocalEndPoint.Port; $u.Close()
# The interface first, so a failure below reads as "the address never
# arrived" rather than "TCP is broken" -- two very different bugs that look
# identical from a failed connect.
$commands += 'cat /proc/netif'
$commands += "nettest 10.0.2.2 $netPort"
$commands += "nettest -u 10.0.2.2 $netUdpPort"
}
if ($Ps) {
$commands += 'ls /proc'
@ -370,6 +376,34 @@ $netJob = if ($Net) {
}
} else { $null }
# The UDP responder. A separate job because it is a separate exchange, and
# because a datagram socket has nothing to accept: it waits for one datagram
# and answers the address it came from, which is precisely what the guest is
# being asked to report back.
$netUdpJob = if ($Net) {
Start-Job -ArgumentList $netUdpPort -ScriptBlock {
param($port)
$u = [System.Net.Sockets.UdpClient]::new($port)
$u.Client.ReceiveTimeout = 900000
$ep = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0)
$got = ''
try {
$data = $u.Receive([ref]$ep)
$got = [Text.Encoding]::ASCII.GetString($data)
$reply = [Text.Encoding]::ASCII.GetBytes("HELLO-UDP-FROM-HOST`n")
$u.Send($reply, $reply.Length, $ep) | Out-Null
# A second datagram, so the guest can show that it reads them one
# at a time. Two sends rather than one long one: the boundary is
# the property being tested, and it only exists if it was put there
# by the sender.
$second = [Text.Encoding]::ASCII.GetBytes("SECOND-DATAGRAM`n")
$u.Send($second, $second.Length, $ep) | Out-Null
} catch { }
$u.Close()
return $got
}
} else { $null }
$psi.Arguments = ($drives + $usbDevices + $netDevices + $monitor + @(
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
'-display none'
@ -571,11 +605,17 @@ try {
# is still sitting in accept, and the second case is a failure to report rather
# than one to wait out.
$netFromGuest = ''
$netUdpFromGuest = ''
if ($Net -and $netJob) {
Wait-Job $netJob -Timeout 20 | Out-Null
$netFromGuest = (Receive-Job $netJob) -join ''
Remove-Job $netJob -Force -ErrorAction SilentlyContinue
}
if ($Net -and $netUdpJob) {
Wait-Job $netUdpJob -Timeout 20 | Out-Null
$netUdpFromGuest = (Receive-Job $netUdpJob) -join ''
Remove-Job $netUdpJob -Force -ErrorAction SilentlyContinue
}
if (-not (Test-Path $log)) { throw "no serial log produced at $log" }
$text = Get-Content $log -Raw
@ -760,6 +800,23 @@ if ($Net) {
ExpectThat ($netFromGuest -match 'HELLO-FROM-SCHOS') `
'the listener on this host received what the guest sent' `
"the listener read '$($netFromGuest -replace '\s+', ' ')'"
# UDP, and specifically the source address. recvfrom filling that in is the
# thing being checked: over a stream the source is known before any byte
# arrives, so a kernel writing nothing at all would still look right there.
Expect 'nettest: udp OK' 'a UDP datagram went out and one came back'
Expect 'HELLO-UDP-FROM-HOST' 'the datagram carried the host''s bytes'
Expect "nettest: udp source 10\.0\.2\.2:$netUdpPort" `
'recvfrom reported the real sender, address and port'
ExpectThat ($netUdpFromGuest -match 'HELLO-UDP-FROM-SCHOS') `
'the responder on this host received the guest''s datagram' `
"the responder read '$($netUdpFromGuest -replace '\s+', ' ')'"
# Message boundaries. The responder sends two datagrams of 20 and 16 bytes;
# a kernel that treated UDP as a byte stream would hand the first read all
# 36 at once, which is why the counts are asserted and not just the text.
Expect 'nettest: udp received 20 bytes' 'the first read returned exactly one datagram'
Expect 'nettest: udp second 16 bytes' 'and the second read returned the next one, whole'
Expect 'SECOND-DATAGRAM' 'the second datagram arrived intact'
}
if ($Ps) {