ldls: a second driver, and everything it found
e1000 -- Linux's, unmodified -- probes, opens, gets an address by DHCP and
carries ping traffic in a SchOS process. The kernel was driving that NIC and
gave it up, which makes it the first real handover as well as the second
driver.
None of what it needed was a missing feature. Every one was a call that
succeeded, returned zero and did nothing:
_find_next_bit "bit 0 is set" -- for_each_cpu never ended
netif_carrier_off carrier already up -- TCTL.EN never set
__x86_indirect_thunk_r8 the jump discarded -- 255 rx buffers, none made
dma_map_page_attrs bus address 0 -- NIC aimed at physical 0
so the rule from memcpy is now general: a stub returning zero is unsafe
wherever zero is a meaningful answer.
The loader grew ko_fixup_percpu(). The stack guard turned out to be one
per-CPU variable among several; `current` cannot be defused because the driver
dereferences it. Taking the %gs off instead leaves the same opcode reading the
same width from storage the host supplies -- a per-CPU variable in a
single-threaded host is just a variable. Eight bytes become seven and a nop,
and the displacement is not always last, so there is now just enough of an x86
decoder in ko.c to know how long the instruction is.
ldls/kpi/thunk.c is fifteen lines of assembly and the highest-value file here:
retpolines are control flow, and a stub cannot stand in for a jump.
The kernel side:
- ioperm.c, the TSS I/O permission bitmap. A driver's outl is an instruction,
not a call, and e1000's reset path picks it deliberately. IOPL=3 would hand
over the PIC and the disk with it, so it is per-port. Writing it found that
the I/O map base had been written at offset 64 rather than 102, leaving the
CPU checking ports 0-831 against the TSS's own RSP0.
- claim masks the device's IRQ line: 450,000 a second, otherwise.
- net_detach(), the missing half of net_attach_late(). An interface left in
the stack keeps its route and sends every frame to a driver that refuses
it, which looks exactly like a userspace driver that does not work.
- e1000_release() gives up the interface, not just the hardware.
8139too still passes 4/4; ldls-check still passes 27; four CPUs, no panic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
f38eb8a7fd
commit
071499a378
26 changed files with 2009 additions and 46 deletions
61
include/ioperm.h
Normal file
61
include/ioperm.h
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#pragma once
|
||||
|
||||
// Port I/O, from a process.
|
||||
//
|
||||
// A Linux driver does not always ask. `ioread32` is a function call LDLS can
|
||||
// implement, but `outl` is an *instruction*, compiled into the module, and the
|
||||
// first foreign driver to need one found this the hard way:
|
||||
//
|
||||
// e1000_io_write+0xd: out %eax,(%dx) -> trap 13 err 0, rdx=0xc000
|
||||
//
|
||||
// That is not a driver doing something exotic. e1000's own reset path picks it
|
||||
// deliberately -- "these controllers can't ack the 64-bit write when issuing
|
||||
// the reset, so use IO-mapping as a workaround" -- so on QEMU's 82540EM there
|
||||
// is no route to a working NIC that avoids it. Any layer that runs somebody
|
||||
// else's drivers has to let a process reach the ports of the device it holds.
|
||||
//
|
||||
// **Which ports, and no others.** x86 offers two ways to grant this and they
|
||||
// are very far apart. IOPL=3 is one flag and hands over all 65536 ports -- the
|
||||
// interrupt controller, the timer, the disk. The TSS I/O permission bitmap is
|
||||
// per port, and the ports a process gets are exactly the I/O BARs of the PCI
|
||||
// devices it has claimed. The second costs a few hundred lines and is the only
|
||||
// one worth having: a foreign driver already has DMA with no IOMMU, and the
|
||||
// answer to "this is not a security boundary" is not to move it further.
|
||||
//
|
||||
// It is the same bargain as kpi_io_ok() in ldls/kpi/io.c, one level down and
|
||||
// enforced by hardware rather than by the driver's host being polite.
|
||||
|
||||
#include "types.h"
|
||||
|
||||
struct proc;
|
||||
|
||||
// Ranges per process. A PCI device has at most six BARs and a process may hold
|
||||
// several devices; eight is past anything seen and small enough to scan.
|
||||
#define NIOPORT 8
|
||||
|
||||
struct ioport_range {
|
||||
uint16 base;
|
||||
uint16 len;
|
||||
};
|
||||
|
||||
// Grant / revoke. Called from /dev/pci when a process claims or releases a
|
||||
// device, and from exit() by way of pciuser_release_all().
|
||||
//
|
||||
// Granting a range twice is not an error -- a driver may claim, release and
|
||||
// claim again -- and revoking one that was never granted is not either.
|
||||
void ioperm_grant(struct proc *p, uint16 base, uint16 len);
|
||||
void ioperm_revoke_all(struct proc *p);
|
||||
|
||||
// Install this process's ranges in the running CPU's TSS. Called from
|
||||
// switchuvm() with interrupts off.
|
||||
void ioperm_switch(struct proc *p);
|
||||
|
||||
// The per-CPU TSS, which now has a bitmap attached to it. seginit() asks for
|
||||
// this CPU's and points the GDT descriptor at it.
|
||||
void *ioperm_tss(int cpu, uint64 *limit);
|
||||
|
||||
// The same block, for everyone after seginit().
|
||||
void *ioperm_tss_of(int cpu);
|
||||
|
||||
// Where RSP0 lives, so switchuvm() does not have to know the layout.
|
||||
void ioperm_set_rsp0(void *tss, addr_t stktop);
|
||||
Loading…
Reference in a new issue