PCIe · Module 19
Interrupt Routing — From a Correct Notification to the Right CPU
An Endpoint never interrupts a CPU. It emits a notification whose routing information software programmed and the platform interprets — and the boundary between those two ownerships is where wrong-vector bugs are actually found.
Chapters 19.1–19.3 built three ways for a device to say "something happened." All three deliberately stopped at the same place: the notification leaves the Endpoint.
That is not the end of the story, and the gap is bigger than it looks.
An Endpoint with a perfectly correct MSI-X table, the right vector, and a clean transmission has done everything it is capable of doing — and the interrupt can still reach the wrong CPU, the wrong guest, or nothing at all. Between the device and the handler sits a routing system the device does not control and cannot see.
So how does a notification travel from a correct Endpoint to the correct destination, and where exactly does responsibility change hands?
1. The Verified Sources
2. Generating Is Not Delivering
The distinction the whole chapter rests on.
device generates a notification ≠ a CPU takes an interruptEverything Chapters 19.1–19.3 taught happens on the left. The device decides an event occurred, selects a vector, reads a table entry, and transmits.
Everything on the right is somebody else's. Decoding the destination, validating that the device is allowed to target it, and delivering it to an interrupt controller that will eventually preempt a core.
3. The Device Stores What It Cannot Interpret
A short section, because the rule is the mental model.
For MSI and MSI-X, the destination information lives in the device — MSI's Address and Data registers (Chapter 19.2 §4), or an MSI-X table entry (Chapter 19.3 §3).
But software wrote it, and the device cannot read meaning into it. Chapter 19.3 §6 already established the rule: address and data are opaque, transmitted verbatim, never adjusted.
Which is why an "obviously wrong-looking" address may be exactly right. The device has no model of the host's address map, no knowledge of which addresses are interrupt destinations, and no way to validate anything. Validation is the platform's job (§8) — and it has to be, because the device is precisely the component that might be malicious.
For INTx the split is even starker (§1). The Interrupt Pin register says which pin the device uses — a device property. The Interrupt Line register is "written by configuration software to indicate to which system interrupt line INTAn is connected." Software writes it, software reads it, and the device does nothing with it at all. It is a note software leaves itself, stored in the device's Configuration Space.
4. INTx Routing
INTx routing is legacy-compatible routing, and its defining property is that the notification carries no destination.
An Assert INTA message says "INTA is asserted." It does not say where that should go. The mapping from (device, pin) to a host interrupt is a property of the topology and the platform's configuration, resolved outside the device entirely.
Two consequences follow.
Routing is topology-dependent. The same card in a different slot, behind a different bridge, may land on a different host interrupt. Bridges may remap which pin an assertion appears as as it moves upward — the practice usually called swizzling, which exists so that four devices behind one bridge do not all land on INTA. This chapter names the mechanism and does not publish the rules, which are not in its sources (§1).
And sharing is structural. Chapter 19.1 §7 showed the OR discarding identity inside the device; routing then merges assertions from different devices onto shared host interrupts. By the time software sees it, several independent devices may be behind one interrupt — which is why every driver on it must be called and must poll its own registers.
§1's Interrupt Line register is how software records the answer it computed from topology and platform configuration. Storing it in the device is a convenience, not a mechanism.
5. MSI and MSI-X: A Write the Platform Recognizes
The routing model changes completely, because the notification now carries its own destination.
Endpoint PCIe Platform
──────── ──── ────────
posted memory write ──► routed by ADDRESS ──► address decoded as an
to a programmed like any write interrupt destination
address → delivery6. Per-Vector Routing
MSI-X's table gives each vector its own address and data (Chapter 19.3 §3), so different vectors can carry different destinations.
That is the mechanism behind interrupt affinity. Vector 3's entry may hold an address the platform decodes to one destination and vector 7's another — so the two interrupts are delivered to different places without any per-interrupt software work.
But note carefully who decides. The device stores different addresses; software chose them (§3). When an operating system changes a vector's affinity, it rewrites that table entry (Chapter 19.3 §7's masked reprogramming) — the device's behaviour is unchanged, and it never learns that anything moved.
MSI is weaker here for a structural reason (Chapter 19.2 §4): one address for the whole Function. All vectors write to the same place and are distinguished by data bits, so their destinations cannot diverge in the same way. Chapter 19.5 owns what that costs.
7. Requester Identity and Remapping
The platform can do more than decode an address. It can decide whether this device was allowed to send it.
The architectural shape:
incoming MSI/MSI-X write
+ the requester's identity (which Function issued it)
+ platform context
↓
optional interrupt-remapping lookup
↓
validated / remapped destinationEvery PCIe request carries the identity of its Requester (Chapter 11.3), so a platform receiving an interrupt write knows which Function sent it — not merely what it asked for.
That enables two things. Validation: is this Function permitted to target this destination? And translation: a Function's notion of a destination can be mapped to a real one, which is what allows a guest to program interrupt destinations as though it owned the machine while the platform maps them into the host's real space.
This chapter deliberately stops there. The table formats, lookup structures and enable mechanisms are IOMMU and platform architecture, they differ between vendors, and §1's sources do not cover them. What generalizes is the architecture — identity plus request, in, validated destination out — and §13's RTL models exactly that and nothing more.
8. Routing Is a Protection Boundary
9. What a Switch Does
A Switch forwards an MSI or MSI-X write the way it forwards any memory write — by address, according to PCIe routing rules. It does not service the interrupt.
Stated plainly because the confusion is common: a Switch has no interrupt controller, runs no handler, and does not know that a particular write is an interrupt. To it, an MSI is a posted memory write moving upstream, and it applies the same routing and flow control it applies to DMA traffic (§5).
INTx is different, and the difference is instructive. An INTx assertion is a Message, not an address-routed write, so it does not follow address routing at all — and a Switch's handling of it involves the legacy-compatibility semantics §4 described, including the pin remapping that happens as assertions move upward.
So the two mechanisms genuinely traverse the hierarchy differently: one as address-routed traffic, one as messages with topology-dependent identity. That is the structural reason §17's "INTx works but MSI does not" is such a productive symptom — the two use different paths and different permissions (§14), so a failure in one and not the other localizes immediately.
10. The Root Complex Boundary
The Root Complex is where PCIe transaction space meets the host's memory and interrupt architecture.
Below it: PCIe. Requests routed by address or ID, flow-controlled, ordered, with requester identity attached.
Above it: the platform. A memory map in which some regions are DRAM and some are interrupt-delivery logic; an interrupt controller; CPUs.
This chapter uses "platform interrupt-delivery logic" deliberately rather than naming blocks. §1's sources do not describe a specific interrupt controller, and the internals differ across vendors, generations and virtualization configurations. What is stable is the boundary: PCIe delivers an address-routed write with a requester identity, and the platform turns that into an interrupt.
11. The Routing Path
Three things to read out of the figure.
Two distinct paths leave the Endpoint, and they do not converge until the Root Complex (§9).
The Switch touches only the write path, and only as a forwarder.
And the software arrow points into the Endpoint, not into the routing chain. Software's influence on routing is exercised by programming values the device stores (§3) — which is why an affinity change is a table write, not a message to the platform.
12. RTL — Route Descriptor and Classifier
// SYNTHESIZABLE (host-side teaching model, not an Endpoint block).
// NO PLATFORM ADDRESS CONSTANTS APPEAR HERE. Section 1 declines to publish
// interrupt-controller address formats, so the decoder is parameterized
// windows -- which is also how a real decoder is configured.
package irq_route_pkg;
parameter int N_ROUTES = 8;
// GUARDED. Never zero-width, even at N_ROUTES == 1.
parameter int ROUTE_W = (N_ROUTES <= 1) ? 1 : $clog2(N_ROUTES);
typedef struct packed {
logic valid;
logic [15:0] requester_id; // 16 bits: PCIe Requester ID (Chapter 11.3)
logic [63:0] message_addr; // OPAQUE to the device (section 3)
logic [31:0] message_data;
logic is_msix; // classification only; both are memory writes
} irq_route_req_t;
// A configured decode window plus the identity it is valid for.
typedef struct packed {
logic valid;
logic [63:0] base;
logic [63:0] mask; // 1 bits participate in the compare
logic [15:0] rid_match;
logic [15:0] rid_mask; // all-zero = "any requester"
} route_window_t;
// Normalized destination. Section 21's caution: only generic fields,
// because this chapter's sources do not define platform destination
// formats (section 1).
typedef struct packed {
logic valid;
logic [15:0] destination_id;
logic [15:0] vector_id;
} route_dest_t;
function automatic bit window_hit(input route_window_t w,
input logic [63:0] a,
input logic [15:0] rid);
return w.valid
&& ((a & w.mask) == (w.base & w.mask))
&& ((rid & w.rid_mask) == (w.rid_match & w.rid_mask));
endfunction
endpackageimport irq_route_pkg::*;
// SYNTHESIZABLE. Decode one interrupt request against configured windows.
// THE OUTPUT IS DELIBERATELY THREE-WAY: no match, exactly one match, or
// AMBIGUOUS. Section 16 measured why the third case cannot be ignored --
// in randomized configurations, 42.4% of requests matched more than one
// window, and a priority encoder would have hidden every one of them.
module irq_route_classify (
input irq_route_req_t req,
input route_window_t [N_ROUTES-1:0] windows,
output logic [N_ROUTES-1:0] match_vector,
output logic route_hit,
output logic route_ambiguous,
output logic route_miss,
output logic [ROUTE_W-1:0] route_index
);
always_comb begin
for (int i = 0; i < N_ROUTES; i++)
match_vector[i] = window_hit(windows[i], req.message_addr, req.requester_id);
end
// ==================================================================
// EXACTLY ONE, OR IT IS NOT A ROUTE.
//
// $onehot -> usable
// zero -> explicit MISS (section 13: never entry 0)
// many -> AMBIGUOUS, an error, not a preference
//
// A priority encoder would turn `many` into a silent answer. Section 16
// measured the exposure: 42.4% of randomized cases, and the resulting
// interrupt goes to a real destination -- just not the intended one.
// ==================================================================
assign route_hit = req.valid && $onehot(match_vector);
assign route_ambiguous = req.valid && !$onehot0(match_vector);
assign route_miss = req.valid && (match_vector == '0);
// The index is meaningful ONLY when route_hit. It is computed with a
// one-hot encoder so an ambiguous vector cannot produce a plausible
// index that downstream logic might use anyway.
always_comb begin
route_index = '0;
if (route_hit)
for (int i = 0; i < N_ROUTES; i++)
if (match_vector[i]) route_index = ROUTE_W'(i);
end
endmoduleClassification: both synthesizable (host-side teaching model).
The three-way output is the design decision. A conventional decoder returns "which one"; this returns "exactly one, none, or ambiguous" — because §16 measured that ambiguity is not rare in configured systems, and a priority encoder converts a configuration error into a wrong-but-plausible destination.
And the requester identity participates in the match (§7). A window may be scoped to a Function or a set of them, which is the mechanism §8 describes: the same address from a different requester need not decode the same way.
Failure — four. A priority encoder silently resolves ambiguity. A miss defaulting to entry 0 delivers an unauthorized interrupt to a real destination. Ignoring the requester ID removes the isolation boundary entirely. And an index computed regardless of route_hit, which downstream logic may use.
13. RTL — Destination Lookup and Snapshot
import irq_route_pkg::*;
// SYNTHESIZABLE. Resolve a hit into a destination, and OWN IT.
// THE ROUTING TABLE IS SOFTWARE-WRITABLE AND MAY CHANGE AT ANY TIME --
// an OS changing affinity rewrites it (section 6). Section 16 measured
// what happens without a snapshot: the owned destination changed under
// stall in 57.8% of cases.
module irq_route_lookup (
input logic clk,
input logic rst_n,
input irq_route_req_t req,
output logic req_ready,
input logic route_hit,
input logic route_ambiguous,
input logic route_miss,
input logic [ROUTE_W-1:0] route_index,
// Software-writable destination table. Changes whenever software says.
input route_dest_t [N_ROUTES-1:0] dest_table,
// Downstream: platform interrupt-delivery logic.
output logic out_valid,
output route_dest_t out_dest,
output logic [15:0] out_requester_id,
output logic [31:0] out_message_data,
input logic out_ready,
output logic err_ambiguous,
output logic err_miss,
output logic err_dest_invalid
);
route_dest_t held_dest_q;
logic [15:0] held_rid_q;
logic [31:0] held_data_q;
logic busy_q, amb_q, miss_q, inv_q;
assign out_valid = busy_q;
assign out_dest = held_dest_q;
assign out_requester_id = held_rid_q;
assign out_message_data = held_data_q;
assign req_ready = !busy_q;
assign err_ambiguous = amb_q;
assign err_miss = miss_q;
assign err_dest_invalid = inv_q;
// Range safety: route_index is only meaningful on a hit, and the table
// entry itself must be valid.
wire dest_ok = route_hit && dest_table[route_index].valid;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
held_dest_q <= '0; held_rid_q <= '0; held_data_q <= '0;
busy_q <= 1'b0; amb_q <= 1'b0; miss_q <= 1'b0; inv_q <= 1'b0;
end else begin
if (!busy_q && req.valid) begin
if (route_ambiguous) begin
// NOT a route. Reported, and nothing is forwarded (section 12).
amb_q <= 1'b1;
end else if (route_miss) begin
// NOT entry 0. An unmatched interrupt is dropped and reported --
// silently routing it would deliver an interrupt nobody
// authorised (section 17, mutation 3).
miss_q <= 1'b1;
end else if (!dest_ok) begin
inv_q <= 1'b1;
end else begin
// ==========================================================
// SNAPSHOT AT THE OWNERSHIP BOUNDARY.
//
// The destination, the requester identity and the message data
// are captured TOGETHER, in one assignment. Downstream reads
// only these registers -- never dest_table -- so a software
// affinity change during the stall cannot redirect a request
// that has already been accepted.
//
// Section 16: reading the live table instead changed the owned
// destination in 57.8% of stall/update cases.
//
// This is Chapter 19.3 section 7's MSI-X snapshot rule, one
// layer up: a structure two agents share must be sampled once,
// whole, at a defined boundary.
// ==========================================================
held_dest_q <= dest_table[route_index];
held_rid_q <= req.requester_id; // identity travels WITH it
held_data_q <= req.message_data;
busy_q <= 1'b1;
end
end else if (busy_q && out_ready) begin
busy_q <= 1'b0; // ownership moves ONCE
end
end
end
endmoduleClassification: synthesizable (host-side teaching model).
The snapshot is the whole block. Destination, requester identity and message data are captured in one assignment at the ownership boundary, and downstream reads only the captured copy.
Measured (§16): with a snapshot, 0 cases of the owned destination changing under stall; reading the live table, 57.8%.
And the requester identity travels with the request. §8's protection argument only works if the identity that was validated is the identity still attached downstream — a design that dropped it after the match would validate one thing and deliver another.
Failure — five. Reading dest_table downstream (§16's counterexample). A miss defaulting to entry 0. Ambiguity resolved by priority. Dropping the requester ID after the match. And clearing busy_q on out_valid rather than the handshake, which forwards one request twice.
14. Assertions
// SVA over the routing blocks. LOCAL contract only. Nothing asserts that a
// CPU services the interrupt, that the routing table is configured, or that
// downstream ever becomes ready.
// ---- ENVIRONMENT ------------------------------------------------------
assume property (@(posedge clk) disable iff (!rst_n)
(req.valid && !req_ready) |=> (req.valid && $stable(req)));
// ---- DECODE -----------------------------------------------------------
// P1: A ROUTE EXISTS ONLY IF EXACTLY ONE WINDOW MATCHES. Section 16: 42.4%
// of randomized configurations produced multiple matches.
property p_onehot_route;
@(posedge clk) disable iff (!rst_n)
route_hit |-> $onehot(match_vector);
endproperty
a_onehot : assert property (p_onehot_route);
// P2: AMBIGUITY IS AN ERROR, NOT A PREFERENCE.
property p_ambiguous_reported;
@(posedge clk) disable iff (!rst_n)
(req.valid && !$onehot0(match_vector)) |=> (err_ambiguous && !out_valid);
endproperty
a_amb : assert property (p_ambiguous_reported);
// P3: A MISS IS NEVER SILENTLY ROUTED. Not to entry 0, not to anything.
property p_miss_not_routed;
@(posedge clk) disable iff (!rst_n)
(req.valid && (match_vector == '0)) |=> (err_miss && !out_valid);
endproperty
a_miss : assert property (p_miss_not_routed);
// P4: the route index is in range and only meaningful on a hit.
property p_index_range;
@(posedge clk) disable iff (!rst_n)
route_hit |-> (route_index < ROUTE_W'(N_ROUTES) && match_vector[route_index]);
endproperty
a_idx : assert property (p_index_range);
// ---- OWNERSHIP AND THE SNAPSHOT ---------------------------------------
// P5: THE OWNED DESTINATION IS STABLE UNDER STALL, even if software
// rewrites the routing table. The property the 57.8% figure violates.
property p_dest_stable;
@(posedge clk) disable iff (!rst_n)
(out_valid && !out_ready) |=> (out_valid && $stable(out_dest));
endproperty
a_stable : assert property (p_dest_stable);
// P5b: stated against the TABLE explicitly -- a table write during a stall
// must not appear at the output.
property p_table_write_isolated;
@(posedge clk) disable iff (!rst_n)
(out_valid && !out_ready && dest_table_written) |=> $stable(out_dest);
endproperty
a_iso : assert property (p_table_write_isolated);
// P6: REQUESTER IDENTITY TRAVELS WITH THE REQUEST. Section 8's protection
// argument depends on the validated identity being the delivered one.
property p_identity_preserved;
@(posedge clk) disable iff (!rst_n)
$rose(out_valid) |-> (out_requester_id == $past(req.requester_id));
endproperty
a_ident : assert property (p_identity_preserved);
// P6b: and it is stable while owned.
property p_identity_stable;
@(posedge clk) disable iff (!rst_n)
(out_valid && !out_ready) |=> $stable(out_requester_id);
endproperty
a_istable : assert property (p_identity_stable);
// P7: ONE ACCEPTED INPUT PRODUCES AT MOST ONE DOWNSTREAM NOTIFICATION.
property p_no_duplication;
@(posedge clk) disable iff (!rst_n)
(out_valid && out_ready) |=> !out_valid until_with (req.valid && req_ready);
endproperty
a_dup : assert property (p_no_duplication);
// P8: the message data delivered is the data that arrived -- the routing
// layer does not compose or adjust it (Chapter 19.3 section 6).
property p_data_verbatim;
@(posedge clk) disable iff (!rst_n)
$rose(out_valid) |-> (out_message_data == $past(req.message_data));
endproperty
a_data : assert property (p_data_verbatim);
// P9: an invalid destination entry is refused, not forwarded.
property p_dest_valid;
@(posedge clk) disable iff (!rst_n) out_valid |-> out_dest.valid;
endproperty
a_dv : assert property (p_dest_valid);
// ---- CLASSIFICATION ---------------------------------------------------
// P10: INTx and message-signalled paths remain distinct. An INTx assertion
// is a Message, not an address-routed write (section 9).
property p_intx_not_written;
@(posedge clk) disable iff (!rst_n)
dut_intx_path.assert_msg |-> !req.valid;
endproperty
a_class : assert property (p_intx_not_written);
// P11: reset clears the owned descriptor.
property p_reset;
@(posedge clk) !rst_n |=> (!out_valid && !err_ambiguous && !err_miss);
endproperty
a_reset : assert property (p_reset);P1 with P2 and P3 are the three-way decode contract, and all three are needed: P1 says a route is one-hot, P2 says ambiguity is reported rather than resolved, P3 says a miss is dropped rather than defaulted.
P5 with P5b are the snapshot pair. P5 alone is satisfiable by a design that happens not to be updated during the test; P5b names the table write explicitly.
And P6 is the security-relevant one. §8's isolation argument is only as good as the identity that arrives downstream.
15. Verification, Fault Injection, and Model Verification
Executed before publication.
Route decode — randomized configurations
200,000 random (window configuration, address) cases:
| Outcome | Cases | |
|---|---|---|
| no match | 37,150 | 18.6% — must be an explicit drop |
| exactly one match | 77,990 | 39.0% |
| more than one match | 84,860 | 42.4% — ambiguous |
Ambiguity is not a corner case in configured systems, which is why §12 reports it rather than resolving it.
A worked overlap: windows (base 0x100, mask 0xF00) and (base 0x000, mask 0x000) — the second matches everything. Address 0x140 matches both, and a priority encoder silently picks window 0.
Table update under stall
50,000 random (stall length, update pattern) cases:
| Implementation | Owned destination changed under stall |
|---|---|
| §13's snapshot at the ownership boundary | 0 |
reading dest_table live | 28,879 — 57.8% |
Directed tests
- Each path — INTx abstraction, MSI, MSI-X — verify classification stays distinct (P10).
- Exactly one matching window — verify hit, index and destination (P1, P4).
- Two overlapping windows — verify ambiguity reported, nothing forwarded (P2). Required.
- No matching window — verify drop and report, never entry 0 (P3). Required.
- Destination table entry invalid — verify refusal (P9).
- Table rewritten while a request is stalled — verify the owned destination is unchanged (P5, P5b). Required, and §16's counterexample.
- Same address from two different Requester IDs with requester-scoped windows — verify they decode differently (§7).
- Requester ID dropped — verify P6 fails.
- Downstream stalls for 1, 2 and 50 cycles — verify stability and no duplication (P5, P7).
- Back-to-back requests;
N_ROUTES= 1, 2, 8; reset with a request owned.
The scoreboard maintains an independent route dictionary — a plain map from (address, requester) to destination — and never reads match_vector, route_index or the DUT's tables.
Mutations
| # | Mutation | Caught by | System symptom |
|---|---|---|---|
| 1 | requester ID dropped after the match | P6 | isolation lost; validated identity is not the delivered one (§8) |
| 2 | two matches resolved by priority encoder | P1, P2 | interrupt to a real but wrong destination — 42.4% exposure |
| 3 | no match routes to entry 0 | P3 | unauthorized interrupt delivered to a valid destination |
| 4 | destination read live from the table | P5b | affinity change redirects an in-flight interrupt — 57.8% |
| 5 | message data adjusted by the routing layer | P8 | vector identity altered in transit |
| 6 | route index used without checking route_hit | P4 | ambiguous or missed request produces a plausible index |
| 7 | INTx treated as an address-routed write | P10 | legacy path forced through the wrong machinery (§9) |
| 8 | MSI-X vector identity lost in the data field | P8 | all vectors of a Function deliver identically |
| 9 | Switch modelled as servicing the interrupt | review + §9 | architecture that cannot exist |
| 10 | remap bypassed for one path | P6 | one path unvalidated; the isolation boundary has a hole |
| 11 | busy_q cleared on out_valid | P7 | one notification delivered twice |
| 12 | snapshot cleared before the handshake | P5 | destination becomes stale mid-transfer |
| 13 | stale route reused after reset | P11 | first post-reset interrupt goes to the previous destination |
| 14 | table write during a stall mutates the request | P5b | same as 4, from the software side |
| 15 | one device's window applied to another requester | P6 + scoreboard | cross-device interrupt delivery |
Same-cycle audit
| Case | Declared resolution |
|---|---|
| request arrives + table write same cycle | the request captures the value presented that cycle; §13 assigns all fields together so no mixed snapshot exists |
| route captured + window invalidated | the captured request is unaffected (P5); the invalidation applies to the next request |
| downstream transfer + new request | ownership moves out, then in — req_ready rises the cycle after (P7) |
| two matches + a valid destination | ambiguity wins; nothing is forwarded (P2) |
| miss + downstream ready | no transfer; the miss is reported (P3) |
| reset + owned request | reset wins (P11) |
16. Debugging
Symptom → which side of the boundary → signal → distinguishing experiment.
The first move is never to open the MSI-X table (§2). It is to establish which of the four owners is involved.
The Endpoint sends vector 5 and CPU 7 receives vector 9
Walk the chain in order; each step has a different owner:
1. device vector selection did it pick 5? -> 19.2 §12, 19.3 §13
2. table entry contents what addr/data is in 5? -> driver
3. what was transmitted analyzer: addr + data -> the boundary
4. decode which window matched? -> §12
5. destination lookup what did the table say? -> §13
6. remap was it translated? -> §7
7. delivery which CPU, which vector? -> platformStep 3 is the boundary and the highest-value observation. If the transmitted address and data match table entry 5's contents, the device is exonerated completely — steps 1 and 2 are correct, and everything remaining is platform-side.
Most engineers start at step 2 and stop there because the table is easy to read. A correct table with a wrong outcome means the fault is at step 4 or beyond, and staying at step 2 finds nothing.
An interrupt reaches the wrong CPU only after an affinity change
This is §16's counterexample — mutation 4, almost certainly.
The tell is the timing. Steady-state delivery is correct; only interrupts in flight across a table update go wrong. That is a snapshot failure, not a decode failure.
The distinguishing experiment: stall the delivery path and rewrite the routing table during the stall. If the delivered destination reflects the new table rather than the old, the destination is being read live (P5b).
One device can interrupt another guest's CPU
A protection failure, and §8 says where to look.
Read three things. The requester identity attached to the offending request — is it correct, or was it dropped (mutation 1)? The remapping configuration for that Requester ID — does a window scoped to it exist? And whether any path bypasses remapping (mutation 10) — an isolation boundary with one hole is not a boundary.
And check §12's ambiguity handling. A window with an all-zero requester mask matches every requester — §16's worked overlap — so one over-broad entry can make every device's interrupts decode through it.
INTx works but MSI does not
Two different paths and two different permissions (§9), and §1 gives the sharpest check.
Read Bus Master Enable. §1: "as MSI/MSI-X interrupt messages are in-band memory writes, setting the Bus Master Enable bit to 0b disables MSI/MSI-X interrupt messages as well" — while "requests other than memory or I/O requests are not controlled by this field", so INTx keeps working.
So "INTx works, MSI does not" is the exact signature of BME being clear, and it takes one register read to confirm. It is also the signature of the MSI address decoding to nothing (P3) — and those two are distinguished by whether the write appears on an analyzer at all.
Interrupts stop entirely after a driver reconfiguration
Check BME first (above), then check for a decode miss (P3). A driver that reprogrammed an address into a region no window covers produces interrupts that are transmitted correctly and dropped silently — which is why §12 reports misses rather than swallowing them.
17. Common Misconceptions
- "The Endpoint chooses which CPU to interrupt." It stores an address software programmed and cannot interpret (§3).
- "An MSI address is ordinary DRAM." It is an address the platform decodes as an interrupt destination (§5).
- "A Switch services the interrupt." It forwards a memory write (§9).
- "MSI routing is the same on every platform." Above the Root Complex it is platform architecture (§10).
- "An MSI-X entry contains the handler address." It contains a destination the platform decodes; the handler is software's business (§3).
- "Requester ID is irrelevant to interrupt security." It is the part the device cannot forge, and the basis of validation (§7, §8).
- "Route-table updates can freely change in-flight interrupts." 57.8% redirection without a snapshot (§16).
- "CPU affinity is a field in the TLP." It is expressed by which address software programmed (§6).
- "INTx and MSI use the same host machinery." Different paths, different permissions (§9, §14).
- "A no-match should go somewhere sensible." It must be dropped and reported (§12).
- "Two matching routes is a preference." It is a configuration error — 42.4% exposure in randomized configs (§16).
- "The device can validate its own interrupt address." It has no model of the host address map, and it is the untrusted party (§3, §8).
18. Understanding Check
19. What's Next
Routing is where a correct notification becomes a correct interrupt — or does not.
The Endpoint's job ends at transmission (§2). It stores routing information software programmed and cannot interpret (§3), and everything past the Root Complex is platform architecture (§10).
MSI and MSI-X travel as address-routed memory writes that Switches forward like any other write (§9), while INTx travels as messages with topology-dependent identity (§4) — which is why the same Bus Master Enable bit stops one and not the other (§1, §14).
And the routing layer is the protection boundary (§8), because it holds the one thing the device cannot forge: the requester's identity.
Chapter 19.5 — Performance Advantages closes Module 19 by making the argument quantitative. Not "MSI-X is faster" — but which costs message-signalled interrupts actually remove, which ones they leave untouched, and why more vectors do not automatically mean more performance.
The idea to carry forward: a structure two agents share must be sampled once, whole, at a defined boundary.