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:
Nexoniarz 2026-09-14 15:20:07 +02:00
commit 071499a378
26 changed files with 2009 additions and 46 deletions

View file

@ -216,6 +216,57 @@ resolve(void *ctx, const char *name, unsigned long long *out)
return -1;
}
// --- per-CPU variables ---------------------------------------------------
//
// The stack guard was one of these; `current` is another, and every driver
// touches it. ko_fixup_percpu() turns each `%gs:` reference into a `lea` of
// memory we supply here, so a per-CPU variable becomes an ordinary one --
// which is what it is, on a host with one thread pretending to be one CPU.
//
// The blobs are zeroed and generously sized, because we do not know what the
// driver will read through them: `current` is a task_struct of some kilobytes,
// and a driver that walks into it should get zeroes rather than a fault. Same
// bargain as the logging stubs, and the same caveat -- see the memcpy note in
// docs/architecture/ldls.md.
#define PERCPU_SLOTS 8
#define PERCPU_SIZE (16 * 1024)
static char percpu_mem[PERCPU_SLOTS][PERCPU_SIZE];
static const char *percpu_name[PERCPU_SLOTS];
static int percpu_n;
static unsigned long long
percpu_for(void *ctx, const char *name)
{
int i;
(void)ctx;
for (i = 0; i < percpu_n; i++)
if (streq(percpu_name[i], name))
return (unsigned long long)(void *)percpu_mem[i];
if (percpu_n == PERCPU_SLOTS)
return 0;
i = percpu_n++;
percpu_name[i] = name;
// Zero is the right contents for a counter and for `cpu_number`, and the
// wrong contents for `current`: a driver reads that one and dereferences
// what it got. So the pointer-valued ones point at the rest of their own
// slot, which is zeroes all the way down -- every field the driver reads
// out of the task it thinks it has comes back 0, and anything it writes
// lands somewhere harmless.
if (streq(name, "const_current_task") || streq(name, "current_task")) {
*(unsigned long long *)percpu_mem[i] =
(unsigned long long)(void *)(percpu_mem[i] + 64);
puts_(" per-CPU "); puts_(name); puts_(" -- a task of zeroes\n");
} else {
puts_(" per-CPU "); puts_(name); puts_(" -- 16K of zeroes\n");
}
return (unsigned long long)(void *)percpu_mem[i];
}
static unsigned long long
find_symbol(struct ko *k, const char *want)
{
@ -263,6 +314,15 @@ wr64(unsigned long base, unsigned int off, unsigned long v)
// match is made against that rather than against anything LDLS knows -- which
// is the property that keeps this general: nothing here has heard of an
// rtl8139.
// Which device, when more than one matches.
//
// The first driver tested was an rtl8139 in a machine whose only other NIC was
// an e1000 the kernel was driving, so "the first that matches" was the same as
// "the right one". With two e1000s it is not: taking the first means taking
// the one the console's own network is on. Written as `bus:slot.func`, or -1
// for "any", which is still the default.
static int sel_bus = -1, sel_slot = -1, sel_func = -1;
static int
claim_for(unsigned long idtab, unsigned int id_size, unsigned int id_vendor,
unsigned int id_device, struct pciuser_info *out)
@ -279,6 +339,11 @@ claim_for(unsigned long idtab, unsigned int id_size, unsigned int id_vendor,
if (ioctl(kpi.pcifd, PCIIOC_INFO, &in) < 0)
continue;
if (sel_bus >= 0 && ((int)in.bus != sel_bus ||
(int)in.slot != sel_slot ||
(int)in.func != sel_func))
continue;
for (n = 0; n < 4096; n++) {
unsigned long e = idtab + (unsigned long)n * id_size;
unsigned int v = rd32(e, id_vendor), d = rd32(e, id_device);
@ -372,12 +437,35 @@ kpi_layout_from_btf(struct btf *b)
GET(nd, "real_num_tx_queues", kpi_l.nd_real_num_tx);
GET(nd, "state", kpi_l.nd_state);
GET(nd, "flags", kpi_l.nd_flags);
{
int dv = btf_find(b, "device", BTF_KIND_STRUCT);
int ko = btf_find(b, "kobject", BTF_KIND_STRUCT);
int pd = btf_find(b, "pci_dev", BTF_KIND_STRUCT);
if (dv && ko && pd) {
GET(pd, "dev", kpi_l.pd_dev);
GET(dv, "kobj", kpi_l.dev_kobj);
GET(ko, "name", kpi_l.kobj_name);
}
}
{
int nq = btf_find(b, "netdev_queue", BTF_KIND_STRUCT);
if (!nq) { puts_("ldlsd: no struct netdev_queue\n"); return -1; }
kpi_l.nq_size = btf_type_size(b, nq);
GET(nq, "dev", kpi_l.nq_dev);
}
{
long long v = 0;
if (btf_enum_value(b, "__LINK_STATE_NOCARRIER", &v) < 0) {
puts_("ldlsd: this BTF has no __LINK_STATE_NOCARRIER\n");
return -1;
}
kpi_l.link_nocarrier = (unsigned int)v;
}
{
int wk = btf_find(b, "work_struct", BTF_KIND_STRUCT);
if (!wk) { puts_("ldlsd: no struct work_struct\n"); return -1; }
GET(wk, "func", kpi_l.work_func);
}
GET(ops, "ndo_open", kpi_l.ndo_open);
GET(ops, "ndo_stop", kpi_l.ndo_stop);
GET(ops, "ndo_start_xmit", kpi_l.ndo_start_xmit);
@ -439,6 +527,10 @@ run_interface(struct btf *b, void *ndev)
d[kpi_l.nd_flags] |= 1; /* IFF_UP */
}
// Each phase gets its own first forty. A driver makes thousands of stub
// calls during probe, so without this the interesting ones -- the last
// thing it did before it stopped -- are always past the cap.
g_calls = 0;
puts_("\n calling ndo_open\n\n");
{
int (*op)(void *) = (int (*)(void *))open_fn;
@ -472,6 +564,18 @@ run_interface(struct btf *b, void *ndev)
}
}
// The driver's own handler, as if the line had been asserted. Its
// first act is to read the device's cause register, so calling it
// with nothing pending costs one register read and returns IRQ_NONE.
if (kpi_irq_poll())
moved = 1;
// Anything the driver asked to have done later. A watchdog belongs
// here and not in a timer interrupt: it wants to read registers and
// touch the rings, which is exactly what the rest of this loop does.
if (kpi_work_run() > 0)
moved = 1;
// And anything the chip has. The driver's own poll does the work; all
// this decides is when it gets to run.
if (kpi_napi_poll) {
@ -483,7 +587,9 @@ run_interface(struct btf *b, void *ndev)
if (++idle > 200) { sleep(1); idle = 0; }
} else {
idle = 0;
if (((tx + rx) % 64) == 0) {
// The first few, then every sixty-fourth. Bringing a driver up,
// the question is always whether the *first* frame moved.
if (tx + rx <= 4 || ((tx + rx) % 64) == 0) {
puts_(" ldls0: tx "); putn(tx);
puts_(" rx "); putn(rx); puts_("\n");
}
@ -582,6 +688,32 @@ drive_probe(const char *btfpath)
pdev = (unsigned long)getmem(dev_size);
if (!pdev) { puts_("ldlsd: no memory for a pci_dev\n"); return; }
// A name for the device.
//
// `pci_name(pdev)` is `pdev->dev.kobj.name`, and a driver copies it
// into its interface's name during probe. Left null it is a fault
// inside strnlen with nothing to say which field was missing.
if (kpi_l.pd_dev && kpi_l.kobj_name) {
static char nm[16];
unsigned long q;
int j = 0;
nm[j++] = '0'; nm[j++] = '0'; nm[j++] = '0'; nm[j++] = '0';
nm[j++] = ':';
nm[j++] = "0123456789abcdef"[(in.bus >> 4) & 0xf];
nm[j++] = "0123456789abcdef"[in.bus & 0xf];
nm[j++] = ':';
nm[j++] = "0123456789abcdef"[(in.slot >> 4) & 0xf];
nm[j++] = "0123456789abcdef"[in.slot & 0xf];
nm[j++] = '.';
nm[j++] = "0123456789abcdef"[in.func & 0xf];
nm[j] = 0;
q = (unsigned long)nm;
for (int i = 0; i < 8; i++)
((unsigned char *)pdev)[kpi_l.pd_dev + kpi_l.dev_kobj +
kpi_l.kobj_name + i] =
(unsigned char)(q >> (i * 8));
}
wr16(pdev, dev_vendor, in.vendor);
wr16(pdev, dev_device, in.device);
wr32(pdev, dev_devfn, (unsigned int)((in.slot << 3) | in.func));
@ -677,11 +809,34 @@ main(int argc, char *argv[])
int nsec, i;
if (argc < 2) {
puts_("usage: ldlsd <module.ko> [vmlinux-btf]\n");
puts_("usage: ldlsd <module.ko> [vmlinux-btf] [bus:slot.func]\n");
puts_(" neither file ships with SchOS; both are yours.\n");
exit();
}
// An optional bus address, to pick between devices that all match.
if (argc > 3) {
const char *a = argv[3];
int f[3] = {0, 0, 0}, got = 0, v = 0, any = 0;
for (;; a++) {
int d = -1;
if (*a >= '0' && *a <= '9') d = *a - '0';
else if (*a >= 'a' && *a <= 'f') d = *a - 'a' + 10;
else if (*a >= 'A' && *a <= 'F') d = *a - 'A' + 10;
if (d >= 0) { v = v * 16 + d; any = 1; continue; }
if (any && got < 3) f[got++] = v;
v = 0; any = 0;
if (*a == 0) break;
}
if (got != 3) {
puts_("ldlsd: a device is written bus:slot.func, as /dev/pci prints it\n");
exit();
}
sel_bus = f[0]; sel_slot = f[1]; sel_func = f[2];
puts_(" device only "); puthexn(sel_bus, 2); puts_(":");
puthexn(sel_slot, 2); puts_("."); puthexn(sel_func, 1); puts_("\n");
}
img = slurp(argv[1], &len);
if (!img) { puts_("ldlsd: cannot read "); puts_(argv[1]); puts_("\n"); exit(); }
@ -738,6 +893,16 @@ main(int argc, char *argv[])
if (g == 0) g = ko_defuse_gs(&k, "__stack_chk_guard");
if (g > 0) { puts_(" defused "); putn(g); puts_(" %gs stack-guard load(s)\n"); }
}
// Whatever %gs references are left name real per-CPU variables, and those
// cannot be defused -- the driver dereferences what it reads.
{
int p = ko_fixup_percpu(&k, percpu_for, 0);
if (p < 0) {
puts_("ldlsd: per-CPU: "); puts_(ko_err(&k)); puts_("\n");
exit();
}
if (p > 0) { puts_(" rehomed "); putn(p); puts_(" %gs per-CPU reference(s)\n"); }
}
puts_(" loaded "); putn(span); puts_(" bytes at "); puthex((unsigned long long)mem);
puts_("\n");