diff --git a/userland/capsule_linux/src/linux/boot_guest.rs b/userland/capsule_linux/src/linux/boot_guest.rs index 8b604395b..9c30233ea 100644 --- a/userland/capsule_linux/src/linux/boot_guest.rs +++ b/userland/capsule_linux/src/linux/boot_guest.rs @@ -54,7 +54,13 @@ fn read_when_ready() -> Option> { if !super::settle::wait_settled() { return None; } - match store_read(&key(BOOT_GUEST), MAX_NAME) { + /* One byte past the limit is asked for: the store cuts a longer file + * short without saying so, and a cut line is an argument never given. */ + match store_read(&key(BOOT_GUEST), MAX_NAME + 1) { + Ok(named) if named.len() > MAX_NAME as usize => { + say(b"[LINUX] boot guest refused: its file is over 1024 bytes\n"); + None + } Ok(named) => Some(named), Err("vfs open failed") => None, Err(_) => { diff --git a/userland/capsule_linux/src/linux/call/console.rs b/userland/capsule_linux/src/linux/call/console.rs index f70884604..fe37c6f69 100644 --- a/userland/capsule_linux/src/linux/call/console.rs +++ b/userland/capsule_linux/src/linux/call/console.rs @@ -23,8 +23,17 @@ use crate::linux::guest::Guest; /// Cap on one transfer, matching the kernel's own peer-copy ceiling. const MAX_IO: u64 = 1 << 20; +/* The longest line the kernel's debug channel takes; it refuses a longer one + * whole (src/syscall/microkernel/debug.rs MAX_LEN). */ +const MAX_LINE: usize = 256; + /// A guest's console output, carried to the host's log. The bytes are the /// guest's and are never interpreted, only forwarded. +/* + * Forwarded in pieces the kernel takes. The count returned is what was + * carried: a write the log refuses part way is a short write, as Linux + * reports one, never a claimed success. + */ pub(super) fn console(guest: &Guest, buf: u64, len: u64) -> u64 { if len == 0 { return errno::ok(0); @@ -33,7 +42,16 @@ pub(super) fn console(guest: &Guest, buf: u64, len: u64) -> u64 { let Some(bytes) = guest.read(buf, take as usize) else { return errno::fail(errno::EFAULT); }; - let _ = nonos_libc::mk_debug(bytes.as_ptr(), bytes.len()); - errno::ok(take) + let mut done = 0; + for piece in bytes.chunks(MAX_LINE) { + if nonos_libc::mk_debug(piece.as_ptr(), piece.len()) < 0 { + break; + } + done += piece.len(); + } + match done { + 0 => errno::fail(errno::EIO), + n => errno::ok(n as u64), + } } diff --git a/userland/capsule_linux/src/linux/heap.rs b/userland/capsule_linux/src/linux/heap.rs index b669aa944..13ccfd162 100644 --- a/userland/capsule_linux/src/linux/heap.rs +++ b/userland/capsule_linux/src/linux/heap.rs @@ -21,9 +21,19 @@ use nonos_libc::{heap_init, heap_init_sized, mk_args}; /// An install holds a distribution's index while it resolves a closure. /// Kali's main is 21 MB fetched and 85 MB inflated, parsed into records -/// beside it; Alpine's is a few. A run takes the default. +/// beside it; Alpine's is a few. A run takes RUN_HEAP. const INSTALL_HEAP: usize = 320 << 20; +/* + * A run reads each program it starts or execs whole into one buffer that + * doubles as it fills, and the kernel verifies a program of up to 16 MiB + * (capsule_load/copy.rs MAX_ARTIFACT): 8 MiB outgrown beside 16 MiB at the + * peak. On top of that, the 16 MiB every run held before, which served guests + * whose programs are a few MB; a 4.9 MB Go program's execve ended the whole + * family there, failing to allocate its 8 MiB buffer. + */ +const RUN_HEAP: usize = (16 << 20) + (24 << 20); + pub fn init() { let mut buf = [0u8; 256]; let n = mk_args(buf.as_mut_ptr(), buf.len()); @@ -36,5 +46,10 @@ pub fn init() { let line = b"[LINUX] no room for a large index, installing in the default heap\n"; let _ = nonos_libc::mk_debug(line.as_ptr(), line.len()); } + if heap_init_sized(RUN_HEAP).is_ok() { + return; + } + let line = b"[LINUX] no room for a 40 MiB heap, running in the default 16 MiB\n"; + let _ = nonos_libc::mk_debug(line.as_ptr(), line.len()); let _ = heap_init(); } diff --git a/userland/capsule_linux/src/linux/start.rs b/userland/capsule_linux/src/linux/start.rs index d7b1d9b38..034693388 100644 --- a/userland/capsule_linux/src/linux/start.rs +++ b/userland/capsule_linux/src/linux/start.rs @@ -54,7 +54,7 @@ pub fn run() -> ! { } let mut guest = Guest::new(pid as u32); guest.links = alloc::rc::Rc::new(super::guest::Links::load()); - let code = match start(&mut guest, &launch) { + let code = match start(&mut guest, launch) { Ok(()) => { say(b"[LINUX] guest running\n"); serve(guest) diff --git a/userland/capsule_linux/src/linux/start_guest.rs b/userland/capsule_linux/src/linux/start_guest.rs index c4802a5ee..094c1cc23 100644 --- a/userland/capsule_linux/src/linux/start_guest.rs +++ b/userland/capsule_linux/src/linux/start_guest.rs @@ -25,7 +25,12 @@ use super::launch::Launch; use super::origin::Origin; use super::start::say; -pub(super) fn start(guest: &mut Guest, launch: &Launch) -> Result<(), &'static [u8]> { +/* + * The launch is taken, not borrowed: once the program is mapped its bytes are + * never read again, and kept they would hold up to 16 MiB of the heap that + * every later execve reads its own program into. They go when this returns. + */ +pub(super) fn start(guest: &mut Guest, launch: Launch) -> Result<(), &'static [u8]> { let (path, bytes) = (&launch.path[..], &launch.bytes[..]); if let Err(why) = prove(path, bytes, &launch.origin) { say(b"[LINUX] refused: "); diff --git a/userland/linux_guests/GoSuite.mk b/userland/linux_guests/GoSuite.mk new file mode 100644 index 000000000..269a77422 --- /dev/null +++ b/userland/linux_guests/GoSuite.mk @@ -0,0 +1,38 @@ +# The Go standard-library suite's guests, included by Guests.mk. + +# Go's own standard-library tests as guests, opt in with NONOS_LINUX_GO_SUITE=1, +# so the default build does not grow: each test binary is the one `go test -c` +# makes, and the same bytes are run on the build host for comparison. +# NONOS_LINUX_GO_SUITE_PKGS names the packages enrolled; each is the guest +# gs, ids 5042 upward in list order, 29 at most in +# the 5040 to 5099 range. gostd (5040), a static C program, changes to the +# package's directory and becomes its test binary, since go test runs each one +# there, beside testdata/. +ifeq ($(NONOS_LINUX_GO_SUITE),1) +NONOS_LINUX_GO_SUITE_PKGS ?= sync time os +GO_STD_OUT := $(TARGET_DIR)/linux-guests/go-std +GO_ROOT := $(shell $(GO) env GOROOT) +ifneq ($(word 30,$(NONOS_LINUX_GO_SUITE_PKGS)),) +$(error NONOS_LINUX_GO_SUITE_PKGS names more than the 29 packages ids 5042 to 5099 hold) +endif +$(GO_STD_OUT)/%.test: $(GO) + @mkdir -p $(@D) && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + GOCACHE=$(abspath $(GO_OUT))/cache GOPATH=$(abspath $(GO_OUT))/path \ + $(GO) test -c -o $(abspath $@) $(subst _,/,$*) +$(LINUX_GUESTS_C)/gostd: $(LINUX_GUESTS_DIR)/go/std/gostd.c + @mkdir -p $(@D) && musl-gcc -O2 -static -o $@ $< +$(eval $(call LINUX_GUEST,gostd,5040,5041,$(LINUX_GUESTS_C)/gostd)) +$(foreach i,$(shell seq 1 $(words $(NONOS_LINUX_GO_SUITE_PKGS))),$(eval $(call LINUX_GUEST,gs$(subst /,,$(word $(i),$(NONOS_LINUX_GO_SUITE_PKGS))),$(shell expr 5040 + 2 \* $(i)),$(shell expr 5041 + 2 \* $(i)),$(GO_STD_OUT)/$(subst /,_,$(word $(i),$(NONOS_LINUX_GO_SUITE_PKGS))).test))) +# A 12 MB program that execs itself: it runs only if the personality lets the +# first program's bytes go once it is running. It takes the ids after the +# packages, so the list is one shorter when it is asked for. +ifeq ($(NONOS_LINUX_GO_SUITE_EXECBIG),1) +GO_SUITE_EXECBIG_ID := $(shell expr 5042 + 2 \* $(words $(NONOS_LINUX_GO_SUITE_PKGS))) +ifneq ($(shell test $(GO_SUITE_EXECBIG_ID) -le 5098 && echo ok),ok) +$(error NONOS_LINUX_GO_SUITE_EXECBIG=1 needs a free id pair; name at most 28 packages) +endif +$(LINUX_GUESTS_C)/execbig: $(LINUX_GUESTS_DIR)/go/std/execbig.c + @mkdir -p $(@D) && musl-gcc -O2 -static -o $@ $< +$(eval $(call LINUX_GUEST,execbig,$(GO_SUITE_EXECBIG_ID),$(shell expr $(GO_SUITE_EXECBIG_ID) + 1),$(LINUX_GUESTS_C)/execbig)) +endif +endif diff --git a/userland/linux_guests/GuestFiles.mk b/userland/linux_guests/GuestFiles.mk index 75a418455..035c227b6 100644 --- a/userland/linux_guests/GuestFiles.mk +++ b/userland/linux_guests/GuestFiles.mk @@ -41,3 +41,35 @@ LINUX_GUEST_STORE_ENTRIES += --entry /linux/lib/libprobe_bad.so=$(LINUX_GUEST_BA --entry /linux/lib/libprobe_bad.so.nonos_id_cert.bin=$(linux-guest-libprobe_CERT) \ --entry /linux/lib/libprobe_bad.so.manifest.bin=$(linux-guest-libprobe_MANIFEST) \ --entry /linux/lib/libprobe_bad.so.zk_trailer.bin=$(linux-guest-libprobe_ATTESTATION) + +# A Go suite image holds the wrapper, the packages NONOS_LINUX_GO_SUITE_STORE +# names (all enrolled ones unless narrowed; the word execbig adds that proof), +# each package's testdata/ and sources at the paths they have on the build +# host, and Go's zone database, and nothing else: one test binary is 4 to 15 +# MB against the store's 16 MiB and 128 entries (tools/nonos-store-pack). +# Changing this list needs only the store step. +ifeq ($(NONOS_LINUX_GO_SUITE),1) +NONOS_LINUX_GO_SUITE_STORE ?= $(NONOS_LINUX_GO_SUITE_PKGS) +GO_SUITE_ENTRY = --entry /linux/bin/$(1)=$(linux-guest-$(1)_BIN) \ + --entry /linux/bin/$(1).nonos_id_cert.bin=$(linux-guest-$(1)_CERT) \ + --entry /linux/bin/$(1).manifest.bin=$(linux-guest-$(1)_MANIFEST) \ + --entry /linux/bin/$(1).zk_trailer.bin=$(linux-guest-$(1)_ATTESTATION) +# A test also reads its own sources: an example reads example_test.go, and +# the package directory exists for gostd to change into only if something is +# in it. NONOS_LINUX_GO_SUITE_SOURCES=0 leaves them out for a package whose +# testdata alone nearly fills the 128 entries (runtime). +NONOS_LINUX_GO_SUITE_SOURCES ?= 1 +GO_SUITE_SOURCES = $(if $(filter 1,$(NONOS_LINUX_GO_SUITE_SOURCES)),$(call GO_SUITE_GO,$(1))) +# Every .go file where they fit, since a test may glob or read the package's +# other sources (io/fs TestGlob reads glob.go); otherwise only *_test.go (os +# and syscall have 153 and 298 files against the 128 entries). +GO_SUITE_ALL = $(notdir $(wildcard $(GO_ROOT)/src/$(1)/*.go)) +GO_SUITE_GO = $(if $(word 100,$(GO_SUITE_ALL)),$(notdir $(wildcard $(GO_ROOT)/src/$(1)/*_test.go)),$(GO_SUITE_ALL)) +GO_SUITE_TESTDATA = $(foreach f,$(shell cd $(GO_ROOT)/src/$(1) && find testdata -type f 2>/dev/null | sort) $(GO_SUITE_SOURCES), \ + --entry /linux$(GO_ROOT)/src/$(1)/$(f)=$(GO_ROOT)/src/$(1)/$(f)) +LINUX_GUEST_STORE_ENTRIES := $(call GO_SUITE_ENTRY,gostd) \ + $(foreach p,$(filter-out execbig,$(NONOS_LINUX_GO_SUITE_STORE)),$(call GO_SUITE_ENTRY,gs$(subst /,,$(p))) $(call GO_SUITE_TESTDATA,$(p))) \ + --entry /linux$(GO_ROOT)/lib/time/zoneinfo.zip=$(GO_ROOT)/lib/time/zoneinfo.zip \ + --entry /linux/etc/nonos-boot-guest=$(LINUX_GUEST_BOOT_FILE) \ + $(if $(filter execbig,$(NONOS_LINUX_GO_SUITE_STORE)),$(call GO_SUITE_ENTRY,execbig)) +endif diff --git a/userland/linux_guests/Guests.mk b/userland/linux_guests/Guests.mk index c42c8e873..601c60e4b 100644 --- a/userland/linux_guests/Guests.mk +++ b/userland/linux_guests/Guests.mk @@ -119,6 +119,9 @@ $(LINUX_GUESTS_C)/cwait: $(LINUX_GUESTS_DIR)/c/cwait.c @mkdir -p $(@D) && musl-gcc -O2 -static -o $@ $< $(eval $(call LINUX_GUEST,cwait,4978,4979,$(LINUX_GUESTS_C)/cwait)) +# Go's own standard-library tests as guests, opt in (GoSuite.mk). +include $(LINUX_GUESTS_DIR)/GoSuite.mk + # The Linux-guest test store is about guests, not the desktop's media and demo # capsules. Drop both so the signed guest set fits the vfs load budget; the # normal image, which does not set NONOS_LINUX_GUESTS, still ships them. diff --git a/userland/linux_guests/go/std/execbig.c b/userland/linux_guests/go/std/execbig.c new file mode 100644 index 000000000..478210187 --- /dev/null +++ b/userland/linux_guests/go/std/execbig.c @@ -0,0 +1,31 @@ +/* + * A 12 MB program that execs itself once. The personality reads a program + * whole before it runs it, so this second read of the same 12 MB happens + * while the first program's bytes could still be held: it passes only if + * the personality let them go once the first one was running. + * + * /bin/execbig prints its first line, then execs /bin/execbig again + * /bin/execbig second prints its second line and ends with status 0 + */ +#include +#include +#include +#include + +/* Initialised, so all 12 MiB are in the file, not left for the loader. */ +static volatile char blob[12 << 20] = { 1 }; + +int main(int argc, char **argv) +{ + char *again[] = { "/bin/execbig", "second", NULL }; + + if (argc > 1 && strcmp(argv[1], "second") == 0) { + printf("[EXECBIG] second image running, first byte %d\n", blob[0]); + return 0; + } + printf("[EXECBIG] first image running, %zu bytes of data\n", sizeof(blob)); + fflush(stdout); + execve(again[0], again, NULL); + printf("[EXECBIG] execve refused: %s\n", strerror(errno)); + return 1; +} diff --git a/userland/linux_guests/go/std/gostd.c b/userland/linux_guests/go/std/gostd.c new file mode 100644 index 000000000..14dd7a712 --- /dev/null +++ b/userland/linux_guests/go/std/gostd.c @@ -0,0 +1,41 @@ +/* + * The start of a Go standard-library test inside the guest. go test runs each + * test binary from its package's directory, beside testdata/, and the boot + * guest always starts at /. So this changes to the directory named first and + * becomes the program named after it, with the rest as its arguments. + * NAME=value words between the two are added to the environment, as env(1) + * adds them, so a run can set GODEBUG or GOTRACEBACK: + * + * /bin/gostd /usr/local/go/src/time GODEBUG=schedtrace=1000 /bin/gstime -test.v + * + * It is C, not Go: a Go runtime here would take signals of its own before the + * test starts, and the test's own settings could not reach it. A refused chdir + * or execve is printed with its errno and ends with status 127, the shell's + * status for a program that could not be run. + */ +#include +#include +#include +#include +#include + +extern char **environ; + +int main(int argc, char **argv) +{ + int i = 2; + + if (argc < 3) { + fprintf(stderr, "[GOSTD] usage: gostd [NAME=value...] [args...]\n"); + return 127; + } + for (; i < argc - 1 && argv[i][0] != '/' && strchr(argv[i], '='); i++) + putenv(argv[i]); + if (chdir(argv[1]) != 0) { + fprintf(stderr, "[GOSTD] chdir %s: %s\n", argv[1], strerror(errno)); + return 127; + } + execve(argv[i], argv + i, environ); + fprintf(stderr, "[GOSTD] execve %s: %s\n", argv[i], strerror(errno)); + return 127; +}