kernel: boot on AMD — SYSRET puts the RPL in the STAR base
A Ryzen 3 3200G triple-faulted on its first timer tick after reaching init. Three defects in a chain, each hiding the one beneath it. STAR's SYSRET base was 0x10, so SS came back as base+8 = 0x18 with RPL 0 while CS carried RPL 3. Intel ORs RPL 3 into SS on SYSRET; AMD only does so for CS. Ring 3 ran fine — RPL is not checked on data access — and died the moment an interrupt tried to IRETQ back, where SS.RPL must equal CS.RPL. The base now carries the RPL (0x13), as Linux does. Two fixes below it, both of which made the first one unreadable: scheduler() read IA32_GS_BASE and dereferenced it without testing for zero, so every fault reporter faulted in turn — a panic inside a panic, and the machine reset before printing anything. Cast after the null test, plus a re-entrancy guard in the panic handler. NT is now masked in SFMASK alongside the rest, and isr.s exports isr_return_iretq at the faulting instruction so a frame dump can say which IRETQ died and print the CS/SS it was about to load. That dump is what identified the RPL mismatch.
This commit is contained in:
@@ -518,4 +518,13 @@ isr_smap_patch:
|
||||
testb $3, 8(%rsp)
|
||||
jz 1f
|
||||
swapgs
|
||||
1: iretq
|
||||
# Named so a fault reporter can recognise its own return instruction. A #GP
|
||||
# here is the frame's fault, not this code's — the five words below RSP are
|
||||
# what the CPU rejected, and they are the only evidence of why. Note that on
|
||||
# the ring-3 path the swapgs above has already run, so a fault at this exact
|
||||
# address re-enters the kernel with the *user's* GS base: per-CPU reads in
|
||||
# that handler are reading user-controlled state and must not be trusted.
|
||||
1:
|
||||
.global isr_return_iretq
|
||||
isr_return_iretq:
|
||||
iretq
|
||||
|
||||
@@ -57,10 +57,21 @@ pub fn setLocal(index: usize, scheduler_ptr: usize) void {
|
||||
io.wrmsr(ia32_gs_base, @intFromPtr(&blocks[index]));
|
||||
}
|
||||
|
||||
/// The scheduler pointer for the running core (via the GS base). Valid in any
|
||||
/// ring-0 context under the swapgs discipline.
|
||||
/// The scheduler pointer for the running core (via the GS base), or 0 before this
|
||||
/// core has one. Valid in any ring-0 context under the swapgs discipline.
|
||||
///
|
||||
/// **Zero is an answer here, not a fault.** A core has no per-CPU block between
|
||||
/// reset and its `setLocal`, and the fault reporters are documented to be callable
|
||||
/// unconditionally: `currentIdSafe`, `currentNameSafe`, `currentCpuIndex` and
|
||||
/// `sync.releaseIfHeldHere` all test this for 0 and mean it. Casting the base
|
||||
/// before testing it put the null one instruction out of their reach — an early
|
||||
/// fault reported itself by panicking on the cast, and the panic handler, calling
|
||||
/// those same reporters on its next line, panicked again, so the machine reset
|
||||
/// instead of halting with the message that would have said what went wrong.
|
||||
pub fn scheduler() usize {
|
||||
return @as(*const ArchitecturePerCpu, @ptrFromInt(io.rdmsr(ia32_gs_base))).scheduler;
|
||||
const base = io.rdmsr(ia32_gs_base);
|
||||
if (base == 0) return 0;
|
||||
return @as(*const ArchitecturePerCpu, @ptrFromInt(base)).scheduler;
|
||||
}
|
||||
|
||||
/// Record core `index`'s kernel stack top, used by the system_call entry stub to
|
||||
@@ -76,16 +87,35 @@ const ia32_star = 0xC000_0081;
|
||||
const ia32_lstar = 0xC000_0082;
|
||||
const ia32_sfmask = 0xC000_0084;
|
||||
|
||||
/// The SYSRET half of STAR: `sysret` loads CS from base+16 and SS from base+8.
|
||||
/// The base carries RPL 3 itself — 0x13, not the 0x10 the GDT layout suggests —
|
||||
/// because **only CS is guaranteed to come out at ring 3**. CS must: `sysret`
|
||||
/// sets CPL to 3, so the selector it loads is forced to match. SS is under no
|
||||
/// such obligation, and the two vendors differ on it: Intel ORs 3 into the SS
|
||||
/// selector as well, AMD hands it over exactly as the arithmetic produced it.
|
||||
///
|
||||
/// With base 0x10 an AMD machine therefore enters ring 3 carrying SS 0x18 — RPL
|
||||
/// 0 — and nothing complains, because a data access is checked against CPL and
|
||||
/// the descriptor's DPL, never the selector's RPL. It runs perfectly until the
|
||||
/// first interrupt: the CPU pushes that SS, and the `iretq` returning to ring 3
|
||||
/// requires SS.RPL to equal CS.RPL, refusing 0 against 3 with #GP(0x18). A
|
||||
/// process that made system calls happily then dies on its first timer tick.
|
||||
///
|
||||
/// Putting the 3 in the base makes both selectors right by construction on
|
||||
/// either vendor: 0x13 + 8 = 0x1B, 0x13 + 16 = 0x23. Intel's OR is then a no-op
|
||||
/// rather than the thing holding it together.
|
||||
const star_sysret_base: u64 = 0x13; // user data 0x18 / user code 0x20, with RPL 3
|
||||
const star_syscall_base: u64 = 0x08; // kernel code 0x08 / kernel data 0x10
|
||||
|
||||
/// Enable the `system_call`/`sysret` fast path on this core (BSP and each AP). EFER.SCE
|
||||
/// turns the instructions on; STAR sets the selectors system_call/sysret load; LSTAR
|
||||
/// is the entry stub (isr.s); SFMASK clears RFLAGS bits on entry (notably IF —
|
||||
/// the handler runs with interrupts off, like the int-gate path). The GDT is laid
|
||||
/// out (kernel code 0x08, then user data 0x18 / code 0x20) precisely so these line
|
||||
/// up: system_call loads CS 0x08 / SS 0x10; sysret loads CS = base+16 and SS = base+8
|
||||
/// with RPL forced to 3, so base 0x10 gives CS 0x23 (user code|3) and SS 0x1B.
|
||||
/// up: system_call loads CS 0x08 / SS 0x10; sysret loads CS 0x23 and SS 0x1B.
|
||||
pub fn initSystemCall() void {
|
||||
io.wrmsr(ia32_efer, io.rdmsr(ia32_efer) | 1); // SCE
|
||||
io.wrmsr(ia32_star, (@as(u64, 0x08) << 32) | (@as(u64, 0x10) << 48));
|
||||
io.wrmsr(ia32_star, (star_syscall_base << 32) | (star_sysret_base << 48));
|
||||
const entry = @extern(*const anyopaque, .{ .name = "syscall_entry" });
|
||||
io.wrmsr(ia32_lstar, @intFromPtr(entry));
|
||||
// Clear IF, TF, DF, AC and NT on entry. The first four are the usual
|
||||
|
||||
Reference in New Issue
Block a user