Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
status: open
kind: finding
opened: 2026-09-08
---

# `getsockname` answers an address nobody asked netd for

`userland/libc/src/socket.rs`'s `getsockname()` fills the caller's `sockaddr`
with `[10, 0, 2, 15]` and the socket's local port. The address is a literal in
that function; netd is not asked, and the SDK has nothing to ask it with —
`toyos::net` carries `tcp_connect`, `tcp_bind`, `tcp_accept`, the UDP calls and
`dns_lookup`, and no call that answers "what is this machine's address".

It was true by coincidence until now: netd carried the same literal, so the
shim and the stack agreed. netd takes its address from DHCP as of the change
that filed this, so the two agree only on a machine whose server happens to
lease `10.0.2.15` — QEMU's user-mode backend does, and the bench's router does
not. Every C program that asks what address it is bound to is told the wrong
one there.

What it costs to fix is a message type on netd's protocol and one plumbed
answer; what it costs to leave is that the one caller of `getsockname` in a
POSIX program is the one that then advertises an address nothing can reach.
Nothing in the tree reads it today, which is why this is a finding and not a
defect.
41 changes: 24 additions & 17 deletions issues/hardware/the-t14-answers-only-through-a-usb-stick.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ is on a cable on the same LAN as the development Mac and its NIC is the onboard
Intel I219 at `00:1f.6`, `8086:15fc`, which the kernel enumerates and nothing
claims. The track is to make that cable the answer path.

The substrate a process needs to drive a PCI function itself is built
(`kernel/src/pcidev/mod.rs`, `userland/netd/src/virtio_net.rs`). What is left is
the I219 driver in netd, with DHCP under the hostname `toyos-t14` and a first
ping and ssh from the Mac; a record stream from logd to a listener in the
harness, so a boot's log arrives while it is booting; command execution, file
transfer both ways and key auth in sshd, with the harness running userland tests
over ssh through a russh client; and a netboot spike in which the firmware
fetches the loader over HTTP so the stick leaves the boot path.
Built and green under QEMU: the substrate (`kernel/src/pcidev/mod.rs`), the
I219 driver (`toyos-i219/`, `userland/netd/src/i219.rs`), netd's address from
DHCP (`userland/netd/src/dhcp.rs`), the record stream (`toyos-logstream/`,
`userland/logd/src/stream.rs`) and sshd's exec, transfer and key auth. What is
left is the laptop — the claim on its own card (`tests/lancase`), the stream and
the ssh from the Mac over the cable (`tests/ssh-client-host`), and a netboot
spike that takes the stick out of the boot path — and all of it waits on
`issues/kernel/a-32-bit-bar-needs-the-host-bridges-aperture.md`.

Constraints a reader would otherwise pay to re-derive:

Expand All @@ -34,12 +34,20 @@ Constraints a reader would otherwise pay to re-derive:
before suspecting the driver.
- **ssh is the bench's transport and a real feature**: sshd is built on russh
and the harness's client is russh too. No host ssh binary, no fork.
- **Addressing is DHCP with a hostname**, resolved through the router's DNS. The
T14's MAC is the same under ToyOS and Ubuntu, so the lease is the one `t14`
already resolves to. Wi-Fi is out — the AX210 needs a firmware image.
- The I219 has **32-bit BARs**, and `pcidev`'s window allocator has only ever
placed a 64-bit one: `Refusal::NoWindow` on that machine means nothing was
found above everything firmware described and below the platform's fixed MMIO.
- **Addressing is DHCP with a hostname**, and netd sends `toyos-t14` as the
host-name option — but **the name resolves to nothing on this LAN**, measured:
the T14's DHCP-served resolvers are the ISP's, and on the development Mac
`t14` resolves to the Tailscale address `100.92.92.12`, which only Ubuntu ever
holds. The address is read off the claimed PCI function instead
(`Driver::wire`): `enp0s31f6` at `192.168.1.46/24`, the Mac on `192.168.1.47`.
Wi-Fi is out — the AX210 needs a firmware image.
- **The I219 is an MSI part**, measured: `/proc/interrupts` names its interrupt
`IR-PCI-MSI-0000:00:1f.6` and `msi_irqs/162` reads `mode=msi`.
- The I219 has a **32-bit BAR** (`bar0=0xbcf00000`): `pcidev`'s window allocator
places a BAR above everything firmware described, and below 4 GiB there is no
above — the platform's fixed MMIO is at `0xFEC00000`. Leaving the BAR where it
sits is not a way out either: the internal NVMe's `0xbce00000` is in the same
2 MiB page, which is the only page size this kernel maps.
- **QEMU's `virtio-net-pci-non-transitional` on `q35` advertises no PCIe
function-level reset** — measured, not assumed: `pcidev`'s refusal on that
ground reddened every netd registration at once. So a re-claim is made safe by
Expand All @@ -49,9 +57,8 @@ Constraints a reader would otherwise pay to re-derive:
one; the I219 does, so on the T14 both hold.
- **The record stream is `logstream=<a.b.c.d>:<port>` on the parameter line**,
copied by the kernel into `/system/bin/init`'s environment and read from there
by `logd` (`toyos-logstream`'s `PARAM` and `ENV`). What is left to build is the
metal half: arming the flashed image with the Mac's address and listening while
the T14 boots. A boot that dies before `logd` runs still needs the stick.
by `logd` (`toyos-logstream`'s `PARAM` and `ENV`). A boot that dies before
`logd` runs still needs the stick.
- **A stalled peer's backpressure reaches `logd`'s queue only after megabytes.**
Between them stand a 2 MiB kernel pipe (`kernel/src/pipe.rs`'s `PIPE_SIZE`) and
netd's 64 KiB send buffer, and a `log-storm` at `--smp 8` produces 4,213 lines
Expand Down
64 changes: 64 additions & 0 deletions issues/kernel/a-32-bit-bar-needs-the-host-bridges-aperture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
status: open
kind: defect
opened: 2026-09-08
---

# A 32-bit BAR cannot be handed to a process, because nothing here reads the host bridge's aperture

`kernel/src/pcidev`'s window allocator places a claimed function's BARs on
2 MiB pages **above everything firmware described**. In 64 bits that is always
possible. Below 4 GiB it never is: the platform's fixed MMIO sits at
`0xFEC00000` and the UEFI map reaches it, so `window(narrow_end, PLATFORM_MMIO)`
answers `0x0..0x0` on every machine. Read off the ThinkPad T14, run 29:

```
pcidev: 24 functions; a 32-bit window comes from 0x0..0x0, a 64-bit one from 0x603dc00000..0x6040c00000
pcidev: PCI 00:1f.6 NOT HANDED OVER — this machine has no 2 MiB-aligned address space above what firmware assigned to put a BAR in
```

That function is the bench's own NIC, and its BAR is 32-bit
(`bar0=0xbcf00000`). So the cable this project's test bench answers on cannot be
driven by a process at all until this is built.

**Leaving the BAR where firmware put it is not the fix.** The kernel maps 2 MiB
pages, and on this machine the I219's BAR shares its page with the internal
NVMe's (`0xbcf00000` and `0xbce00000` are both inside `0xbce00000..0xbd000000`):
handing it over unmoved would put a disk controller's registers inside a
network daemon's mapping. `alone_in_its_page` is the assertion that says so, and
the refusal is right.

**What is missing is a free run, and what is missing to find one is ACPI.** A
32-bit window is a run *between* things rather than a span above them, and three
of the four things it has to miss are readable already — the firmware map, the
BARs this bus assigned, and every range a PCI-to-PCI bridge forwards to a
secondary bus (`toyos_pci::bridge`, and `survey_low_space` prints all three on
the machine where the window comes out empty). The fourth is the host bridge's
own aperture: which addresses below 4 GiB the root complex decodes and forwards
to PCI at all. That is the `_CRS` of the `PNP0A08` device, an AML method, and
this kernel runs no AML. An address outside the aperture is not free space — it
is unrouted, and a read of it answers ones, which `Refusal::Dead` cannot tell
from a device that is simply not there.

Linux's own answer is the same one: `acpi_pci_probe_root_resources` reads
`_CRS`, and the per-chipset fallbacks it keeps are quirks for firmware that gets
`_CRS` wrong, not an alternative to it. Reading a host bridge register such as
Intel's `TOLUD` instead would be chipset-specific and is not a road this project
takes.

So the work is one of:

- an AML interpreter far enough to evaluate `_CRS` on the host bridge, which is
a large thing to want for one method; or
- a 4 KiB mapping for a claimed BAR, which removes the need to move a BAR whose
page is shared and is a change in `mm` rather than here; or
- the owner ruling that some other source of the aperture is admissible.

The survey is committed and prints the candidates; nothing hands one out. The
line it ends with says why:

```
pcidev: a run above is a candidate and not a claim — what says whether an address below 4 GiB
reaches this bus at all is the host bridge's own aperture, which is ACPI's `_CRS`, and this
kernel runs no AML
```
39 changes: 37 additions & 2 deletions kernel/src/drivers/pci.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use alloc::vec::Vec;

use toyos_pci::{bar, caps, msi, msix};
use toyos_pci::{bar, bridge, caps, msi, msix};

use crate::mm::Mmio;
use crate::mm::paging::MmioPolicy;
Expand Down Expand Up @@ -326,7 +326,7 @@ impl PciDevice {
}
cap.write_u16(msi.data(), data as u16);
if let Some(mask) = msi.mask() {
cap.write_u32(mask, 0);
cap.write_u32(mask, msi::UNMASKED);
}
cap.write_u16(msi::MESSAGE_CONTROL, msi::Msi::enabled(control));
self.report_message(
Expand All @@ -337,6 +337,41 @@ impl PciDevice {
true
}

/// Put MSI back off: the counterpart of [`Self::disable_msix`], and what a
/// claimed MSI function has in place of masking a table entry.
///
/// The mask first where the function implements one, because that is the
/// per-vector lever and the one MSI-X's own hand-back uses; the enable bit
/// after, because every function has that one. A function still delivering
/// once its holder is gone writes its message into a slot with no reader.
pub fn disable_msi(&self) {
let Some(cap) = self.capabilities().find(|c| c.id() == msi::CAP_ID) else { return };
let control = cap.read_u16(msi::MESSAGE_CONTROL);
if let Some(mask) = msi::Msi::decode(control).mask() {
cap.write_u32(mask, msi::MASKED);
}
cap.write_u16(msi::MESSAGE_CONTROL, msi::Msi::disabled(control));
}

/// Every memory range this function forwards to its secondary bus, below
/// 4 GiB. Empty on a function that is not a bridge.
///
/// **Read, never probed**: these are the ranges nothing above this bridge
/// may hand out, and reading them costs the machine nothing — unlike
/// `bar_size`, which takes memory decode off for the length of its probe.
pub fn forwarded_below_4g(&self) -> Vec<bridge::Window> {
if self.read_config_u8(HEADER_TYPE) & !MULTI_FUNCTION != bridge::HEADER_TYPE_BRIDGE {
return Vec::new();
}
let mut out = Vec::new();
out.extend(bridge::window(self.read_config_u32(bridge::MEMORY_BASE)));
out.extend(bridge::prefetch_below_4g(
self.read_config_u32(bridge::PREFETCH_BASE),
self.read_config_u32(bridge::PREFETCH_BASE_UPPER),
));
out
}

pub fn capabilities(&self) -> CapabilityIter<'_> {
let first = self.mmio.read_u8(CAPABILITIES_PTR);
CapabilityIter { device: self, walk: caps::CapWalk::new(), next: first }
Expand Down
Loading
Loading