Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion userland/capsule_linux/src/linux/boot_guest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ fn read_when_ready() -> Option<Vec<u8>> {
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(_) => {
Expand Down
22 changes: 20 additions & 2 deletions userland/capsule_linux/src/linux/call/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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),
}
}

17 changes: 16 additions & 1 deletion userland/capsule_linux/src/linux/heap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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();
}
2 changes: 1 addition & 1 deletion userland/capsule_linux/src/linux/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion userland/capsule_linux/src/linux/start_guest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: ");
Expand Down
38 changes: 38 additions & 0 deletions userland/linux_guests/GoSuite.mk
Original file line number Diff line number Diff line change
@@ -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<package without slashes>, 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
32 changes: 32 additions & 0 deletions userland/linux_guests/GuestFiles.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions userland/linux_guests/Guests.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions userland/linux_guests/go/std/execbig.c
Original file line number Diff line number Diff line change
@@ -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 <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

/* 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;
}
41 changes: 41 additions & 0 deletions userland/linux_guests/go/std/gostd.c
Original file line number Diff line number Diff line change
@@ -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 <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

extern char **environ;

int main(int argc, char **argv)
{
int i = 2;

if (argc < 3) {
fprintf(stderr, "[GOSTD] usage: gostd <dir> [NAME=value...] <program> [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;
}
Loading