SchOS/kernel/arch/x86_64/ioperm.c
Nexoniarz 071499a378 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
2026-09-14 15:20:07 +02:00

165 lines
5.2 KiB
C

// The TSS I/O permission bitmap. See include/ioperm.h for why.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "mmu.h"
#include "proc.h"
#include "ioperm.h"
// The 64-bit TSS, written out rather than indexed.
//
// The old code reached it as `uint *tss` and set `tss[16]` to put 0x68 in the
// I/O map base -- but the I/O map base is at offset 102, which is the high half
// of the dword at index 25, not 16. Index 16 is offset 64, in the middle of the
// interrupt stack table.
//
// That was not harmless. The field it meant to write stayed 0, and a base of 0
// with a limit of 0x67 does not mean "no bitmap" -- it means the bitmap starts
// at the top of the TSS, so the CPU checked ports 0-831 against the TSS's own
// bytes. Those bytes are RSP0: a kernel stack pointer, whose zero bits are
// *permission*. Some low ports were reachable from user mode and had been
// since the kernel was 64-bit, which nothing had any reason to notice.
//
// Naming the fields is what makes that visible, so they are named.
struct taskstate64 {
uint32 res0;
uint64 rsp0;
uint64 rsp1;
uint64 rsp2;
uint64 res1;
uint64 ist[7];
uint64 res2;
uint16 res3;
uint16 iomb; // offset from the base of the TSS
} __attribute__((packed));
#define IOMAP_BITS 65536
#define IOMAP_BYTES (IOMAP_BITS / 8)
// The TSS and its bitmap have to be one object: `iomb` is a 16-bit offset from
// the TSS base, and the TSS descriptor's limit is what cuts the bitmap off.
// So they are declared together and there is no way for them to drift apart.
//
// 8 KB per CPU of permanently resident kernel memory is the cost of being able
// to say "these ports and no others", and it is charged whether or not any
// process ever uses it. A bitmap sized to the highest port actually granted
// would be smaller and would have to be resized at runtime, under a descriptor
// the CPU is using; this is the version that is obviously correct.
struct tssblk {
struct taskstate64 ts;
uint8 map[IOMAP_BYTES];
// Intel requires a byte of all-ones past the end, so an access that straddles
// the final port still reads a deny bit rather than whatever follows.
uint8 tail;
} __attribute__((packed, aligned(16)));
static struct tssblk tssblk[NCPU];
// What each CPU currently has open. Kept per CPU rather than read back from the
// process, because switching *away* has to close what was opened and the
// process that opened it may be part-way through exit() by then.
static struct {
struct ioport_range r[NIOPORT];
int n;
} installed[NCPU];
static void
map_set(uint8 *map, uint16 base, uint16 len, int allow)
{
uint32 p, end = (uint32)base + len;
if (end > IOMAP_BITS)
end = IOMAP_BITS;
for (p = base; p < end; p++) {
if (allow)
map[p >> 3] &= ~(1 << (p & 7));
else
map[p >> 3] |= (1 << (p & 7));
}
}
void *
ioperm_tss(int cpu, uint64 *limit)
{
struct tssblk *b = &tssblk[cpu];
// Deny everything to start with. A zero bit is permission, so a zeroed
// bitmap is an open machine -- this memset is not initialisation tidiness.
memset(b->map, 0xFF, sizeof(b->map));
b->tail = 0xFF;
b->ts.iomb = (uint16)__builtin_offsetof(struct tssblk, map);
installed[cpu].n = 0;
// The limit is the offset of the last valid byte, and it has to include the
// trailing 0xFF -- stopping one byte short is the difference between a
// straddling access being denied and it reading the next object.
*limit = (uint64)__builtin_offsetof(struct tssblk, tail);
return b;
}
// The same block, without the reset. seginit() calls ioperm_tss() once per CPU
// to build it; everything after that just wants the pointer.
void *
ioperm_tss_of(int cpu)
{
return &tssblk[cpu];
}
void
ioperm_set_rsp0(void *tss, addr_t stktop)
{
((struct taskstate64 *)tss)->rsp0 = (uint64)stktop;
}
void
ioperm_grant(struct proc *p, uint16 base, uint16 len)
{
int i;
if (len == 0)
return;
for (i = 0; i < p->nioport; i++)
if (p->ioport[i].base == base && p->ioport[i].len == len)
return;
if (p->nioport >= NIOPORT)
return; // refused quietly; the driver will fault loudly
p->ioport[p->nioport].base = base;
p->ioport[p->nioport].len = len;
p->nioport++;
}
void
ioperm_revoke_all(struct proc *p)
{
p->nioport = 0;
// The bitmap is not touched here. This can run on a CPU that is not the one
// holding the process's ranges open, and the next switchuvm() on each CPU
// closes what that CPU opened -- which is the only place the two can be made
// to agree without an IPI.
}
void
ioperm_switch(struct proc *p)
{
int c = cpunum();
struct tssblk *b = &tssblk[c];
int i;
// Close whatever was open. Doing this unconditionally rather than diffing
// against the incoming process costs a few hundred bit clears on a machine
// where nobody uses ports at all -- which is `installed[c].n == 0`, and then
// this loop does nothing.
for (i = 0; i < installed[c].n; i++)
map_set(b->map, installed[c].r[i].base, installed[c].r[i].len, 0);
installed[c].n = 0;
if (!p || p->nioport == 0)
return;
for (i = 0; i < p->nioport && i < NIOPORT; i++) {
map_set(b->map, p->ioport[i].base, p->ioport[i].len, 1);
installed[c].r[i] = p->ioport[i];
}
installed[c].n = i;
}