diff --git a/Makefile b/Makefile index 4d8274c..133ac31 100644 --- a/Makefile +++ b/Makefile @@ -926,6 +926,7 @@ crashqr: # ----------------------------------------------------------------------- LDLS_INSPECT = $(BUILD)/tools/ldls-inspect$(EXE) LDLS_RUN = $(BUILD)/tools/ldls-run$(EXE) +LDLS_TYPES = $(BUILD)/tools/ldls-types$(EXE) LDLS_REFMOD = $(BUILD)/ldls/refmod.o $(LDLS_INSPECT): ldls/host/inspect.c ldls/ko.c ldls/ko.h @@ -935,10 +936,11 @@ $(LDLS_INSPECT): ldls/host/inspect.c ldls/ko.c ldls/ko.h # L1. Calls a module's init with every symbol stubbed, to read off the call # order. -fno-strict-aliasing because this deliberately treats loaded bytes as # code, and no optimiser assumption about that is one we want. -$(LDLS_RUN): ldls/host/run.c ldls/ko.c ldls/stub.c ldls/ko.h ldls/stub.h +$(LDLS_RUN): ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c \ + ldls/ko.h ldls/stub.h ldls/btf.h @mkdir -p $(BUILD)/tools $(HOSTCC) $(HOSTWARN) -O1 -fno-strict-aliasing -o $@ \ - ldls/host/run.c ldls/ko.c ldls/stub.c + ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c # The test object, compiled and never linked. Ours, because docs/plan/ldls.md # rule 1 means no .ko is committed here -- see the header of refmod.c. @@ -950,11 +952,17 @@ $(LDLS_REFMOD): ldls/test/refmod.c @mkdir -p $(BUILD)/ldls $(HOSTCC) -c -O1 -fno-pic -fno-asynchronous-unwind-tables -o $@ $< -ldls: $(LDLS_INSPECT) $(LDLS_RUN) +# L2. Reads struct layouts out of a kernel's own BTF, so they are never in our +# source. See ldls/btf.h. +$(LDLS_TYPES): ldls/host/types.c ldls/btf.c ldls/btf.h + @mkdir -p $(BUILD)/tools + $(HOSTCC) $(HOSTWARN) -O1 -o $@ ldls/host/types.c ldls/btf.c + +ldls: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_TYPES) # Load our own object and check the loader got it right. No network, no QEMU, # no module from anywhere. -ldls-check: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_REFMOD) +ldls-check: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_TYPES) $(LDLS_REFMOD) $(PYTHON) tools/python/ldls_check.py $(LDLS_INSPECT) $(LDLS_REFMOD) $(LDLS_RUN) $(MKFS): tools/mkfs.c include/fs.h include/stat.h include/param.h diff --git a/docs/plan/1.0.md b/docs/plan/1.0.md index cfdf6e2..7c1fd27 100644 --- a/docs/plan/1.0.md +++ b/docs/plan/1.0.md @@ -95,7 +95,7 @@ The galling ones. The work exists, is tested, and nothing uses it. |---|---| | **`foundations.md`** | Item 1 built. Items 2 (a shareable memory object), 3 (`AF_UNIX` in the filesystem namespace), 4 and 5 open. **Wayland stops at item 2.** | | ~~**`hybrid.md`**~~ | **Closed.** All four mechanisms built; H1, H2 and H3 done. H4 — the USB eviction — is *withdrawn* rather than outstanding: USB is the bus peripherals arrive on, not a peripheral, and an eviction that has to leave an identical driver behind has moved nothing. See [hybrid.md](hybrid.md), "Decided: USB stays in the kernel". | -| **`ldls.md`** | L0 and L1 done — the loader handles 2,440 stock modules, and 449 of 500 run their `init_module` to completion against stubs. L2 (the hardware) and L3 (a packet) open. Every symbol is still a stub that logs and returns zero. | +| **`ldls.md`** | L0, L1 and L2 done — the loader handles 2,440 stock modules; 449 of 500 run `init_module`; and `8139too`'s `probe()` runs to completion and returns 0, reached through struct offsets read from the kernel's own BTF. L3 (a packet, and the `/dev/pci` back end) open. The hardware is still invented. | | **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. | | **`debugger.md`** | D0 through D4 built and described in [../architecture/debugging.md](../architecture/debugging.md). Only the network transport is open — a userspace daemon bridging TCP to `/dev/dbg`, which is deliberately last. | diff --git a/docs/plan/ldls.md b/docs/plan/ldls.md index 345fa49..d09477a 100644 --- a/docs/plan/ldls.md +++ b/docs/plan/ldls.md @@ -180,7 +180,7 @@ One device, one process, claimed and released, on the `/dev/pci` discipline. |---|---|---| | **L0** | **The loader.** Parse a real `.ko`, lay it out, relocate it, and **print every unresolved symbol**. Nothing executes. The output is the measured API surface. | **done** — `ldls/ko.c`, `make ldls-check` | | **L1** | **Stubs.** Every symbol resolves, to a function that logs its name. `init_module` runs, which yields the *call order* — the second thing you need and the other thing you cannot guess. | **done** — `ldls/stub.c`, `ldls-run` | -| **L2** | **The hardware.** `probe()` receives a working `struct pci_dev`, maps its BARs, allocates coherent DMA and registers an interrupt, all through `/dev/pci`. The driver reaches its device. | | +| **L2** | **The hardware.** `probe()` receives a working `struct pci_dev`, maps its BARs, allocates coherent DMA and registers an interrupt, all through `/dev/pci`. The driver reaches its device. | **done**, except `/dev/pci` — see below | | **L3** | **A packet.** The process-backed netdev, wired to the driver's transmit and receive paths. **`ping` over a Linux driver.** This is the milestone that proves the whole idea and the one that cannot be skipped. | | | **L4** | **Kill it.** The host process dies, the kernel clears bus mastering and reclaims the DMA, a new host starts and the interface comes back. | free — H2 built all of this | @@ -373,6 +373,99 @@ the kernel, not the module, is supposed to call. Reaching that is L2. --- +## L2, and being the PCI core + +In Linux, `__pci_register_driver` ends at the PCI core, which matches the +driver's `id_table` against the bus and calls `probe()`. There is no PCI core +here, so LDLS keeps the pointer that call was given and plays the part itself. + +To do that it has to know where `pci_driver.probe` is — and **it asks the +kernel rather than being told by us**. `ldls/btf.c` reads +`/sys/kernel/btf/vmlinux`: 122,279 types, every struct layout and every +function prototype, as generated data describing the kernel that is installed. + +``` +$ ldls-types /sys/kernel/btf/vmlinux --need + pci_driver probe offset 16 size 8 + pci_driver id_table offset 8 size 8 + pci_dev vendor offset 60 size 2 + pci_dev resource offset 1000 size 1088 + resource start offset 0 size 8 + ... + all present +``` + +Cross-checked against `pahole` on the same file, field for field. **No offset +in this is ours**, which is rule 3's uncomfortable row disappearing rather than +being argued about. + +### What the driver then did + +``` + pci_driver at 0x41662400, from BTF: probe +16, id_table +8 + driver name "8139too" + + devices it claims: + 10ec:8139 class 000000/000000 + ... and 16 more + + calling probe(pci_dev 10ec:8139, ...) + + 1 alloc_etherdev_mqs(0x368, 0x1, 0x1) + 2 pci_enable_device(...) + 3 pci_request_regions(...) + 4 pci_set_master(...) + 5 pci_iomap(pdev, 1, 0) + 6..544 ioread8 / iowrite8 -- 0x88, 0x8a, 0x8c, 0x8e, over and over + 545 dev_addr_mod(...) + 547 netif_napi_add_weight_locked(...) + 549 timer_init_key(...) + 550 register_netdev(...) + 551 netdev_info(...) + | %s at 0x%p, %pM, IRQ %d + + probe returned 0 after 552 stub call(s) +``` + +`10ec:8139` is the real RTL8139 id. The driver mapped **BAR 1**, which is the +one we marked `IORESOURCE_MEM`. And those 538 alternating byte accesses with +values stepping `0x88, 0x8a, 0x8c, 0x8e` are **serial EEPROM clocking** — the +driver bit-banging the chip to read its MAC address, then handing the result to +`dev_addr_mod`. It registered a NAPI instance, started a watchdog timer, +registered the interface, printed its "found it" line, and **returned 0**. + +That is a complete driver bring-up, on shape alone. + +### Fifteen symbols + +The whole of that probe touches **15 distinct symbols**, and 538 of the 552 +calls are two of them: + +| | | +|---|---| +| `ioread8` / `iowrite8` / `ioread32` | 540 calls, 3 symbols | +| PCI setup | `pci_enable_device`, `pci_request_regions`, `pci_set_master`, `pci_iomap` | +| net | `alloc_etherdev_mqs`, `dev_addr_mod`, `netif_napi_add_weight_locked`, `register_netdev`, `netdev_info` | +| other | `mutex_lock`, `mutex_unlock`, `timer_init_key` | + +L1 said 44 symbols carry 110 modules through init. L2 says 15 carry this one +through probe. **Neither is a number anyone would have guessed**, and both came +out of running the thing rather than reading about it. + +### What is not done + +`probe()` receives a `pci_dev` whose *shape* is right and whose *contents are +invented*. The BARs are numbers this harness made up; `pci_iomap` returns a +zeroed page, not a mapping; nothing has touched hardware and nothing could, +because this runs on the build machine. + +**The half of L2 that says "all through `/dev/pci`" belongs to the SchOS +side**, where those calls have somewhere real to land. What is settled is the +part that had to come first: that probe can be reached at all, with a struct +the driver accepts, built from offsets nobody wrote down. + +--- + ## What it will not do - **It is not a security boundary, and here that is worse than usual.** There @@ -418,6 +511,15 @@ the kernel, not the module, is supposed to call. Reaching that is L2. - **`ldls-run` is a development harness, not the deliverable.** It runs on the build machine so that loader failures cost a second instead of a boot. The thing that ships is the SchOS-side host. +- **BTF has to come from somewhere.** On Linux it is `/sys/kernel/btf/vmlinux`. + SchOS has no such file and never will — so the user brings the BTF along with + the module, from the kernel the module was built for. Both are theirs, + neither is shipped, and a kernel built without `CONFIG_DEBUG_INFO_BTF` has + neither. +- **`probe()` is reached with invented hardware.** The BARs are numbers the + harness made up and `pci_iomap` returns a zeroed page. A driver that reads a + status register gets whatever was there, which is why 8139too read a MAC + address of zeroes and was perfectly happy about it. - **It will not support "Linux drivers".** It will support *one driver, on one kernel version, that has been tried*. Anything beyond that is a claim, and the list of what has actually been run belongs in the architecture document diff --git a/ldls/btf.c b/ldls/btf.c new file mode 100644 index 0000000..8e3b868 --- /dev/null +++ b/ldls/btf.c @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Reading BTF. See ldls/btf.h. +// +// Implemented from Documentation/bpf/btf.rst. The format is small enough to +// hold in your head: a header, a run of variable-length type records, and a +// string table. The only fiddly part is that a record's length depends on its +// kind, so iterating means knowing how much trails each one -- which is the +// table in trailing_bytes() below and nothing more. + +#include "btf.h" + +static void +btf_fail(struct btf *b, const char *msg, const char *what) +{ + int i = 0, j = 0; + while (msg[i] && i < (int)sizeof(b->err) - 1) { b->err[i] = msg[i]; i++; } + if (what) + while (what[j] && i < (int)sizeof(b->err) - 1) b->err[i++] = what[j++]; + b->err[i] = 0; +} + +static int +btf_streq(const char *a, const char *c) +{ + while (*a && *a == *c) { a++; c++; } + return *a == 0 && *c == 0; +} + +const char *btf_err(const struct btf *b) { return b->err[0] ? b->err : "no error"; } + +// How many bytes follow a type record, given its kind and vlen. +static unsigned int +trailing_bytes(unsigned int info) +{ + unsigned int kind = BTF_INFO_KIND(info); + unsigned int vlen = BTF_INFO_VLEN(info); + + switch (kind) { + case BTF_KIND_INT: return 4; + case BTF_KIND_ARRAY: return 12; /* type, index_type, nelems */ + case BTF_KIND_STRUCT: + case BTF_KIND_UNION: return vlen * 12; /* struct btf_member */ + case BTF_KIND_ENUM: return vlen * 8; + case BTF_KIND_ENUM64: return vlen * 12; + case BTF_KIND_FUNC_PROTO: return vlen * 8; /* struct btf_param */ + case BTF_KIND_VAR: return 4; + case BTF_KIND_DATASEC: return vlen * 12; + case BTF_KIND_DECL_TAG: return 4; + default: return 0; /* PTR, TYPEDEF, CONST, ... */ + } +} + +long +btf_count(const void *image, unsigned long long len) +{ + const struct btf_header *h = (const struct btf_header *)image; + const unsigned char *types; + unsigned int at = 0; + long n = 0; + + if (len < sizeof(struct btf_header)) + return -1; + if (h->magic != BTF_MAGIC || h->version != 1) + return -1; + if ((unsigned long long)h->hdr_len + h->type_off + h->type_len > len) + return -1; + + types = (const unsigned char *)image + h->hdr_len + h->type_off; + + while (at + sizeof(struct btf_type) <= h->type_len) { + const struct btf_type *t = (const struct btf_type *)(types + at); + unsigned int step = sizeof(struct btf_type) + trailing_bytes(t->info); + if (step == 0 || at + step > h->type_len) + break; + at += step; + n++; + if (n > BTF_MAXTYPES) + return -1; + } + return n; +} + +int +btf_open(struct btf *b, const void *image, unsigned long long len, + unsigned int *off, unsigned int noff) +{ + const struct btf_header *h = (const struct btf_header *)image; + unsigned int at = 0, id = 1; + int i; + + for (i = 0; i < (int)sizeof(b->err); i++) + b->err[i] = 0; + b->img = (const unsigned char *)image; + b->len = len; + b->off = off; + b->ntypes = 0; + + if (len < sizeof(struct btf_header)) + { btf_fail(b, "too short to be BTF", 0); return -1; } + if (h->magic != BTF_MAGIC) + { btf_fail(b, "not BTF: wrong magic", 0); return -1; } + if (h->version != 1) + { btf_fail(b, "BTF version is not 1", 0); return -1; } + if ((unsigned long long)h->hdr_len + h->type_off + h->type_len > len || + (unsigned long long)h->hdr_len + h->str_off + h->str_len > len) + { btf_fail(b, "BTF sections run off the end of the file", 0); return -1; } + + b->h = h; + b->types = b->img + h->hdr_len + h->type_off; + b->types_len = h->type_len; + b->str = (const char *)(b->img + h->hdr_len + h->str_off); + b->str_len = h->str_len; + + // Index every type. Ids are assigned in order starting at 1; id 0 is void + // and is not stored. + while (at + sizeof(struct btf_type) <= b->types_len) { + const struct btf_type *t = (const struct btf_type *)(b->types + at); + unsigned int step = sizeof(struct btf_type) + trailing_bytes(t->info); + + if (at + step > b->types_len) + break; + if (id >= noff) + { btf_fail(b, "the caller's type index is too small", 0); return -1; } + off[id++] = at; + at += step; + } + b->ntypes = id - 1; + return 0; +} + +static const struct btf_type * +btf_at(struct btf *b, int id) +{ + if (id <= 0 || (unsigned int)id > b->ntypes) + return 0; + return (const struct btf_type *)(b->types + b->off[id]); +} + +const char * +btf_name_of(struct btf *b, int id) +{ + const struct btf_type *t = btf_at(b, id); + if (!t || t->name_off >= b->str_len) + return ""; + return b->str + t->name_off; +} + +int +btf_kind_of(struct btf *b, int id) +{ + const struct btf_type *t = btf_at(b, id); + return t ? (int)BTF_INFO_KIND(t->info) : -1; +} + +int +btf_find(struct btf *b, const char *name, int kind) +{ + unsigned int id; + + for (id = 1; id <= b->ntypes; id++) { + const struct btf_type *t = (const struct btf_type *)(b->types + b->off[id]); + if ((int)BTF_INFO_KIND(t->info) != kind) + continue; + if (t->name_off >= b->str_len) + continue; + if (btf_streq(b->str + t->name_off, name)) + return (int)id; + } + return 0; +} + +// Follow the things that wrap a type without changing its layout. +static int +btf_strip(struct btf *b, int id) +{ + int guard = 0; + + while (id > 0 && guard++ < 32) { + const struct btf_type *t = btf_at(b, id); + if (!t) + return 0; + switch (BTF_INFO_KIND(t->info)) { + case BTF_KIND_TYPEDEF: + case BTF_KIND_CONST: + case BTF_KIND_VOLATILE: + case BTF_KIND_RESTRICT: + case BTF_KIND_TYPE_TAG: + id = (int)t->u.type; + continue; + default: + return id; + } + } + return 0; +} + +unsigned int +btf_type_size(struct btf *b, int id) +{ + const struct btf_type *t; + + id = btf_strip(b, id); + t = btf_at(b, id); + if (!t) + return 0; + + switch (BTF_INFO_KIND(t->info)) { + case BTF_KIND_INT: + case BTF_KIND_STRUCT: + case BTF_KIND_UNION: + case BTF_KIND_ENUM: + case BTF_KIND_ENUM64: + case BTF_KIND_FLOAT: + return t->u.size; + case BTF_KIND_PTR: + return 8; // x86-64; this reader is built for one ABI + case BTF_KIND_ARRAY: { + const unsigned int *a = (const unsigned int *)(t + 1); + return btf_type_size(b, (int)a[0]) * a[2]; + } + default: + return 0; + } +} + +int +btf_member_off(struct btf *b, int struct_id, const char *member, + unsigned int *off_bytes, unsigned int *size_bytes) +{ + const struct btf_type *t; + const struct btf_member *m; + unsigned int i, vlen, kflag; + + struct_id = btf_strip(b, struct_id); + t = btf_at(b, struct_id); + if (!t) + { btf_fail(b, "no such type", 0); return -1; } + if (BTF_INFO_KIND(t->info) != BTF_KIND_STRUCT && + BTF_INFO_KIND(t->info) != BTF_KIND_UNION) + { btf_fail(b, "not a struct or union", 0); return -1; } + + vlen = BTF_INFO_VLEN(t->info); + kflag = BTF_INFO_KFLAG(t->info); + m = (const struct btf_member *)(t + 1); + + for (i = 0; i < vlen; i++, m++) { + const char *nm = (m->name_off < b->str_len) ? b->str + m->name_off : ""; + unsigned int bit; + + // An unnamed member is an anonymous struct or union, and what looks like + // a field of the outer type is really a field of that. Recurse, adding + // its own offset -- without this, every kernel struct with an anonymous + // union in it reports its later fields as missing. + if (nm[0] == 0) { + unsigned int sub_off, sub_size; + unsigned int here = kflag ? (m->offset & 0xffffff) : m->offset; + if (btf_member_off(b, (int)m->type, member, &sub_off, &sub_size) == 0) { + *off_bytes = here / 8 + sub_off; + *size_bytes = sub_size; + return 0; + } + continue; + } + + if (!btf_streq(nm, member)) + continue; + + if (kflag && (m->offset >> 24) != 0) + { btf_fail(b, "that member is a bitfield: ", member); return -1; } + + bit = kflag ? (m->offset & 0xffffff) : m->offset; + if (bit % 8) + { btf_fail(b, "that member is not byte-aligned: ", member); return -1; } + + *off_bytes = bit / 8; + *size_bytes = btf_type_size(b, (int)m->type); + return 0; + } + + btf_fail(b, "no such member: ", member); + return -1; +} diff --git a/ldls/btf.h b/ldls/btf.h new file mode 100644 index 0000000..fd3441c --- /dev/null +++ b/ldls/btf.h @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Reading BTF: struct layouts and function prototypes, from the kernel that +// the driver was built for. +// +// docs/plan/ldls.md milestone L2, and the thing that makes rule 3 comfortable. +// The shim has to agree with a compiled driver about where `pci_driver.probe` +// sits, and a compiled driver is not negotiable about it. Writing those +// offsets into our source would mean a field-for-field match with Linux's +// headers, which *looks* like transcription however it was arrived at. +// +// It does not have to be in our source. A kernel built with +// CONFIG_DEBUG_INFO_BTF publishes `/sys/kernel/btf/vmlinux`: every struct +// layout and every function prototype, as generated binary type data +// describing the kernel that is installed. LDLS reads it at load time, on the +// user's machine, from the user's own kernel. +// +// Two things follow. The offsets are never ours to have copied, and they are +// right for the kernel actually present rather than one we pinned by guessing +// -- which is most of what rule 4 was worried about. +// +// The format is described in Documentation/bpf/btf.rst, which is a published +// specification; implementing it from that is the case ../docs/licensing.md §5 +// covers. Freestanding, like the rest of ldls/. + +// --- the format ------------------------------------------------------------- + +#define BTF_MAGIC 0xeB9F + +struct btf_header { + unsigned short magic; + unsigned char version; + unsigned char flags; + unsigned int hdr_len; + // Both offsets are from the end of the header, not from the start of it. + unsigned int type_off; + unsigned int type_len; + unsigned int str_off; + unsigned int str_len; +}; + +struct btf_type { + unsigned int name_off; + // vlen in bits 0-15, kind in bits 24-28, kind_flag in bit 31. + unsigned int info; + union { + unsigned int size; // STRUCT, UNION, ENUM, INT, DATASEC, FLOAT + unsigned int type; // PTR, TYPEDEF, CONST, FUNC, VAR, ... + } u; +}; + +struct btf_member { + unsigned int name_off; + unsigned int type; + // With kind_flag set this is a bitfield: size in bits 24-31, bit offset in + // bits 0-23. Without it, a plain bit offset. + unsigned int offset; +}; + +struct btf_param { + unsigned int name_off; + unsigned int type; +}; + +#define BTF_KIND_INT 1 +#define BTF_KIND_PTR 2 +#define BTF_KIND_ARRAY 3 +#define BTF_KIND_STRUCT 4 +#define BTF_KIND_UNION 5 +#define BTF_KIND_ENUM 6 +#define BTF_KIND_FWD 7 +#define BTF_KIND_TYPEDEF 8 +#define BTF_KIND_VOLATILE 9 +#define BTF_KIND_CONST 10 +#define BTF_KIND_RESTRICT 11 +#define BTF_KIND_FUNC 12 +#define BTF_KIND_FUNC_PROTO 13 +#define BTF_KIND_VAR 14 +#define BTF_KIND_DATASEC 15 +#define BTF_KIND_FLOAT 16 +#define BTF_KIND_DECL_TAG 17 +#define BTF_KIND_TYPE_TAG 18 +#define BTF_KIND_ENUM64 19 + +#define BTF_INFO_KIND(i) (((i) >> 24) & 0x1f) +#define BTF_INFO_VLEN(i) ((i) & 0xffff) +#define BTF_INFO_KFLAG(i) (((i) >> 31) & 1) + +// --- the reader --------------------------------------------------------------- + +#define BTF_MAXTYPES 262144 // type ids we will index; vmlinux is ~150k + +struct btf { + const unsigned char *img; + unsigned long long len; + const struct btf_header *h; + const unsigned char *types; // start of the type section + unsigned int types_len; + const char *str; + unsigned int str_len; + + // Type id -> byte offset into the type section. Id 0 is void and has no + // entry, so this is 1-based and index 0 is unused. + unsigned int *off; // the caller's, ntypes + 1 entries + unsigned int ntypes; + + char err[128]; +}; + +// How many type ids this BTF holds, so the caller can size the index. -1 if +// the image is not BTF at all. +long btf_count(const void *image, unsigned long long len); + +// Index it. `off` must have btf_count()+1 entries. +int btf_open(struct btf *b, const void *image, unsigned long long len, + unsigned int *off, unsigned int noff); + +// The type id of a named type of a given kind, or 0. +int btf_find(struct btf *b, const char *name, int kind); + +// Byte offset and size of a named member of a struct or union. +// +// Returns 0 on success. Bitfields are refused rather than rounded: a driver +// that reads one needs the bit position, and a byte offset would be a +// plausible-looking wrong answer. +int btf_member_off(struct btf *b, int struct_id, const char *member, + unsigned int *off_bytes, unsigned int *size_bytes); + +// Size in bytes of a type, following typedefs and qualifiers. 0 if unknown. +unsigned int btf_type_size(struct btf *b, int id); + +const char *btf_name_of(struct btf *b, int id); +int btf_kind_of(struct btf *b, int id); +const char *btf_err(const struct btf *b); diff --git a/ldls/host/run.c b/ldls/host/run.c index 481b380..27ea0ea 100644 --- a/ldls/host/run.c +++ b/ldls/host/run.c @@ -30,6 +30,7 @@ #include "../ko.h" #include "../stub.h" +#include "../btf.h" // --- what the stubs report --------------------------------------------------- @@ -38,6 +39,21 @@ static int g_count; static int g_calls; static int g_verbose; +// What init_module handed to __pci_register_driver. +// +// In Linux that call ends at the PCI core, which matches the driver's id_table +// against the bus and calls probe(). There is no PCI core here, so LDLS keeps +// the pointer and plays that part itself -- which is the whole of L2. +static unsigned long g_pci_driver; +static int g_answer; // give allocators real memory + +static int +is_register_driver(const char *n) +{ + return strcmp(n, "__pci_register_driver") == 0 || + strcmp(n, "pci_register_driver") == 0; +} + // A handful of symbols are worth actually implementing rather than logging, // because what they do is print -- and a driver printing its own banner // through our code is the clearest evidence there is that the load worked. @@ -80,6 +96,40 @@ printk_like(const char *fmt, unsigned long *args) printf("\n"); } +// The smallest set of answers that lets probe() get past its first allocation. +// +// **This is not the shim.** It is six answers chosen to stop the driver +// bailing out at the first call so that the *rest* of its sequence becomes +// visible -- the same reason L1's stubs return zero instead of stopping. +// Implementing these properly, against /dev/pci, is L3. +// +// sizeof(struct net_device) comes from BTF like everything else: a driver +// reaches its private area with netdev_priv(), which is the net_device +// rounded up to NETDEV_ALIGN, so getting it wrong hands the driver a pointer +// into the middle of its own control block. +static unsigned int g_netdev_size; + +static unsigned long +answer(const char *name, unsigned long *args) +{ + if (strcmp(name, "alloc_etherdev_mqs") == 0) { + unsigned long n = (g_netdev_size + 31u) & ~31u; + return (unsigned long)calloc(1, n + args[0] + 64); + } + if (strcmp(name, "pci_iomap") == 0 || + strcmp(name, "pci_ioremap_bar") == 0 || + strcmp(name, "ioremap") == 0) + return (unsigned long)calloc(1, 4096); + if (strcmp(name, "__kmem_cache_create_args") == 0 || + strcmp(name, "kmem_cache_create") == 0) + return (unsigned long)calloc(1, 256); + if (strcmp(name, "dma_alloc_attrs") == 0) + return (unsigned long)calloc(1, args[1] ? args[1] : 4096); + // Success is zero for most of the kernel's int-returning calls, which is + // already what a stub returns -- so nothing is listed for them here. + return 0; +} + unsigned long ldls_stub(long idx, unsigned long *args) { @@ -93,6 +143,17 @@ ldls_stub(long idx, unsigned long *args) if (is_printk(name)) printk_like((const char *)args[0], args); + // netdev_info(dev, fmt, ...) and dev_info(dev, fmt, ...) put the format in + // the *second* argument. Worth handling for the same reason as _printk: the + // line a driver prints when it has found its hardware is the one that says + // whether it really did. + if (strcmp(name, "netdev_info") == 0 || strcmp(name, "netdev_err") == 0 || + strcmp(name, "_dev_info") == 0 || strcmp(name, "_dev_err") == 0) + printk_like((const char *)args[1], args + 1); + + if (is_register_driver(name) && !g_pci_driver) + g_pci_driver = args[0]; + if (g_verbose) printf(" args: %#lx %#lx %#lx %#lx %#lx %#lx\n", args[0], args[1], args[2], args[3], args[4], args[5]); @@ -100,7 +161,7 @@ ldls_stub(long idx, unsigned long *args) // Zero, so the driver carries on and shows the next call. Stopping at the // first tells you one name; continuing tells you the sequence, which is what // this milestone is for. - return 0; + return g_answer ? answer(name, args) : 0; } // --- surviving the module ----------------------------------------------------- @@ -115,6 +176,98 @@ on_fault(int sig) siglongjmp(g_jump, 1); } +// --- playing the PCI core ----------------------------------------------------- +// +// Every offset here is read from the kernel's own BTF rather than written +// down. That is the point of doing it this way: LDLS never claims to know +// where `pci_driver.probe` is, it asks the kernel that the driver was built +// for. See ldls/btf.h. + +struct pcilayout { + unsigned int drv_probe, drv_idtable, drv_name; + unsigned int id_size, id_vendor, id_device, id_class, id_class_mask; + unsigned int dev_size, dev_vendor, dev_device, dev_devfn, dev_irq, + dev_resource; + unsigned int res_size, res_start, res_end, res_flags; +}; + +static int +lay(struct btf *b, const char *type, const char *member, unsigned int *out) +{ + unsigned int off, size; + int id = btf_find(b, type, BTF_KIND_STRUCT); + + if (!id) { + fprintf(stderr, "ldls-run: this BTF has no struct %s\n", type); + return -1; + } + if (!member) { *out = btf_type_size(b, id); return 0; } + if (btf_member_off(b, id, member, &off, &size) < 0) { + fprintf(stderr, "ldls-run: %s.%s: %s\n", type, member, btf_err(b)); + return -1; + } + *out = off; + return 0; +} + +static int +pci_layout(struct btf *b, struct pcilayout *L) +{ + return lay(b, "pci_driver", "probe", &L->drv_probe) || + lay(b, "pci_driver", "id_table", &L->drv_idtable) || + lay(b, "pci_driver", "name", &L->drv_name) || + lay(b, "pci_device_id", 0, &L->id_size) || + lay(b, "pci_device_id", "vendor", &L->id_vendor) || + lay(b, "pci_device_id", "device", &L->id_device) || + lay(b, "pci_device_id", "class", &L->id_class) || + lay(b, "pci_device_id", "class_mask", &L->id_class_mask) || + lay(b, "pci_dev", 0, &L->dev_size) || + lay(b, "pci_dev", "vendor", &L->dev_vendor) || + lay(b, "pci_dev", "device", &L->dev_device) || + lay(b, "pci_dev", "devfn", &L->dev_devfn) || + lay(b, "pci_dev", "irq", &L->dev_irq) || + lay(b, "pci_dev", "resource", &L->dev_resource)|| + lay(b, "resource", 0, &L->res_size) || + lay(b, "resource", "start", &L->res_start) || + lay(b, "resource", "end", &L->res_end) || + lay(b, "resource", "flags", &L->res_flags) ? -1 : 0; +} + +static unsigned int +rd32(unsigned long base, unsigned int off) +{ + unsigned int v; + memcpy(&v, (void *)(base + off), 4); + return v; +} + +static unsigned long +rd64(unsigned long base, unsigned int off) +{ + unsigned long v; + memcpy(&v, (void *)(base + off), 8); + return v; +} + +static void +wr16(unsigned long base, unsigned int off, unsigned short v) +{ memcpy((void *)(base + off), &v, 2); } + +static void +wr32(unsigned long base, unsigned int off, unsigned int v) +{ memcpy((void *)(base + off), &v, 4); } + +static void +wr64(unsigned long base, unsigned int off, unsigned long v) +{ memcpy((void *)(base + off), &v, 8); } + +// The PCI resource flag bits. These are not layout and BTF does not carry +// them -- they are #defines. They are ABI between the kernel and every driver +// ever written against it, which makes them facts in the sense of +// docs/plan/ldls.md rule 3, and there are two of them. +#define LDLS_IORESOURCE_IO 0x00000100 +#define LDLS_IORESOURCE_MEM 0x00000200 + // --- loading ------------------------------------------------------------------ static unsigned char * @@ -185,10 +338,16 @@ main(int argc, char **argv) unsigned char *img, *mem, *code; unsigned long long len, span, entry; int nsec, i, rv = 0; + const char *btfpath = 0; if (argc > 1 && strcmp(argv[1], "-v") == 0) { g_verbose = 1; argv++; argc--; } + if (argc > 1 && strcmp(argv[1], "-a") == 0) { g_answer = 1; argv++; argc--; } + if (argc > 2 && strcmp(argv[1], "--btf") == 0) { + btfpath = argv[2]; argv += 2; argc -= 2; + } if (argc < 2) { - fprintf(stderr, "usage: ldls-run [-v] \n"); + fprintf(stderr, + "usage: ldls-run [-v] [-a] [--btf ] \n"); return 2; } @@ -292,9 +451,117 @@ main(int argc, char **argv) printf("\n stopped by signal %d after %d stub call(s)\n", g_signal, g_calls); printf(" that is a result, not a crash: the driver got that far before it\n" " needed one of those stubs to have done something real\n"); - rv = 0; + goto done; } + // --- L2: be the PCI core ------------------------------------------------- + if (!btfpath) + goto done; + if (!g_pci_driver) { + printf("\n no pci_driver was registered -- nothing to probe\n"); + goto done; + } + + { + struct btf b; + struct pcilayout L; + unsigned char *bimg; + unsigned long long blen; + unsigned int *boff; + long nt; + unsigned long idtab, pdev; + int nid = 0; + + bimg = slurp(btfpath, &blen); + if (!bimg) { fprintf(stderr, "ldls-run: cannot read %s\n", btfpath); goto done; } + nt = btf_count(bimg, blen); + if (nt < 0) { fprintf(stderr, "ldls-run: %s is not BTF\n", btfpath); goto done; } + boff = calloc((size_t)nt + 2, sizeof(unsigned int)); + if (!boff || btf_open(&b, bimg, blen, boff, (unsigned int)nt + 2) < 0) { + fprintf(stderr, "ldls-run: %s\n", btf_err(&b)); + goto done; + } + if (pci_layout(&b, &L) < 0) + goto done; + { + int nd = btf_find(&b, "net_device", BTF_KIND_STRUCT); + g_netdev_size = nd ? btf_type_size(&b, nd) : 4096; + } + + printf("\n pci_driver at %#lx, from BTF: probe +%u, id_table +%u\n", + g_pci_driver, L.drv_probe, L.drv_idtable); + { + const char *dn = (const char *)rd64(g_pci_driver, L.drv_name); + if (dn) printf(" driver name \"%s\"\n", dn); + } + + idtab = rd64(g_pci_driver, L.drv_idtable); + if (!idtab) { printf(" it has no id_table\n"); goto done; } + + printf("\n devices it claims:\n"); + for (;;) { + unsigned long e = idtab + (unsigned long)nid * L.id_size; + unsigned int v = rd32(e, L.id_vendor), d = rd32(e, L.id_device); + unsigned int c = rd32(e, L.id_class); + if (!v && !d && !c) break; + if (nid < 8) + printf(" %04x:%04x class %06x/%06x\n", v, d, c, + rd32(e, L.id_class_mask)); + nid++; + if (nid > 4096) break; + } + if (nid > 8) printf(" ... and %d more\n", nid - 8); + if (!nid) { printf(" (none)\n"); goto done; } + + // A device to hand it. Zeroed, then the few fields a driver reads before + // it has called anything: what it is, where it lives, and its BARs. + // + // The BAR values are invented. The point of L2 is that probe() is reached + // with a pci_dev whose *shape* is right; making the values real is L3's + // job and belongs to /dev/pci rather than to a fake. + pdev = (unsigned long)calloc(1, L.dev_size); + if (!pdev) goto done; + { + unsigned long e = idtab; /* the first entry it claims */ + unsigned long r0 = pdev + L.dev_resource; + unsigned long r1 = r0 + L.res_size; + + wr16(pdev, L.dev_vendor, (unsigned short)rd32(e, L.id_vendor)); + wr16(pdev, L.dev_device, (unsigned short)rd32(e, L.id_device)); + wr32(pdev, L.dev_devfn, 0x18); /* 03.0 */ + wr32(pdev, L.dev_irq, 11); + + wr64(r0, L.res_start, 0xc000); + wr64(r0, L.res_end, 0xc0ff); + wr64(r0, L.res_flags, LDLS_IORESOURCE_IO); + wr64(r1, L.res_start, 0xfebc0000); + wr64(r1, L.res_end, 0xfebc00ff); + wr64(r1, L.res_flags, LDLS_IORESOURCE_MEM); + + printf("\n calling probe(pci_dev %04x:%04x at %#lx, id %#lx)\n\n", + rd32(e, L.id_vendor), rd32(e, L.id_device), pdev, idtab); + + g_calls = 0; + if (sigsetjmp(g_jump, 1) == 0) { + int (*probe)(void *, void *) = + (int (*)(void *, void *))(void *)rd64(g_pci_driver, L.drv_probe); + if (!probe) { printf(" the driver has no probe\n"); goto done; } + { + int r = probe((void *)pdev, (void *)idtab); + printf("\n probe returned %d after %d stub call(s)\n", r, g_calls); + } + } else { + printf("\n probe stopped by signal %d after %d stub call(s)\n", + g_signal, g_calls); + printf(" which is how far a driver gets before one of those stubs\n" + " has to do something real. That list is L3's work queue.\n"); + } + } + } + +done: + ; + free(sec); free(img); return rv; diff --git a/ldls/host/types.c b/ldls/host/types.c new file mode 100644 index 0000000..5981c7d --- /dev/null +++ b/ldls/host/types.c @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// ldls-types -- ask a kernel's BTF where its fields are. +// +// docs/plan/ldls.md milestone L2. This is the tool that proves the layouts LDLS +// needs can be read from the kernel the user already has, rather than written +// into our source from theirs. +// +// ldls-types /sys/kernel/btf/vmlinux pci_driver probe id_table +// ldls-types /sys/kernel/btf/vmlinux --need +// +// `--need` prints every offset the PCI path needs at once, which is both a +// test and the thing L2's driver code consumes. + +#include +#include +#include + +#include "../btf.h" + +static unsigned char * +slurp(const char *path, unsigned long long *len) +{ + FILE *f = fopen(path, "rb"); + unsigned char *buf; + long n; + + if (!f) return 0; + if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 0; } + n = ftell(f); + if (n <= 0) { fclose(f); return 0; } + rewind(f); + buf = malloc((size_t)n); + if (!buf) { fclose(f); return 0; } + if (fread(buf, 1, (size_t)n, f) != (size_t)n) { free(buf); fclose(f); return 0; } + fclose(f); + *len = (unsigned long long)n; + return buf; +} + +static int +show(struct btf *b, const char *type, const char *member) +{ + unsigned int off, size; + int id = btf_find(b, type, BTF_KIND_STRUCT); + + if (!id) + id = btf_find(b, type, BTF_KIND_UNION); + if (!id) { + printf(" %-22s %-22s (no such struct)\n", type, member ? member : ""); + return 1; + } + if (!member) { + printf(" %-22s %-22s sizeof = %u\n", type, "", btf_type_size(b, id)); + return 0; + } + if (btf_member_off(b, id, member, &off, &size) < 0) { + printf(" %-22s %-22s %s\n", type, member, btf_err(b)); + return 1; + } + printf(" %-22s %-22s offset %5u size %u\n", type, member, off, size); + return 0; +} + +// Every layout the PCI path needs to get from init_module to probe(). +static const struct { const char *type, *member; } needed[] = { + { "pci_driver", 0 }, + { "pci_driver", "name" }, + { "pci_driver", "id_table" }, + { "pci_driver", "probe" }, + { "pci_driver", "remove" }, + { "pci_device_id", 0 }, + { "pci_device_id", "vendor" }, + { "pci_device_id", "device" }, + { "pci_device_id", "subvendor" }, + { "pci_device_id", "subdevice" }, + { "pci_device_id", "class" }, + { "pci_device_id", "class_mask" }, + { "pci_device_id", "driver_data"}, + { "pci_dev", 0 }, + { "pci_dev", "vendor" }, + { "pci_dev", "device" }, + { "pci_dev", "devfn" }, + { "pci_dev", "irq" }, + { "pci_dev", "resource" }, + { "resource", 0 }, + { "resource", "start" }, + { "resource", "end" }, + { "resource", "flags" }, +}; + +int +main(int argc, char **argv) +{ + struct btf b; + unsigned char *img; + unsigned long long len; + unsigned int *off; + long n; + int i, bad = 0; + + if (argc < 2) { + fprintf(stderr, "usage: ldls-types [--need | [member...]]\n"); + return 2; + } + + img = slurp(argv[1], &len); + if (!img) { fprintf(stderr, "ldls-types: cannot read %s\n", argv[1]); return 1; } + + n = btf_count(img, len); + if (n < 0) { fprintf(stderr, "ldls-types: %s is not BTF\n", argv[1]); return 1; } + + off = calloc((size_t)n + 2, sizeof(unsigned int)); + if (!off) { fprintf(stderr, "ldls-types: out of memory\n"); return 1; } + + if (btf_open(&b, img, len, off, (unsigned int)n + 2) < 0) { + fprintf(stderr, "ldls-types: %s\n", btf_err(&b)); + return 1; + } + + printf("%s\n %ld types, %u bytes of strings\n\n", argv[1], n, b.str_len); + + if (argc == 2 || strcmp(argv[2], "--need") == 0) { + printf(" what the PCI path needs\n"); + for (i = 0; i < (int)(sizeof(needed) / sizeof(needed[0])); i++) + bad += show(&b, needed[i].type, needed[i].member); + if (bad) + printf("\n %d of %d not found -- this kernel's BTF cannot drive probe()\n", + bad, (int)(sizeof(needed) / sizeof(needed[0]))); + else + printf("\n all present\n"); + return bad ? 1 : 0; + } + + if (argc == 3) + return show(&b, argv[2], 0); + for (i = 3; i < argc; i++) + bad += show(&b, argv[2], argv[i]); + return bad ? 1 : 0; +} diff --git a/tools/python/ldls_check.py b/tools/python/ldls_check.py index 384261f..073b3b6 100644 --- a/tools/python/ldls_check.py +++ b/tools/python/ldls_check.py @@ -126,6 +126,41 @@ def main(): need(m is not None and int(m.group(1), 16) != 0, "arguments reach the stub intact, not zeroed by the thunk") + # --- L2: the layouts come from a kernel's own BTF ----------------------- + # + # Conditional, and it says so when it skips. The BTF is the *host's* + # kernel type data -- not ours, not shipped, and not present on every + # machine. A check that silently passes when it did not run is worse than + # one that is honest about being unable to. + import os + types = os.path.join(os.path.dirname(inspect), "ldls-types") + btf = "/sys/kernel/btf/vmlinux" + if not (os.path.exists(types) and os.path.exists(btf)): + print("ldls-check: %d checks passed " + "(L2 skipped -- no %s on this machine)" % (checks, btf)) + return 0 + + r5 = subprocess.run([types, btf, "--need"], capture_output=True, text=True) + need(r5.returncode == 0, + "every layout the PCI path needs is readable from the kernel's BTF") + need("all present" in r5.stdout, "none of them is missing") + + # The offsets are not ours to have guessed, so the check is that they come + # back at all and are self-consistent -- pci_driver.probe after .id_table + # after .name, which is the order the struct declares them in. + o = {} + for line in r5.stdout.splitlines(): + m2 = re.match(r"\s+(\w+)\s+(\w+)\s+offset\s+(\d+)", line) + if m2: + o[(m2.group(1), m2.group(2))] = int(m2.group(3)) + need(o.get(("pci_driver", "name")) == 0 and + 0 < o.get(("pci_driver", "id_table"), -1) < + o.get(("pci_driver", "probe"), -1), + "the pci_driver offsets come back in declaration order") + need(o.get(("resource", "start")) == 0 and + o.get(("resource", "end"), -1) == 8, + "struct resource reads as the ABI says it is laid out") + print("ldls-check: %d checks passed" % checks) return 0