ldls: the same loader, on SchOS

docs/plan/ldls.md milestone L3b. ldls/ko.c, btf.c and stub.c were written
freestanding so they would build unchanged on both sides; cmd/ldlsd collects
that promise -- the same three files, linked against lib/ instead of a libc,
in an ordinary SchOS process.

    # ldlsd /lib/modules/8139too.ko /lib/modules/btf
      module  8139too      license GPL      wants 94 symbols
      defused 6 %gs stack-guard reference(s)
      calling init_module
        1  _printk(...)
            | 8139too: 8139too Fast Ethernet driver 0.9.28
        2  __pci_register_driver(...)
      btf     122279 types
      layout  pci_driver.probe +16   pci_dev 2680 bytes
      driver  "8139too"    devices it claims: 10ec:8139 ... and 18 more
      calling probe
        1  alloc_etherdev_mqs(0x368, 0x1)
      probe returned -12 after 1 stub call(s)

Byte for byte the host harness's behaviour, which is what the shared source
was for. Two earlier predictions came good with no work: a process's mmap area
starts at 0x40000000, inside the window R_X86_64_32S needs, and the kernel
never sets EFER.NXE so every mapping is executable. The second is a gap rather
than a feature and is now recorded as one -- LDLS is relying on the absence of
a protection an OS should have.

One thing did not just work. The first run faulted before probe's first call,
err 5 -- present, read, user -- at a kernel address, with the rip resolving to

    mov %gs:0x0(%rip),%rax      # rtl8139_init_one+0x26

Modules are built -mstack-protector-guard-reg=gs, so the canary comes through
the GS segment, and that is an ABI between the kernel and its own per-CPU area
which a host that is not that kernel cannot provide. ko_defuse_gs() rewrites
those references to xor %reg,%reg.

There are two forms and the second is easy to miss: the prologue loads the
guard with mov (8b) and the epilogue compares it with sub (2b) before a jne to
__stack_chk_fail. xor is right for both -- it zeroes the saved canary and sets
ZF, so the epilogue's branch falls through exactly as a passing check does. Six
sites in 8139too.

It checks every site before patching any and refuses the whole operation on
anything it does not recognise. That refusal is what found the sub form, by
declining to guess -- a loader that half-understands an instruction should not
be editing it.

make ldls-stage/ldls-unstage put a module and its kernel's BTF into the local
sysroot and take them out again. Not part of `all`, and .gitignore refuses
*.ko, so an image only carries somebody else's GPL code between two commands
you typed on purpose.

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 09:10:01 +02:00
commit 3fd1cc5279
7 changed files with 738 additions and 3 deletions

103
ldls/ko.c
View file

@ -494,6 +494,109 @@ ko_relocate(struct ko *k, ko_resolver r, void *ctx)
return 0;
}
// --- the %gs stack guard ------------------------------------------------------
// Rewrite one `mov %gs:disp32(%rip), %reg` into `xor %reg, %reg` plus padding.
//
// The encoding is fixed and short: 65 (the GS prefix), a REX byte, 8b (MOV
// r64, r/m64), a ModRM of mod=00 reg=DEST rm=101 (RIP-relative), then the
// displacement. Eight bytes, of which the last four are what the relocation
// was going to fill in.
//
// The replacement has to clear the *same* register, so the destination is
// decoded rather than assumed: REX.R is the high bit and ModRM.reg the low
// three. `xor %rN, %rN` is three bytes, and the rest is padded with nops.
static int
defuse_one(unsigned char *insn)
{
unsigned int rex, modrm, reg;
if (insn[0] != 0x65) return -1; /* not GS-prefixed */
if ((insn[1] & 0xF0) != 0x40) return -1; /* not a REX byte */
// Two forms, and both have to go. The prologue *loads* the guard with
// MOV (8b); the epilogue *compares* it with SUB (2b), followed by a jne to
// __stack_chk_fail. Replacing either with `xor %reg,%reg` is right: the
// first makes the saved canary zero, and the second sets the register to
// zero *and* sets ZF, so the jne falls through exactly as a passing check
// does.
if (insn[2] != 0x8b && insn[2] != 0x2b) return -1;
rex = insn[1];
modrm = insn[3];
if ((modrm & 0xC7) != 0x05) return -1; /* not mod=00 rm=101 */
reg = ((rex & 0x4) << 1) | ((modrm >> 3) & 0x7);
insn[0] = (unsigned char)(0x48 | ((reg >= 8) ? 0x05 : 0x00)); /* REX.W(+R+B) */
insn[1] = 0x31; /* XOR r/m64,r64 */
insn[2] = (unsigned char)(0xC0 | ((reg & 7) << 3) | (reg & 7));
insn[3] = 0x90;
insn[4] = 0x90; insn[5] = 0x90; insn[6] = 0x90; insn[7] = 0x90;
return 0;
}
int
ko_defuse_gs(struct ko *k, const char *sym)
{
int i, n = 0;
if (!k->laid_out)
{ ko_fail(k, "defuse before layout", 0); return -1; }
// Two passes: check everything first, then patch. A loader that rewrites
// half the instructions and then refuses has left the object in a state
// nobody can reason about.
for (int pass = 0; pass < 2; pass++) {
n = 0;
for (i = 0; i < k->nsh; i++) {
const struct ko_shdr *sh = ko_sh(k, i);
struct ko_section *tgt;
unsigned long long cnt, j;
if (!sh || sh->sh_type != KO_SHT_RELA || !k->sec[i].data)
continue;
if (sh->sh_info >= (unsigned int)k->nsh)
continue;
tgt = &k->sec[sh->sh_info];
if (!tgt->alloc || tgt->size == 0)
continue;
cnt = sh->sh_size / sizeof(struct ko_rela);
for (j = 0; j < cnt; j++) {
const struct ko_rela *rl =
(const struct ko_rela *)(k->sec[i].data) + j;
unsigned int si = KO_R_SYM(rl->r_info);
const char *nm;
if ((int)si >= k->nsym)
continue;
nm = k->str + k->sym[si].st_name;
if (!ko_streq(nm, sym))
continue;
if (rl->r_offset < 4 || rl->r_offset + 4 > tgt->size)
{ ko_fail(k, "a guard relocation is at an impossible offset", 0);
return -1; }
{
unsigned char *insn = tgt->where + rl->r_offset - 4;
if (pass == 0) {
// Check only.
if (insn[0] != 0x65 || (insn[1] & 0xF0) != 0x40 ||
(insn[2] != 0x8b && insn[2] != 0x2b) ||
(insn[3] & 0xC7) != 0x05)
{ ko_fail(k, "unrecognised instruction at a guard reference: ",
sym); return -1; }
} else if (defuse_one(insn) < 0) {
ko_fail(k, "failed to rewrite a guard reference", 0);
return -1;
}
n++;
}
}
}
}
return n;
}
// --- what the object says about itself ---------------------------------------
static const struct ko_section *