syscall: three globals on the entry path, shared by every CPU
One to four processes died per boot with a fault they could not have caused:
`rip 0x0` -- an instruction fetch at zero, which a process does not branch to
-- or a dereference of `0xffffffffffffffea`, which is `-EINVAL`. It hit shells
and schinitd alike and had been carried for a long time as "zsh sometimes jumps
into .data".
`syscall_entry` has to swap FS_BASE from the user's TLS to the kernel's before
it can touch any `%fs:` data, and `wrmsr` destroys rax, rcx and rdx. Those have
to be parked somewhere first -- not on the user stack, which is untrusted and
has not been switched away from yet. They were parked in three globals:
// Single-CPU scratch for syscall_entry FS swap
// (safe while IF cleared by SYSCALL/SFMASK)
uint64 _sc_rax, _sc_rcx, _sc_rdx;
IF being clear answers for an interrupt arriving on *this* CPU. It says nothing
whatever about a second CPU executing the same three stores at the same
instant, and with more than one processor entering syscalls that is not a rare
interleaving -- it is the ordinary one.
**%rcx is the user return address.** SYSCALL puts the user's rip there and
SYSRET takes it back from there. So the CPU that lost the race returned its
process into *another process's* instruction stream, with %rax -- the syscall
number on the way in, the return value on the way out -- swapped alongside it.
Both symptoms fall straight out of that: a zero handed over and followed, and
an errno used as a pointer.
Per-CPU words at %gs:32..48 now. %gs is usable there, before the FS swap, for
exactly the reason RESTORE_KERNEL_FS can already read %gs:0: GS base is per-CPU
in hardware, seginit() sets it once, and userspace cannot change it --
sys_arch_prctl() implements ARCH_SET_FS and ARCH_GET_FS and rejects the rest.
Measured rather than argued. Both variants built once, `build/esp` saved for
each, the same pristine schfs.img restored before every boot, -smp 4:
three globals 10 boots 10 faults, in five of the ten
three per-CPU 18 boots 0 faults
A _Static_assert pins all seven per-CPU offsets, because trapasm.S spells them
as literals and cannot include a header -- a slot that moves would leave a
register quietly holding somebody else's value, which is the hardest possible
thing to find.
**This is the second bug of this exact shape in this file.** The first was
`kernel_fs_base`, a single global every CPU wrote during its own seginit(), so
the last writer won and every CPU restored one CPU's per-CPU block;
RESTORE_KERNEL_FS reads %gs:0 for that reason. The lesson generalises and is
now written down in both places: **on the entry path, a global is a bug.**
Closes the row added to Gate 0 four commits ago. It was the right row to put
first -- a compositor built on this would have failed in ways indistinguishable
from its own bugs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
374ed80096
commit
3185685997
6 changed files with 173 additions and 36 deletions
|
|
@ -80,6 +80,9 @@ extern int ncpu_online; // processors actually running
|
|||
// +8 syscall scratch trapasm.S parks %rsp here across the stack switch
|
||||
// +16 struct cpu *
|
||||
// +24 struct proc *
|
||||
// +32 syscall rax the three registers wrmsr destroys, parked across
|
||||
// +40 syscall rcx the FS_BASE swap at the top of syscall_entry
|
||||
// +48 syscall rdx
|
||||
//
|
||||
// Offset 8 is NOT free. syscall_entry uses %fs:(8) as its scratch slot, and
|
||||
// because seginit() sets GS base and FS base to the same address, %fs:8 and
|
||||
|
|
@ -87,12 +90,23 @@ extern int ncpu_online; // processors actually running
|
|||
// it with %rsp -- a #GP in syscall_entry itself, several layers below anything
|
||||
// that could report it.
|
||||
//
|
||||
// trapasm.S reads %gs:24 for proc on the way out to user mode, so these three
|
||||
// offsets are shared with assembly and must not be reordered.
|
||||
// **Offsets 32..48 were three globals**, `_sc_rax`/`_sc_rcx`/`_sc_rdx` in
|
||||
// vm.c, under a comment saying they were safe because SYSCALL clears IF. IF
|
||||
// answers for interrupts on *this* CPU and says nothing about another CPU
|
||||
// executing the same three stores. Two CPUs entering a syscall together
|
||||
// overwrote each other's parked registers, and %rcx is the user return address
|
||||
// that sysretq uses -- so one process returned into another's instruction
|
||||
// stream. See the note on syscall_entry in trapasm.S.
|
||||
//
|
||||
// trapasm.S reads all of these, so the offsets are shared with assembly and
|
||||
// must not be reordered.
|
||||
#define PERCPU_SELF 0
|
||||
#define PERCPU_SCRATCH 8
|
||||
#define PERCPU_CPU 16
|
||||
#define PERCPU_PROC 24
|
||||
#define PERCPU_SC_RAX 32
|
||||
#define PERCPU_SC_RCX 40
|
||||
#define PERCPU_SC_RDX 48
|
||||
|
||||
static inline struct cpu *
|
||||
cpu_get(void)
|
||||
|
|
|
|||
Loading…
Reference in a new issue