diff --git a/Makefile b/Makefile index b507b08..bf2f490 100644 --- a/Makefile +++ b/Makefile @@ -128,7 +128,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS)) # Third-party software does NOT go here — see ports/. # ----------------------------------------------------------------------- ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o) -CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd tone hwreport +CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd tone hwreport dynhello CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS)) # ----------------------------------------------------------------------- @@ -1047,6 +1047,33 @@ $(BUILD)/cmd/ldlsd: cmd/ldlsd/main.c $(ULIB) $(LDLS_GUEST) $(BUILD)/cmd/ldlsd.o $(LDLS_GUEST) $(ULIB) $(OBJCOPY) -S $@ +# dynhello is a musl program, and the only dynamically linked one: the test +# that the kernel loads an interpreter and the interpreter maps libraries. +# Not -static, against libc.so rather than libc.a, with musl's loader named as +# the interpreter, and with a shared library of its own beside it -- see +# cmd/dynhello/main.c. +DYNCC = $(PORTCC) -O2 -D__SCHOS__ -I$(MUSLPREFIX)/include -fno-stack-protector +DYNLD = -Wl,-dynamic-linker,/lib/ld-musl-x86_64.so.1 -Wl,-rpath,/lib + +$(MUSLPREFIX)/lib/libc.so: + $(MAKE) -C ports musl CC=gcc + +$(BUILD)/cmd/libdynhello.so: cmd/dynhello/lib.c $(MUSLPREFIX)/lib/libc.so + @mkdir -p $(BUILD)/cmd + $(DYNCC) -fPIC -c $< -o $(BUILD)/cmd/libdynhello.o + $(DYNCC) -shared -nostdlib -Wl,-soname,libdynhello.so -o $@ \ + $(BUILD)/cmd/libdynhello.o -L$(MUSLPREFIX)/lib -lc + +$(BUILD)/cmd/dynhello: cmd/dynhello/main.c $(BUILD)/cmd/libdynhello.so + $(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/dynhello.o + $(DYNCC) -no-pie -nostdlib $(DYNLD) -o $@ \ + $(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/dynhello.o \ + -L$(BUILD)/cmd -ldynhello -L$(MUSLPREFIX)/lib -lc $(PORTEND) + +$(SYSROOT)/lib/libdynhello.so: $(BUILD)/cmd/libdynhello.so + @mkdir -p $(SYSROOT)/lib + cp $< $@ + $(BUILD)/cmd/%: cmd/%/main.c $(ULIB) @mkdir -p $(BUILD)/cmd $(CC) $(CFLAGS) -c $< -o $(BUILD)/cmd/$*.o @@ -1321,7 +1348,7 @@ SYSROOT_DIRS = dev proc sys run tmp root # After lwIP is unpacked: `make -C ports` below unpacks it too, and the two # unzips running at once under -j trip over each other. -sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS)) etc-install | $(LWIPDIR) +sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS)) $(SYSROOT)/lib/libdynhello.so etc-install | $(LWIPDIR) @mkdir -p $(addprefix $(SYSROOT)/, $(SYSROOT_DIRS)) ifdef WINHOST @echo "ports: skipped (Windows host) -- musl/mksh/toybox are used as already staged in $(SYSROOT)" diff --git a/cmd/dynhello/lib.c b/cmd/dynhello/lib.c new file mode 100644 index 0000000..9e75cf7 --- /dev/null +++ b/cmd/dynhello/lib.c @@ -0,0 +1,14 @@ +// libdynhello.so: a shared library that is not libc, so that dynhello proves +// the dynamic linker can find, map and bind something it was not born with. + +#include + +static int calls; + +int +dynhello_greet(const char *who) +{ + calls++; + return printf("dynhello: hello, %s, from a shared library (call %d)\n", + who, calls); +} diff --git a/cmd/dynhello/main.c b/cmd/dynhello/main.c new file mode 100644 index 0000000..787e5ea --- /dev/null +++ b/cmd/dynhello/main.c @@ -0,0 +1,55 @@ +// dynhello: the smallest honest test of dynamic linking. +// +// Linked against libc.so and libdynhello.so, with /lib/ld-musl-x86_64.so.1 as +// its interpreter, so nothing here works unless the kernel loaded the +// interpreter and the interpreter mapped both libraries. It then checks the +// three things a dynamic program leans on that a static one hides: thread- +// local storage (errno), the heap, and a fork whose child still has its +// libraries. + +#include +#include +#include +#include +#include +#include + +int dynhello_greet(const char *who); + +int +main(int argc, char **argv) +{ + int bad = 0; + + dynhello_greet(argc > 1 ? argv[1] : "world"); + + errno = 0; + if (close(-1) == 0 || errno != EBADF) { + printf("dynhello: FAIL errno is %d, not EBADF\n", errno); + bad++; + } + + char *p = malloc(1 << 20); + if (!p) { + printf("dynhello: FAIL malloc\n"); + bad++; + } else { + memset(p, 0x5a, 1 << 20); + free(p); + } + + pid_t pid = fork(); + if (pid == 0) { + dynhello_greet("child"); + _exit(0); + } + int st = 0; + if (pid < 0 || waitpid(pid, &st, 0) != pid || !WIFEXITED(st) || + WEXITSTATUS(st) != 0) { + printf("dynhello: FAIL fork child status %d\n", st); + bad++; + } + + printf("dynhello: %s\n", bad ? "FAILED" : "ok"); + return bad != 0; +} diff --git a/kernel/fs/exec.c b/kernel/fs/exec.c index 000b215..5ffc131 100644 --- a/kernel/fs/exec.c +++ b/kernel/fs/exec.c @@ -142,6 +142,99 @@ arg_copy(struct argsrc *s, uint64 count, pml4e_t *pgdir, addr_t *at, static int do_exec(char *path, struct argsrc *av, struct argsrc *ev); +// --- Dynamic linking --------------------------------------------------------- +// +// A dynamically linked program names an interpreter in PT_INTERP -- on musl, +// /lib/ld-musl-x86_64.so.1, which is libc.so under another name -- and the +// kernel's whole part in dynamic linking is to load that as well and start it +// instead of the program. The interpreter finds the program through the +// auxiliary vector (AT_PHDR, AT_ENTRY), maps libc and anything else it needs +// with mmap(), relocates, and jumps to AT_ENTRY itself. +// +// Position-independent images -- the interpreter always, a PIE program +// sometimes -- are ET_DYN, linked at zero and placed at a base of the +// kernel's choosing. A PIE program goes where a fixed one would; the +// interpreter goes at the start of the mmap area, and mmap carries on above +// it, so the two never meet. +#define ET_DYN 3 +#define PT_INTERP 3 +#define PT_PHDR 6 +#define PIE_BASE 0x400000UL // where lld puts a fixed executable +#define INTERP_BASE 0x40000000UL // the bottom of sys_mmap's bump area + +// Map and read one image's PT_LOAD segments at `base`. Returns 0 and the end +// of the highest segment, and where the program headers are once loaded (0 if +// no segment carries them), or -1. +// +// Segments are mapped from where the previous one's last page ended: two +// segments may share a page, and asking allocuvm() for it twice would map it +// twice. +static int +load_image(pml4e_t *pgdir, struct inode *ip, struct elfhdr *elf, uint64 base, + uint64 *hi, uint64 *phdr_va) +{ + struct proghdr ph; + uint64 mapped = 0; + + *hi = 0; + *phdr_va = 0; + for (int i = 0, off = elf->phoff; i < elf->phnum; i++, off += elf->phentsize) { + if (readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph)) + return -1; + if (ph.type == PT_PHDR) + *phdr_va = base + ph.vaddr; + if (ph.type != ELF_PROG_LOAD) + continue; + if (ph.memsz < ph.filesz || ph.vaddr + ph.memsz < ph.vaddr) + return -1; + + uint64 start = (base + ph.vaddr) & ~0xFFFULL; + uint64 end = base + ph.vaddr + ph.memsz; + if (start < mapped) + start = mapped; + if (start < end && allocuvm(pgdir, start, end) == 0) + return -1; + if (PGROUNDUP(end) > mapped) + mapped = PGROUNDUP(end); + + if (loaduvm(pgdir, (char*)(base + ph.vaddr), ip, ph.off, ph.filesz) < 0) + return -1; + + // The headers are in the file at phoff; whichever segment covers that + // offset is where they are in memory. PT_PHDR says so directly when the + // linker wrote one, and this covers the images that do not. + if (!*phdr_va && ph.off <= elf->phoff && elf->phoff < ph.off + ph.filesz) + *phdr_va = base + ph.vaddr + (elf->phoff - ph.off); + + if (end > *hi) + *hi = end; + } + return 0; +} + +// The interpreter path from PT_INTERP, or "" if there is none. -1 on a +// malformed one. +static int +find_interp(struct inode *ip, struct elfhdr *elf, char *buf, int cap) +{ + struct proghdr ph; + + buf[0] = 0; + for (int i = 0, off = elf->phoff; i < elf->phnum; i++, off += elf->phentsize) { + if (readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph)) + return -1; + if (ph.type != PT_INTERP) + continue; + if (ph.filesz < 2 || ph.filesz > (uint64)cap) + return -1; + if (readi(ip, 0, (uint64)buf, ph.off, ph.filesz) != (int)ph.filesz) + return -1; + buf[ph.filesz - 1] = 0; + return 0; + } + return 0; +} + // envp may be null, which means "no environment" -- the same thing this // function used to do unconditionally. int @@ -165,11 +258,9 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) uint64 _t0 = rdtsc(); fsstat.exec_calls++; char *s, *last; - int i, off; uint64 sz, sp; struct elfhdr elf; struct inode *ip; - struct proghdr ph; pml4e_t *pgdir, *oldpgdir; // The arguments are measured before anything is loaded, so that too many @@ -197,8 +288,11 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) } uint64 max_sz = 0; uint64 rand_va = 0; + uint64 base = 0, phdr_va = 0, interp_end = 0, interp_entry = 0; + char interp[128]; begin_op(); + int in_op = 1; // the error path closes the transaction only once int err; if((ip = nameifollow(0, path, &err)) == 0){ @@ -220,34 +314,37 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) if((pgdir = setupkvm()) == 0) goto bad; - for(i=0, off=elf.phoff; i max_sz) - max_sz = end; - } + base = elf.type == ET_DYN ? PIE_BASE : 0; + if (find_interp(ip, &elf, interp, sizeof(interp)) < 0) + goto bad; + if (load_image(pgdir, ip, &elf, base, &max_sz, &phdr_va) < 0) + goto bad; iunlockput(ip); - end_op(); ip = 0; + // The interpreter, at the bottom of the mmap area. It must itself be + // position independent and must not ask for an interpreter of its own. + if (interp[0]) { + struct elfhdr ielf; + uint64 ihdr; + if ((ip = nameifollow(0, interp, &err)) == 0) + goto bad; + ilock(ip); + if (readi(ip, 0, (uint64)&ielf, 0, sizeof(ielf)) != sizeof(ielf) || + ielf.magic != ELF_MAGIC || ielf.type != ET_DYN) + goto bad; + if (load_image(pgdir, ip, &ielf, INTERP_BASE, &interp_end, &ihdr) < 0) + goto bad; + interp_entry = INTERP_BASE + ielf.entry; + iunlockput(ip); + ip = 0; + } + end_op(); + in_op = 0; + // The stack: 63 pages plus a guard, as always, and on top of that room for // the arguments, the pointer table and AT_RANDOM. - uint64 nslots = 1 + argc + 1 + envc + 1 + 20; + uint64 nslots = 1 + argc + 1 + envc + 1 + 32; uint64 argpages = (strbytes + nslots * 8 + 64 + PGSIZE - 1) / PGSIZE; uint64 stackpages = 64 + argpages; sz = PGROUNDUP(max_sz); @@ -299,6 +396,18 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) for (int k = 0; k < (int)NELEM(fixed); k++) if (tbl_put(&t, fixed[k]) < 0) goto bad; + // Where the program is, for the dynamic linker -- and for a static musl + // program too, which finds its TLS template through AT_PHDR. + uint64 where[] = { + 3, phdr_va, // AT_PHDR + 4, sizeof(struct proghdr), // AT_PHENT + 5, elf.phnum, // AT_PHNUM + 7, interp[0] ? INTERP_BASE : 0, // AT_BASE: the interpreter's load address + 9, base + elf.entry, // AT_ENTRY: the program's, not the interpreter's + }; + for (int k = 0; k < (int)NELEM(where); k++) + if (tbl_put(&t, where[k]) < 0) + goto bad; if (tbl_put(&t, 25) < 0 || tbl_put(&t, rand_va) < 0 || // AT_RANDOM tbl_put(&t, 0) < 0 || tbl_put(&t, 0) < 0 || // AT_NULL tbl_flush(&t) < 0) @@ -321,7 +430,7 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) // Auto-select syscall ABI: musl binaries have entry >= 0x100000; // SchOS-native binaries (init) are linked at 0x4000. - if (elf.entry >= LINUX_ABI_ENTRY_THRESHOLD) { + if (base + elf.entry >= LINUX_ABI_ENTRY_THRESHOLD) { myproc()->syscall_map = linux_syscall_map; myproc()->map_size = linux_syscall_map_size; } else { @@ -358,6 +467,17 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) myproc()->sigsusp_active = 0; myproc()->mmap_next = 0; + // The interpreter is ordinary memory to everything after this: fork copies + // it, exit frees it. mmap carries on above it. + if (interp[0]) { + myproc()->vma[0].addr = INTERP_BASE; + myproc()->vma[0].length = PGROUNDUP(interp_end); + myproc()->vma[0].used = 1; + myproc()->vma[0].no_free = 0; + myproc()->vma[0].shmid = -1; + myproc()->mmap_next = INTERP_BASE + PGROUNDUP(interp_end); + } + myproc()->tf->rax = 0; myproc()->tf->rbx = 0; myproc()->tf->rdx = 0; @@ -373,8 +493,9 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) myproc()->tf->rdi = argc; myproc()->tf->rsi = sp + 8; - myproc()->tf->rip = elf.entry; - myproc()->tf->rcx = elf.entry; + uint64 entry = interp[0] ? interp_entry : base + elf.entry; + myproc()->tf->rip = entry; + myproc()->tf->rcx = entry; myproc()->tf->rsp = sp; // syscall_entry saves user RSP at kstack+PGSIZE-16 (not in the trapframe). @@ -404,9 +525,9 @@ do_exec(char *path, struct argsrc *av, struct argsrc *ev) kfree(bounce); if(pgdir) freevm(pgdir); - if(ip){ + if(ip) iunlockput(ip); + if(in_op) end_op(); - } return -1; } diff --git a/kernel/sys/sys_mem.c b/kernel/sys/sys_mem.c index 4c171fd..6108fff 100644 --- a/kernel/sys/sys_mem.c +++ b/kernel/sys/sys_mem.c @@ -76,6 +76,41 @@ vma_note_anon(struct proc *p, uint64 va, uint64 len) return 0; } +// Record [va, va+len) as anonymous memory wherever no mapping already covers +// it, for a MAP_FIXED that may land anywhere: inside an earlier mapping, across +// the end of one, or in a gap. Below sz is the program image and its heap, +// which fork copies as [0, sz) and needs no record -- a second one there would +// have fork map those pages twice. +static int +vma_cover_anon(struct proc *p, uint64 va, uint64 len) +{ + uint64 cur = va, end = va + len; + + if (cur < PGROUNDUP(p->sz)) + cur = PGROUNDUP(p->sz); + while (cur < end) { + uint64 next = end; + int inside = -1; + for (int i = 0; i < NVMA; i++) { + struct vma *v = &p->vma[i]; + if (!v->used) + continue; + if (v->addr <= cur && cur < v->addr + v->length) + inside = i; + else if (v->addr > cur && v->addr < next) + next = v->addr; + } + if (inside >= 0) { + cur = p->vma[inside].addr + p->vma[inside].length; + continue; + } + if (vma_note_anon(p, cur, next - cur) < 0) + return -ENOMEM; + cur = next; + } + return 0; +} + addr_t sys_sbrk(void) { @@ -127,11 +162,34 @@ sys_mmap(void) return va; for (uint64 a = va; a < va + length; a += PGSIZE) { char *mem = kalloc(); - if (!mem) return -1; + if (!mem) return -ENOMEM; memset(mem, 0, PGSIZE); - if (mappages(myproc()->pgdir, (void*)a, PGSIZE, V2P((uint64)mem), PTE_W | PTE_U) < 0) - return -1; + if (mappages(myproc()->pgdir, (void*)a, PGSIZE, V2P((uint64)mem), PTE_W | PTE_U) < 0) { + kfree(mem); + return -ENOMEM; + } } + + // The file, when there is one. This path used to ignore it and hand + // back zeroed pages whatever fd said -- and a dynamic linker maps + // every segment of every library after the first exactly this way, + // MAP_FIXED at an offset into the file, so libc came up as a library + // of zeros. A private copy, as in the non-fixed path below. + if (f && f->type == FD_INODE && f->ip->type != T_DEV && + !(flags & MAP_ANONYMOUS)) { + if ((flags & MAP_SHARED) && (prot & PROT_WRITE)) + return -ENODEV; + if (!f->readable) + return -EACCES; + ilock(f->ip); + readi(f->ip, 0, va, offset, length); + iunlock(f->ip); + } + + // And remembered. Without a record fork() did not copy these pages, + // so a child of a dynamically linked program had no libc. + if (vma_cover_anon(myproc(), va, length) < 0) + return -ENOMEM; return va; } diff --git a/ports/musl/Makefile b/ports/musl/Makefile index 8bb485e..2fc5b41 100644 --- a/ports/musl/Makefile +++ b/ports/musl/Makefile @@ -31,11 +31,22 @@ $(SRCDIR)/.patched: patches/0001-schos-tls.patch | $(SRCDIR) cd $(SRCDIR) && patch -p1 < $(CURDIR)/patches/0001-schos-tls.patch touch $@ -$(BUILDDIR)/config.mak: $(SRCDIR)/.patched +# Shared as well as static: libc.so is also the dynamic linker, and it is +# what a dynamically linked program names as its interpreter. Every port +# still links -static on purpose; this only makes the other kind possible. +# +# --syslibdir into the prefix, not the default /lib: `make install` puts a +# symlink to the loader there, and the default is the *host's* /lib. The copy +# the OS ships is placed by the sysroot rule below. +# +# Reconfigured when this file changes, so a tree configured before shared +# libraries were turned on does not quietly stay static-only. +$(BUILDDIR)/config.mak: $(SRCDIR)/.patched Makefile + rm -rf $(BUILDDIR) mkdir -p $(BUILDDIR) cd $(BUILDDIR) && $(SRCDIR)/configure \ --prefix=$(PREFIX) \ - --disable-shared \ + --syslibdir=$(PREFIX)/lib \ CC=$(CC) \ CFLAGS="-D__SCHOS__ -fno-stack-protector" @@ -45,7 +56,14 @@ $(BUILDDIR)/lib/libc.a: $(BUILDDIR)/config.mak $(PREFIX)/lib/libc.a: $(BUILDDIR)/lib/libc.a $(MAKE) -C $(BUILDDIR) install -install: $(PREFIX)/lib/libc.a +# The dynamic linker, under the name every dynamically linked x86_64 musl +# program asks for. A copy rather than the prefix's symlink: that one points +# at a path on the build host. +$(SYSROOT)/lib/ld-musl-x86_64.so.1: $(PREFIX)/lib/libc.a + mkdir -p $(SYSROOT)/lib + cp $(PREFIX)/lib/libc.so $@ + +install: $(PREFIX)/lib/libc.a $(SYSROOT)/lib/ld-musl-x86_64.so.1 clean: rm -rf $(PORTDIR)