Commit graph SchOS/ldls
Author SHA1 Message Date
Nexoniarz
2d9a0ec6e3 ldls: being the PCI core, with offsets nobody wrote down
docs/plan/ldls.md milestone L2, less the half that needs /dev/pci.

In Linux, __pci_register_driver ends at the PCI core, which matches a
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.

To do that it must 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. Implemented from
Documentation/bpf/btf.rst, which is a published specification.

    $ ldls-types /sys/kernel/btf/vmlinux --need
      pci_driver   probe      offset    16  size 8
      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.

And then the driver:

      driver name "8139too"
      devices it claims: 10ec:8139 ... and 16 more
      calling probe(pci_dev 10ec:8139, ...)
        1  alloc_etherdev_mqs(0x368, 1, 1)
        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
      probe returned 0 after 552 stub call(s)

10ec:8139 is the real RTL8139 id. It mapped BAR 1, the one marked
IORESOURCE_MEM. Those 538 alternating byte accesses stepping 0x88/0x8a/0x8c/
0x8e are serial EEPROM clocking -- the driver bit-banging the chip for its MAC
address and handing the result to dev_addr_mod. Then NAPI, a watchdog timer,
register_netdev, its "found it" line, and zero.

Fifteen distinct symbols carry that entire probe, and three of them are 540 of
the 552 calls. 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 from running the thing.

Recorded as not done, because it is not: probe receives a pci_dev whose shape
is right and whose contents are invented. The BARs are made up, pci_iomap
returns a zeroed page, and 8139too read a MAC address of zeroes and was
perfectly happy about it. The "all through /dev/pci" half of L2 belongs to the
SchOS side. Also recorded: BTF has to come from somewhere, and SchOS has no
/sys/kernel/btf/vmlinux -- the user brings it with the module, from the kernel
the module was built for.

make ldls-check is 27 checks, four of them L2's, and it says so out loud when
it skips them on a machine with no BTF rather than passing silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 08:14:57 +02:00
Nexoniarz
7a7c7611e0 ldls: the stubs, and the driver's own voice
docs/plan/ldls.md milestone L1. Every undefined symbol resolves to a
trampoline -- twenty generated bytes that load the symbol's index and jump to
a shared thunk, which saves the argument registers and reports the call. Then
init_module is simply called.

    $ ldls-run 8139too.ko
      module  8139too
      license GPL
      wants   94 symbols
      calling init_module at 0x416fc020

        1  _printk(0x416fb6c0, 0, 0)
          | 8139too: 8139too Fast Ethernet driver 0.9.28
        2  __pci_register_driver(0x416fb400, 0x416fff80, 0x416fbdbb)

      init_module returned 0 after 2 stub call(s)

That second line is the driver's own banner, printed through a _printk that
is ours, from a string in the module's own .rodata that a relocation put
there. There is no stronger evidence that a load worked than the loaded thing
speaking.

Stubs return zero rather than stopping, which departs from what the milestone
originally said and does so deliberately: stopping at the first call tells you
one name, carrying on tells you the sequence, and the sequence is the thing a
symbol table cannot give you. A driver that then dereferences the zero faults,
and that is a result too -- it says how far it got before it needed the kernel
to have really done something.

The first run failed on neither of those. It failed on a relocation:

    ldls-run: relocate: a signed 32-bit relocation does not fit

A module is compiled -mcmodel=kernel, so the compiler emits R_X86_64_32S for
anything it believes lives in the top 2 GB, and an ordinary mmap at 0x7f...
has nowhere to put it. Linux gives modules a region within 2 GB of the kernel
text; the harness uses MAP_32BIT to reach the same window from the other end.
On SchOS this costs nothing -- a process's mmap area starts at 0x40000000 and
is already inside it -- but it is a real constraint on where the host may load
a module, and a loader that did not check its own arithmetic would have found
it much later and much more confusingly.

Swept over 500 stock modules: 449 ran init to completion, 22 faulted on a
stub's zero, 29 have no init_module, and zero hit a loader error.

Recorded honestly in "what it will not do": running init is not working. Every
symbol is a stub, so what has been shown is that drivers ask for things in a
readable order, not that anything answers. Also recorded: every undefined
symbol in a module is NOTYPE, so the stubs cannot tell jiffies from
pci_iomap and both get a trampoline -- BTF distinguishes VAR from FUNC and
that is where the fix belongs rather than in a guess.

make ldls-check is 23 checks now, five of them L1's, all against
ldls/test/refmod.c which is ours and now has an init_module of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 08:02:29 +02:00
Nexoniarz
901ae03a69 ldls: Apache-2.0, and where the boundary actually is
Answers the question directly in docs/licensing.md §6, because "LDLS runs
Linux drivers" reads like it contradicts §2 and does not.

LDLS is Apache-2.0. Every file under ldls/ is ours; the loader came from the
ELF-64 gABI and the x86-64 psABI. The licence of LDLS and the licence status
of what happens at run time are two different questions and only the first is
about us:

  ldls/                       Apache-2.0    distributed by us
  a .ko                       GPL-2.0       distributed by the user's distro
  the two in one process      --            distributed by nobody

GPLv2 §0 says where its obligations attach -- "the act of running the Program
is not restricted" -- so no combined work is distributed by anyone and no
obligation is triggered. Same position FreeBSD has shipped from for a decade.
Stated honestly in the same section: the *user* cannot redistribute the
combination either, which is why RAMDISK_EXCLUDE exists and why .gitignore
refuses *.ko.

EXPORT_SYMBOL_GPL was left open in the plan; it is now answered and the answer
is cleaner than expected. That marking gates *Linux's own* symbols. Every
symbol LDLS provides is ours, reimplemented from facts, so there is no gate
being bypassed because no Linux symbol is being exposed. LDLS implements no
licence check -- it reads the declared licence and prints it before loading
anything.

And the sharpest edge in rule 3 mostly goes away. The uncomfortable row was
struct layouts, which a compiled driver indexes by offset, so the shim must
reproduce them exactly -- and a field-for-field match looks like transcription
even when it is not. It does not have to be in our source: kernels built with
CONFIG_DEBUG_INFO_BTF publish /sys/kernel/btf/vmlinux, and that carries both
the layouts and the function prototypes as generated type data about the
kernel the user already has. Verified on this machine: struct pci_dev field by
field, and FUNC 'pci_iomap' with its FUNC_PROTO. LDLS reads them at load time
on the user's machine; our source contains neither.

That also mostly dissolves rule 4, since BTF describes the kernel that is
actually installed rather than one pinned by guessing. What BTF does not give
is behaviour -- what napi_complete_done *does* is still ours to write from
documentation, and that is the whole of the real work.

Also adds the SPDX header ldls/ko.h was missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 07:52:01 +02:00
Nexoniarz
1be5f3a704 ldls: the loader, and what it measured
docs/plan/ldls.md milestone L0. ldls/ko.c indexes the sections of an ELF64
relocatable object, places the allocatable ones, resolves undefined symbols
against a caller's table and applies the relocations. Nothing is executed.

It is freestanding -- no libc, no allocation, no I/O -- so the same source
builds as a host tool now and as the SchOS-side host later. That is the
arrangement mkschfs and the kernel already share, and for the same reason:
the failures at this layer are format failures, and one found on the host
costs a second instead of a boot.

    $ ldls-inspect 8139too.ko
      module    8139too
      license   GPL
      loads to  33988 bytes
      relocations: 1980
        R_X86_64_64 363, PC32 991, PLT32 428, 32S 169, PC64 29
      undefined symbols: 94
      stops at: unresolved: __fentry__

The licence is printed before anything is loaded, because it is the one
property that decides whether the object may be on the machine at all and it
is a plain string in .modinfo. Rule 1 is easier to keep when the answer is on
screen.

94 symbols is the measured API surface of 8139too on 6.18.46 -- the list,
exactly, rather than an estimate -- and it is L1's work queue. It is smaller
than it looks: __fentry__, __stack_chk_fail, __ref_stack_chk_guard,
__x86_return_thunk and __fortify_panic are compiler and instrumentation
artefacts that stub out in a line each.

Swept over every module in a stock 6.18.46 tree: 2,440 objects, all of them
laid out and relocated to completion, none with a relocation type the loader
does not handle. That sweep is also what retired the fixed section table --
nvidia-modeset has 7,069 sections and 74,305 relocations, against a median of
50 -- so the caller supplies the table and there is no limit to run into
rather than a generous one that fails later.

Loading is not running, and the doc says so: not one symbol is implemented,
and the distance between "relocates" and "transmits a packet" is the rest of
the plan.

No .ko is in this tree and .gitignore now refuses one outright. make
ldls-check runs 18 checks against ldls/test/refmod.c, which is ours, written
to contain one of each thing the loader has to get right.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 07:45:31 +02:00