Skip to content
Merged
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,52 @@
---
status: open
kind: defect
opened: 2026-10-12
---

# A BAR behind a bridge with no free 2 MiB of its windows is not handed over

A claim moves each BAR it maps onto a 2 MiB window of its own
(`place_bar`, `kernel/src/pcidev/mod.rs`), because a 2 MiB page is the only
one this kernel maps into a process. Behind a bridge the window is cut only
where every bridge on the function's path forwards
(`placement::behind_bridges`, `kernel/pci/src/placement.rs`); where none of
that is free, the BAR stays where firmware put it and is handed to nobody,
named `NoRoutedRun`, and a function with no other mappable BAR is refused.
A bridge firmware sized to its BARs leaves no such 2 MiB. Where the BAR is
also the one holding the function's MSI-X table, a claim withholds it and
the function is refused `NoMappableBar` before placement; a claim that
handed the table's BAR over, as #921 does, would meet `NoRoutedRun`:

- **The AMD bench**: the AX200 at 01:00.0 has BAR 0 at 0xd0600000, behind
bridge 00:02.2, which forwards 0xd0600000..0xd0700000 (#884's E13 §1,
https://github.com/ToyOSOrg/evidence/blob/2eb315326d1b4d0363b4c918da8dd2bfaabd1234/e13-amd-ax200-bar.log);
the xHCI at 03:00.4 is behind 00:08.1, whose 0xd0000000..0xd0500000
bus 3 fills. Its one BAR holds its MSI-X table, which a claim withholds,
so it is refused `NoMappableBar` before placement; a claim that handed
the table's BAR over, as #921 does, would meet `NoRoutedRun` there.
- **The T14**, as S1 read it (lines 111 to 116,
https://github.com/ToyOSOrg/evidence/blob/cd368b25b2259a76b80dc31f4219dac1c91d84dc/s1/wifi-s1-a-kernel.log):
the AX210 at 09:00.0 has BAR 0 at 0xbc300000, 16 KiB, its MSI-X table
in it (`issues/there-is-no-wifi.md`, S1's read-out), behind root port
00:1c.4, which forwards 0xbc300000..0xbc400000.

Two placement issues rest on placement as it is, and P1 takes them up with
this one:

- `issues/nothing-reds-when-the-bar-survey-drops-an-assigned-bar.md` asks for
a guest that reds when the forwarded-range push is deleted.
- `issues/no-boot-reaches-the-retry-that-undoes-a-placement-nothing-answered.md`
wants a function behind a `pcie-root-port` to settle at a later candidate;
`bar_map_behind_a_bridge` now settles one at the first.

Owner: P1 of `issues/there-is-no-wifi.md`.

**Exit**:
- a BAR inside every window on its path to the root is mapped in place, in
4 KiB pages;
- a BAR that shares a 4 KiB page with another function's is moved within
those windows, never out of them, or is refused by name;
- the T14's AX210 is claimed with its BAR in place;
- each of the two placement issues above closed, or its exit rewritten to the
placement P1 lands.

This file was deleted.

2 changes: 1 addition & 1 deletion issues/there-is-no-wifi.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ page included.** Owner: the orchestrator.
- `msix-exclusive-bar=on` gone from `tests/common/qemu.rs` and
`build/qemu.rs`, so the NVMe guests drive QEMU's default controller
through its table in BAR 0;
- `issues/pcidev-moves-a-bar-behind-a-bridge-out-of-the-range-the-bridge-forwards.md`
- `issues/a-bar-behind-a-bridge-with-no-free-2-mib-of-its-windows-is-not-handed-over.md`
closed, and each of the two placement issues it names closed or its exit
rewritten to the placement P1 lands;
- the exit of
Expand Down
127 changes: 127 additions & 0 deletions kernel/pci/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,74 @@ pub const PREFETCH_BASE: u64 = 0x24;
/// removed.
pub const HEADER_TYPE_BRIDGE: u8 = 1;

/// Byte offsets of a Type 1 header's Secondary and Subordinate Bus Number
/// registers (§3.2.5.3, §3.2.5.4): the bus directly below the bridge, and
/// the highest bus number anywhere below it.
pub const SECONDARY_BUS: u64 = 0x19;
pub const SUBORDINATE_BUS: u64 = 0x1A;

/// One bridge, as the path to a function behind it reads it: the bus it is
/// on, and its bus numbers and windows as its registers answer, which a
/// bridge firmware never configured (both bus numbers reset to 0) or a
/// hostile switch may answer with anything.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Bridge {
pub bus: u8,
pub secondary: u8,
pub subordinate: u8,
pub memory: Option<Window>,
pub prefetchable: Option<Window>,
}

/// Why a function's path to its root bus cannot be named.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathRefused {
/// Two bridges each name this bus as their secondary one, and requests
/// for it reach the function through at most one of them.
Ambiguous { bus: u8 },
}

/// The bridges from a function's bus up to its root bus, nearest first; empty
/// for a function on a root bus.
///
/// **A step is a bridge whose secondary bus is the one reached, which sits on
/// a lower bus and whose subordinate range holds the function's** (§3.2.5.3,
/// §3.2.5.4): anything else is not a bridge requests for that bus pass, and
/// is passed over, so an unconfigured or lying bridge moves no function off
/// its root bus. Each step lowers the bus, so the walk ends; two bridges that
/// could each be the step refuse the walk.
pub fn path(bridges: &[Bridge], bus: u8) -> impl Iterator<Item = Result<&Bridge, PathRefused>> {
let mut at = Some(bus);
core::iter::from_fn(move || {
let reached = at?;
let mut parents = bridges.iter().filter(|b| {
b.secondary == reached && b.bus < b.secondary && b.secondary <= bus && bus <= b.subordinate
});
let step = match (parents.next(), parents.next()) {
(None, _) => None,
(Some(parent), None) => Some(Ok(parent)),
(Some(_), Some(_)) => Some(Err(PathRefused::Ambiguous { bus: reached })),
};
at = match step {
Some(Ok(parent)) => Some(parent.bus),
_ => None,
};
step
})
}

impl Bridge {
/// Whether this bridge forwards all of `at..end` to its secondary side
/// through a window a BAR of this kind may sit in (§3.2.5.6 to
/// §3.2.5.8). The prefetchable window may be read ahead and its writes
/// merged, so only a prefetchable BAR belongs there; the memory window
/// takes either kind.
pub fn forwards(&self, at: u64, end: u64, prefetchable: bool) -> bool {
let holds = |window: Option<Window>| window.is_some_and(|w| w.start <= at && end <= w.end);
holds(self.memory) || (prefetchable && holds(self.prefetchable))
}
}

/// The granularity both windows are expressed in: bits 31:20.
const GRANULE: u64 = 1 << 20;

Expand Down Expand Up @@ -161,4 +229,63 @@ mod tests {
);
assert_eq!(prefetch(0x0000_0010, 0, 0), None);
}

fn port(bus: u8, secondary: u8, subordinate: u8) -> Bridge {
Bridge { bus, secondary, subordinate, memory: None, prefetchable: None }
}

fn walked(bridges: &[Bridge], bus: u8) -> std::vec::Vec<Result<Bridge, PathRefused>> {
path(bridges, bus).map(|step| step.copied()).collect()
}

/// The T14's shape: every claimed function on bus 0, and root ports with
/// buses below them. A port firmware never configured reads both bus
/// numbers 0, and a hostile one may claim every bus; neither moves a
/// bus-0 function off its root bus.
#[test]
fn a_function_on_the_root_bus_has_no_path_whatever_a_bridge_says() {
let real = port(0, 9, 9);
assert_eq!(walked(&[real], 0), []);
assert_eq!(walked(&[real, port(0, 0, 0)], 0), []);
assert_eq!(walked(&[real, port(0, 0, 0xff)], 0), []);
assert_eq!(walked(&[real, port(0, 0, 0xff)], 9), [Ok(real)]);
}

/// A walk climbs one bus per step to the root bus, each step a bridge on
/// a lower bus whose subordinate range holds the function's bus; a bridge
/// that claims a bus from above it or a range not holding the function is
/// no step.
#[test]
fn a_path_climbs_from_the_functions_bus_to_the_root() {
let upstream = port(0, 2, 4);
let downstream = port(2, 3, 3);
assert_eq!(walked(&[upstream, downstream], 3), [Ok(downstream), Ok(upstream)]);
assert_eq!(walked(&[upstream, port(5, 3, 3)], 3), []);
assert_eq!(walked(&[upstream, port(2, 3, 2)], 3), []);
}

/// Two bridges naming one secondary bus refuse the walk for a function on
/// it, and for no function elsewhere.
#[test]
fn a_bus_two_bridges_claim_refuses_its_functions_alone() {
let one = port(0, 3, 3);
let other = port(1, 3, 3);
let elsewhere = port(0, 5, 5);
assert_eq!(walked(&[one, other, elsewhere], 3), [Err(PathRefused::Ambiguous { bus: 3 })]);
assert_eq!(walked(&[one, other, elsewhere], 5), [Ok(elsewhere)]);
}

/// The memory window forwards either kind of BAR; the prefetchable window
/// forwards a prefetchable BAR alone, and only an extent wholly inside.
#[test]
fn forwarding_holds_the_bars_kind_and_its_whole_extent() {
let memory = Window { start: 0xd000_0000, end: 0xd040_0000 };
let prefetch = Window { start: 0xe000_0000, end: 0xe040_0000 };
let b = Bridge { memory: Some(memory), prefetchable: Some(prefetch), ..port(0, 1, 1) };
assert!(b.forwards(0xd000_0000, 0xd020_0000, false));
assert!(b.forwards(0xd000_0000, 0xd020_0000, true));
assert!(b.forwards(0xe000_0000, 0xe020_0000, true));
assert!(!b.forwards(0xe000_0000, 0xe020_0000, false));
assert!(!b.forwards(0xd030_0000, 0xd050_0000, true));
}
}
Loading