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
15 KiB
LDLS — the Linux Driver Layer for SchOS
SchOS cannot write a driver for every device that exists. No project can. The only route to the hardware support an established system has is to run someone else's drivers, and in practice "someone else's drivers" means Linux's.
LDLS itself is Apache-2.0, like everything else in this tree, and stays that way because it never contains a line of Linux. What the user combines it with on their own machine is a separate question, and ../licensing.md §6 is where that one is answered.
hybrid.md built the mechanisms for this and said so in as many
words — MMIO, DMA, interrupts and config space to a process, with SchOS's own
drivers as the first consumers so the interface would be general rather than a
hole shaped like something foreign. vgpud proved it. LDLS is what those
mechanisms were built for.
The rules
These are the spine. Everything below is an implementation of them, and anything that violates one is wrong however well it works.
1. Nothing GPL ever enters an image we build
The driver is the user's, obtained by them, loaded on their machine. SchOS ships the layer and never the thing it loads. This is not a new position — it was decided on 2026-08-08 and is written up in ../architecture/devices.md and ../licensing.md §2:
Apache-2.0 code and GPL-2.0-only code cannot be combined into one distributable work. It is not a matter of taste or of how carefully the files are kept apart in the tree.
RAMDISK_EXCLUDE and NOTPORTS are the existing mechanisms that keep this
true mechanically rather than by intention, and LDLS must stay inside them.
2. It runs in a process, never in the kernel
Two independent reasons, and either alone would decide it.
Legally, a module linked into our kernel is the strongest derivative-work
argument that exists — it is the case the FSF has always made. A module
combined with our shim inside a user process, on the user's machine, at their
direction is a different thing at arm's length, and it is the position
FreeBSD has shipped from for about a decade: a BSD-licensed linuxkpi in base,
GPL drm-kmod installed by the user from ports.
Practically, this is foreign code. A driver we did not write, cannot fully audit and did not choose the quality of, running with the machine's memory in reach, is exactly the code that should not be able to take the machine down. Crash isolation is not a nicety here; it is the reason.
3. Facts, not expression
The shim has to present Linux's internal API, and that API is expressed in GPL headers. The line LDLS holds:
| A symbol exists, and takes these arguments | fact — reproduce it |
| A struct has these fields in this order at these offsets | fact — a compiled driver indexes them, so there is no other way to express it. And mostly it need not be in our source at all; see BTF below |
| The comment above it | expression — never |
| The macro body, the inline function body, the implementation | expression — never |
Every KPI file records where its shape came from. Not decoration: it is the difference between a clean-room reimplementation and a transcription, and the only time anyone will ask is the time it matters.
This is the same discipline ../licensing.md §5 already describes — "the xHCI, USB, PCI, fbdev and ELF specifications were all read and implemented directly, which is why those drivers have no licence entanglement at all" — applied to a case where the specification is thinner.
Not legal advice, and it should be checked by someone whose job that is before anything ships. The reasoning rests on interfaces being outside copyright (EU Software Directive Art. 1(2)) and on Google v. Oracle (2021), and neither is a blanket permission.
The layouts do not have to be ours to write
The uncomfortable row above is the struct layouts, and it turns out to be
largely avoidable. Kernels built with CONFIG_DEBUG_INFO_BTF publish
/sys/kernel/btf/vmlinux, which carries the layouts and the function
prototypes as generated binary type data about the kernel that is installed:
struct pci_dev {
struct list_head bus_list; /* 0 16 */
struct pci_bus * bus; /* 16 8 */
[97814] FUNC 'pci_iomap' type_id=97813 linkage=static
[97815] FUNC_PROTO '(anon)' ret_type_id=108 vlen=4
So LDLS reads them at load time, on the user's machine, from the user's own kernel. Our source contains neither, and there is nothing to have transcribed. It also mostly dissolves rule 4 below, because BTF describes the kernel that is actually there rather than one we pinned by guessing.
BTF gives shape, not behaviour. What napi_complete_done does is still
ours to implement from documentation, and that is the whole of the real work.
A kernel without BTF needs hand-written layouts for a named version, which is
why this section stays rather than being replaced.
4. One kernel version at a time, pinned and named
(Mostly superseded by BTF, above — but it still governs any kernel that does not carry it.)
Linux states in writing that it has no stable in-kernel ABI —
Documentation/process/stable-api-nonsense.rst exists to say so. Believing
them is cheaper than being surprised by them.
So LDLS targets one version, says which, and is re-pinned deliberately. FreeBSD's LinuxKPI does exactly this per drm-kmod release. A layer that claims to support "Linux drivers" without a version is claiming something nobody has ever delivered.
5. The unresolved-symbol list is the specification
Do not guess the API surface. Load a real module and let the loader say what is missing. That list is the work queue, it is exact, and it is available before a single line of shim is written.
What it is made of
Four pieces, of which three are ordinary and one is the project.
The loader — ours, clean, and small
A .ko is a relocatable ELF64 object. Loading one is: lay out the allocatable
sections, apply the relocations, resolve the undefined symbols against a table,
and find the entry point. SchOS already parses ELF in kernel/fs/exec.c, for a
different object type but with the same plumbing.
There is no licence question here at all — ELF is a published specification and so is the relocation set.
The KPI — the shim, and the actual work
Our Apache-2.0 reimplementation of the Linux symbols a given driver needs,
organised under Linux's own header names so it is navigable: kpi/slab.h,
kpi/pci.h, kpi/netdevice.h. Rule 3 governs every file.
The back end — already built
/dev/pci: PCIIOC_INFO, PCIIOC_CLAIM, PCIIOC_CFG, mmap of a BAR,
PCIIOC_DMA, PCIIOC_IRQ. A Linux driver's pci_iomap, dma_alloc_coherent
and request_irq land on these, which is the whole reason they were specified
in terms of PCI devices rather than in terms of drivers.
The class bridge — the one new kernel interface
This is the piece that does not exist, and it is the interesting one.
A hosted driver produces packets in a process. SchOS's network stack consumes
them through struct netdev_ops in the kernel — transmit, receive, mac,
link_up, four entries and nothing else. So what is missing is a netdev
whose backend is a process: the kernel publishes the vtable, the four calls
are forwarded over a descriptor, and lwIP never learns that the driver is not
in the kernel.
It is the exact analogue of what display_ops was for the GPU, and
netdev.h's own opening comment already anticipates the shape — "the stack
should ask for the network interfaces and get whatever is bound … exactly as a
compositor asks for the display and does not care which driver answers."
One device, one process, claimed and released, on the /dev/pci discipline.
Milestones
| 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 and stops. init_module runs and dies at the first real call — which yields the call order, the second thing you need and the other thing you cannot guess. |
|
| 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. |
|
| 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 |
L4 is listed for completeness rather than as work. pciuser_stop(),
pciuser_release_all() and KeepAlive already do it, and that they apply
unchanged to a foreign driver is the point of having built them against our
own.
The first target: 8139too, not r8169
The obvious choice is a Realtek r8169, because that silicon is everywhere.
It is the wrong first choice for one decisive reason: QEMU cannot emulate
it, so L0 through L3 would have no way to be tested, and a driver layer
developed against hardware you cannot run is a layer nobody can debug.
QEMU emulates rtl8139. 8139too is small, self-contained, needs no firmware
blob, and uses the thin mii helpers rather than dragging in phylib — which
r8169 does, and which is a substantial surface on its own.
So: 8139too to build the layer, r8169 to prove it was general. The
second is the interesting one and it is much cheaper once the first works.
This is the /dev/pci lesson applied backwards. There, a native driver was the
first consumer so the interface would not be shaped like something foreign.
Here the interface is fixed by Linux, and what is chosen instead is the
smallest real consumer that can be tested — because the risk is no longer
designing the wrong interface, it is building a hundred stubs nobody has ever
run.
L0, and what it measured
ldls/ko.c is the loader: index the sections, place the allocatable ones,
resolve the undefined symbols against a table, apply the relocations. It is
freestanding — no libc, no allocation, no I/O — so the same source builds as a
host tool today and as the SchOS-side host later, which is the arrangement
mkschfs and the kernel already share.
$ ldls-inspect 8139too.ko
119288 bytes, 55 sections, 363 symbols
module 8139too
license GPL
describes RealTek RTL-8139 Fast Ethernet driver
loads to 33988 bytes
relocations: 1980
R_X86_64_64 363
R_X86_64_PC32 991
R_X86_64_PLT32 428
R_X86_64_32S 169
R_X86_64_PC64 29
undefined symbols: 94
...
stops at: unresolved: __fentry__
The licence is printed first, before anything is loaded, because it is the
one property that decides whether the object may be on this machine at all and
it is a plain string in .modinfo. Rule 1 is easier to keep when the answer is
on screen.
What the measurement says
94 symbols is the API surface of 8139too on Linux 6.18.46. Not an
estimate — the list, exactly, and it is L1's work queue. It is also smaller
than it looks, because a good fraction of it is not API at all: __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.
Five relocation types, and a corpus run says that is the whole set: over
2,440 real modules from a stock 6.18.46 tree, every one lays out and
relocates to completion with nothing unhandled. That includes nvidia-modeset
at 7,069 sections and 74,305 relocations, which is what retired the loader's
fixed section table — the caller supplies it now, so there is no limit to run
into rather than a generous one.
Relocating against stubs is a separate question from listing what is missing, and it is the one that says the loader is finished: with nothing resolved, the first unknown name stops everything and no relocation type past it is ever reached.
Nothing foreign is in this tree
make ldls-check runs the loader against ldls/test/refmod.c — ours, compiled
with -c, written to contain one of each thing the loader has to get right:
undefined functions and undefined data, .text with calls, .data holding a
pointer into .rodata, a .bss that must arrive zeroed, and a .modinfo
shaped the way a module's is. The modules above were read from the host's own
/nix/store and never copied here; .gitignore refuses *.ko outright so
that cannot happen by accident.
What it will not do
- It is not a security boundary, and here that is worse than usual. There is no IOMMU, so a hosted driver can point its device at any physical address. For SchOS's own drivers that isolates bugs and not intent, which hybrid.md says at length. For foreign drivers the distinction between bug and intent is one we are in no position to make at all. This is the single strongest argument for an IOMMU that this project has produced, and it should be recorded as such rather than waved past.
- It implements no licence check, and that is not circumvention.
EXPORT_SYMBOL_GPLis Linux marking Linux's own symbols, and the check it drives protects those. Every symbol LDLS provides is ours, reimplemented from facts under Apache-2.0, so there is no gate being bypassed because there is no Linux symbol being exposed. LDLS reads the declared licence and prints it before loading anything, because that is the property that decides whether an object belongs on a machine at all. See ../licensing.md §6. - No firmware loading.
request_firmwareis a stub that fails. Many drivers need a blob, most blobs are redistributable only under their own terms, and that is a second licensing problem which is not this one. - No module-to-module dependencies. One object, no
depmod, no symbol exports between hosted modules. - No sysfs, procfs or debugfs. Drivers that only report through them will work; drivers that are configured through them will not.
- No unload.
module_exitis never called; the way to stop a driver is to kill the process, which H2 made a supported operation rather than a failure. - No sleeping-context fidelity. Linux drivers assume a kernel's rules about
what may sleep, what runs in interrupt context and what
spin_lock_irqsavecosts. A process has different rules, and where they differ the shim will be wrong in ways that show up as rare races rather than as failures to build. This is the deepest technical risk in the whole plan. - Loading is not running. L0 proves 2,440 modules can be laid out and relocated. That says the format is handled and says nothing whatever about whether any of them would work: not one symbol is implemented, and the distance between "relocates" and "transmits a packet" is the entire rest of this plan.
- 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 when there is one.