From 0a68417d13d86caee7515a936fa742b65aae8987 Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Mon, 10 Nov 2025 09:28:17 -0500 Subject: [PATCH 01/11] hypercall for accessing config parameters --- guest-utils/scripts/get_config | 7 +++++++ pyplugins/core/core.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 guest-utils/scripts/get_config diff --git a/guest-utils/scripts/get_config b/guest-utils/scripts/get_config new file mode 100644 index 000000000..bac2866a3 --- /dev/null +++ b/guest-utils/scripts/get_config @@ -0,0 +1,7 @@ +#!/igloo/utils/sh +OUTPUT=$(/igloo/utils/send_hypercall get_config $@) +echo -n "$OUTPUT" +if [ "$OUTPUT" = "False" ] || [ "$OUTPUT" = "None" ] || [ "$OUTPUT" = "false" ] || [ "$OUTPUT" = "" ]; then + exit 1 +fi +exit 0 diff --git a/pyplugins/core/core.py b/pyplugins/core/core.py index dd08e44c2..6f8d18383 100644 --- a/pyplugins/core/core.py +++ b/pyplugins/core/core.py @@ -37,6 +37,8 @@ import signal import threading import time +from typing import Tuple +from collections.abc import Mapping, Sequence from penguin import Plugin, yaml, plugins from penguin.defaults import vnc_password @@ -66,6 +68,7 @@ def __init__(self) -> None: plugs = self.get_arg("plugins") conf = self.get_arg("conf") + self.config = conf.args # since the config is an ArgsBox telnet_port = self.get_arg("telnet_port") @@ -151,6 +154,9 @@ def __init__(self) -> None: with open(os.path.join(self.outdir, "core_config.yaml"), "w") as f: f.write(yaml.dump(self.get_arg("conf").args)) + # Set up hypercall handler for config access from guest + plugins.send_hypercall.subscribe("get_config", self.get_config) + signal.signal(signal.SIGUSR1, self.graceful_shutdown) # Load the "timeout" plugin which is a misnomer - it's just going @@ -260,3 +266,28 @@ def uninit(self) -> None: if hasattr(self, "shutdown_event") and not self.shutdown_event.is_set(): # Tell the shutdown thread to exit if it was started self.shutdown_event.set() + + def get_config(self, input: str) -> Tuple[int, str]: + """ + Config accessor used by the guest + """ + keys = input.split('.') + current = self.config + + for key in keys: + try: + if isinstance(current, Mapping) and key in current: + current = current[key] + elif isinstance(current, Sequence) and not isinstance(current, str): + try: + index = int(key) + current = current[index] + except (ValueError, IndexError): + return 0, "" + elif hasattr(current, key): + current = getattr(current, key) + else: + return 0, "" + except (KeyError, AttributeError, TypeError): + return 0, "" + return 1, str(current)[:0x1000] # send_hypercall has a 4096 byte output buffer From 9453bbf5e37e45d3704485747d7bddc97dafcea6 Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Mon, 10 Nov 2025 17:12:55 -0500 Subject: [PATCH 02/11] replace environment variables in guest with hypercall for config options --- guest-utils/ltrace/inject_ltrace.c | 79 ++++++++++++++----- guest-utils/scripts/igloo_profile | 1 + pyplugins/core/core.py | 32 ++++---- src/penguin/penguin_config/structure.py | 12 ++- src/resources/source.d/40_mount_shared_dir.sh | 3 +- .../source.d/50_launch_root_shell.sh | 3 +- src/resources/source.d/55_force_www.sh | 3 +- src/resources/source.d/90_enable_strace.sh | 3 +- 8 files changed, 91 insertions(+), 45 deletions(-) diff --git a/guest-utils/ltrace/inject_ltrace.c b/guest-utils/ltrace/inject_ltrace.c index 35481e68b..fbd1e1c21 100644 --- a/guest-utils/ltrace/inject_ltrace.c +++ b/guest-utils/ltrace/inject_ltrace.c @@ -10,7 +10,8 @@ __attribute__((constructor)) void igloo_start_ltrace(void) { // Don't do anything if the user doesn't want to ltrace - if (!getenv("IGLOO_LTRACE")) { + int status = system("/igloo/utils/get_config core.ltrace > /dev/null 2>&1"); + if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0)) { return; } @@ -40,23 +41,65 @@ __attribute__((constructor)) void igloo_start_ltrace(void) } // Don't do anything if the user doesn't want to ltrace this process - char *excluded_cmds = getenv("IGLOO_LTRACE_EXCLUDED"); - if (excluded_cmds) { - bool excluded = false; - excluded_cmds = strdup(excluded_cmds); - char *tok = strtok(excluded_cmds, ","); - while (tok) { - if (!strcmp(tok, comm)) { - excluded = true; - break; - } - tok = strtok(NULL, ","); - } - free(excluded_cmds); - if (excluded) { - return; - } - } + bool should_trace = true; + + // Check include list first + FILE *include_fp = popen("/igloo/utils/get_config core.ltrace.include 2>/dev/null", "r"); + if (include_fp) { + char included_cmds[1024]; + if (fgets(included_cmds, sizeof(included_cmds), include_fp)) { + if (included_cmds[strlen(included_cmds) - 1] == '\n') { + included_cmds[strlen(included_cmds) - 1] = '\0'; + } + + // If there's an include list, default to false and only trace if included + should_trace = false; + char *included_copy = strdup(included_cmds); + char *tok = strtok(included_copy, ","); + while (tok) { + if (!strcmp(tok, comm)) { + should_trace = true; + break; + } + tok = strtok(NULL, ","); + } + free(included_copy); + } + pclose(include_fp); + } + + // If we're not supposed to trace based on include list, return early + if (!should_trace) { + return; + } + + // Check exclude list + FILE *exclude_fp = popen("/igloo/utils/get_config core.ltrace.exclude 2>/dev/null", "r"); + if (exclude_fp) { + char excluded_cmds[1024]; + if (fgets(excluded_cmds, sizeof(excluded_cmds), exclude_fp)) { + if (excluded_cmds[strlen(excluded_cmds) - 1] == '\n') { + excluded_cmds[strlen(excluded_cmds) - 1] = '\0'; + } + + bool excluded = false; + char *excluded_copy = strdup(excluded_cmds); + char *tok = strtok(excluded_copy, ","); + while (tok) { + if (!strcmp(tok, comm)) { + excluded = true; + break; + } + tok = strtok(NULL, ","); + } + free(excluded_copy); + if (excluded) { + pclose(exclude_fp); + return; + } + } + pclose(exclude_fp); + } if (fork()) { // In parent, wait for child to set up tracing and then continue to the diff --git a/guest-utils/scripts/igloo_profile b/guest-utils/scripts/igloo_profile index 231c00f53..24d020638 100755 --- a/guest-utils/scripts/igloo_profile +++ b/guest-utils/scripts/igloo_profile @@ -4,6 +4,7 @@ done export PATH="/igloo/utils:$PATH" # Show project name in prompt if we have it, otherwise hostname +PROJ_NAME=$(/igloo/utils/get_config core.proj_name 2>/dev/null) if [ -z "${PROJ_NAME}" ]; then PROJ_NAME="\h" fi diff --git a/pyplugins/core/core.py b/pyplugins/core/core.py index 6f8d18383..e43e45887 100644 --- a/pyplugins/core/core.py +++ b/pyplugins/core/core.py @@ -10,7 +10,6 @@ - Handling SIGUSR1 for graceful shutdown of emulation. - Creating a `.ran` file in the output directory after a non-crash shutdown. - Optionally enforcing a timeout for emulation and shutting down after the specified period. -- Setting up environment variables for features like root shell, graphics, shared directory, strace, ltrace, and forced WWW. - Logging information about available services (e.g., root shell, VNC) based on configuration and environment. Arguments @@ -23,8 +22,6 @@ This plugin does not provide a direct interface for other plugins, but it writes configuration and plugin information to files in the output directory, which other plugins or tools may read. -It also sets environment variables in the configuration dictionary that may be used by other -components or plugins. Overall Purpose --------------- @@ -85,10 +82,7 @@ def __init__(self) -> None: if hasattr(p, "ensure_init"): p.ensure_init() - # If we have an option of root_shell we need to add ROOT_SHELL=1 into env - # so that the init script knows to start a root shell if conf["core"].get("root_shell", False): - conf["env"]["ROOT_SHELL"] = "1" # Print port info if container_ip := os.environ.get("CONTAINER_IP", None): self.logger.info( @@ -123,15 +117,21 @@ def __init__(self) -> None: f"VNC @ {container_ip}:5900 with password '{vnc_password}'" ) - # Same thing, but for a shared directory - if conf["core"].get("shared_dir", False): - conf["env"]["SHARED_DIR"] = "1" + # Warn if env is set for any of the old options. Can happen with old configs. + legacy_env_vars = ["ROOT_SHELL", "SHARED_DIR", "STRACE", "IGLOO_LTRACE", "WWW", "PROJ_NAME"] + core_env = conf["core"].get("env", {}) - if conf["core"].get("strace", False) is True: - conf["env"]["STRACE"] = "1" + found_legacy_vars = [] + for var in legacy_env_vars: + if var in core_env: + found_legacy_vars.append(var) - if conf["core"].get("ltrace", False) is True: - conf["env"]["IGLOO_LTRACE"] = "1" + if found_legacy_vars: + self.logger.warning( + f"Legacy environment variables found in core.env: {', '.join(found_legacy_vars)}. " + "This likely indicates you are running an old project and this message can safely be ignored." + "However, if you have set them intentionally, be aware they will stop working in the future." + ) if conf["core"].get("force_www", False): if conf.get("static_files", {}).get( @@ -139,12 +139,10 @@ def __init__(self) -> None: self.logger.warning( "Force WWW unavailable - no webservers were statically identified (/igloo/utils/www_cmds is empty)" ) - else: - conf["env"]["WWW"] = "1" - # Add PROJ_NAME into env based on dirname of config + # Add proj_name to config based on dirname of config (kinda evil) if proj_name := self.get_arg("proj_name"): - conf["env"]["PROJ_NAME"] = proj_name + conf["core"]["proj_name"] = proj_name # Record loaded plugins with open(os.path.join(self.outdir, "core_plugins.yaml"), "w") as f: diff --git a/src/penguin/penguin_config/structure.py b/src/penguin/penguin_config/structure.py index 1453a91fc..a2647b61e 100644 --- a/src/penguin/penguin_config/structure.py +++ b/src/penguin/penguin_config/structure.py @@ -157,15 +157,23 @@ class Core(PartialModelMixin, BaseModel): ), ] ltrace: Annotated[ - Union[bool, list[str]], + Union[bool, list[str], dict], Field( False, title="Enable ltrace", description=" ".join(( "If true, run ltrace for entire system starting from init.", "If names of programs, enable ltrace only for those programs.", + "If dict with 'include' and/or 'exclude' keys, specify programs to trace or exclude.", )), - examples=[False, True, ["lighttpd"]], + examples=[ + False, + True, + ["lighttpd"], + {"include": ["lighttpd"]}, + {"exclude": ["busybox", "sh"]}, + {"include": ["lighttpd"], "exclude": ["busybox"]} + ], ), ] gdbserver: Optional[GDBServerPrograms] = None diff --git a/src/resources/source.d/40_mount_shared_dir.sh b/src/resources/source.d/40_mount_shared_dir.sh index 8a00c060f..c07922498 100644 --- a/src/resources/source.d/40_mount_shared_dir.sh +++ b/src/resources/source.d/40_mount_shared_dir.sh @@ -1,5 +1,4 @@ -if [ ! -z "${SHARED_DIR}" ]; then - unset SHARED_DIR +if /igloo/utils/get_config core.shared_dir > /dev/null 2>&1; then /igloo/utils/busybox mkdir /igloo/shared echo '[IGLOO INIT] Mounting shared directory'; /igloo/utils/busybox mount -t 9p -o trans=virtio igloo_shared_dir /igloo/shared -oversion=9p2000.L,posixacl,msize=8192000 diff --git a/src/resources/source.d/50_launch_root_shell.sh b/src/resources/source.d/50_launch_root_shell.sh index e6864f70b..b4ea61115 100644 --- a/src/resources/source.d/50_launch_root_shell.sh +++ b/src/resources/source.d/50_launch_root_shell.sh @@ -1,5 +1,4 @@ -if [ ! -z "${ROOT_SHELL}" ]; then +if /igloo/utils/get_config core.root_shell > /dev/null 2>&1; then echo '[IGLOO INIT] Launching root shell'; ENV=/igloo/utils/igloo_profile /igloo/utils/console & - unset ROOT_SHELL fi diff --git a/src/resources/source.d/55_force_www.sh b/src/resources/source.d/55_force_www.sh index ac4904d31..b7afb2fdb 100644 --- a/src/resources/source.d/55_force_www.sh +++ b/src/resources/source.d/55_force_www.sh @@ -1,7 +1,6 @@ -if [ ! -z "${WWW}" ]; then +if /igloo/utils/get_config core.force_www > /dev/null 2>&1; then if [ -e /igloo/utils/www_cmds ]; then echo '[IGLOO INIT] Force-launching webserver commands'; /igloo/utils/sh /igloo/utils/www_cmds & fi - unset WWW fi diff --git a/src/resources/source.d/90_enable_strace.sh b/src/resources/source.d/90_enable_strace.sh index 3ec691ea4..289b78a17 100644 --- a/src/resources/source.d/90_enable_strace.sh +++ b/src/resources/source.d/90_enable_strace.sh @@ -1,6 +1,5 @@ -if [ ! -z "${STRACE}" ]; then +if /igloo/utils/get_config core.strace > /dev/null 2>&1; then # Strace init in the background (to follow through the exec) /igloo/utils/sh -c "/igloo/utils/strace -f -p 1" & /igloo/utils/sleep 1 - unset STRACE fi From 9e664c75d050f982b3e85bc61c79ce4880606eea Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Thu, 20 Nov 2025 13:35:30 -0500 Subject: [PATCH 03/11] broken portalcall version of getting config --- guest-utils/ltrace/inject_ltrace.c | 6 +- guest-utils/native/Makefile | 4 + guest-utils/native/get_config.c | 265 ++++++++++++++++++++++++ guest-utils/scripts/get_config | 7 - pyplugins/core/core.py | 28 +-- pyplugins/interventions/nvram2.py | 2 + src/penguin/penguin_config/structure.py | 17 ++ 7 files changed, 308 insertions(+), 21 deletions(-) create mode 100644 guest-utils/native/get_config.c delete mode 100644 guest-utils/scripts/get_config diff --git a/guest-utils/ltrace/inject_ltrace.c b/guest-utils/ltrace/inject_ltrace.c index fbd1e1c21..9a1114151 100644 --- a/guest-utils/ltrace/inject_ltrace.c +++ b/guest-utils/ltrace/inject_ltrace.c @@ -7,11 +7,13 @@ #include #include +int libinject_get_config_bool(const char *config_key); +int libinject_get_config(const char *key, char *output, unsigned long buf_size); + __attribute__((constructor)) void igloo_start_ltrace(void) { // Don't do anything if the user doesn't want to ltrace - int status = system("/igloo/utils/get_config core.ltrace > /dev/null 2>&1"); - if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0)) { + if(!libinject_get_config_bool("core.ltrace")) { return; } diff --git a/guest-utils/native/Makefile b/guest-utils/native/Makefile index 172de3063..e32999959 100644 --- a/guest-utils/native/Makefile +++ b/guest-utils/native/Makefile @@ -84,6 +84,10 @@ out/%/test_ioctl_interaction: test_ioctl_interaction.c @mkdir -p $(dir $@) $(CC_$*) $(CFLAGS_$*) $< -o $@ +out/%/get_config: get_config.c + @mkdir -p $(dir $@) + $(CC_$*) $(CFLAGS_$*) $< -o $@ + out/%/test_nvram: test_nvram.c @mkdir -p $(dir $@) $(CC_$*) $(CFLAGS_DYNAMIC_$*) -Wl,--dynamic-linker=/igloo/dylibs/ld-musl-$*.so.1 $< -o $@ diff --git a/guest-utils/native/get_config.c b/guest-utils/native/get_config.c new file mode 100644 index 000000000..0ea03f097 --- /dev/null +++ b/guest-utils/native/get_config.c @@ -0,0 +1,265 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "portal_call.h" + +#define GET_CONFIG_MAGIC 0x6E7C04F0 +#define BUFFER_SIZE 4096 +#define CONFIG_CACHE_DIR "/igloo/config_tmpfs" +#define CONFIG_LOCK_FILE CONFIG_CACHE_DIR ".lock" + +#define DEBUG 0 + +#define PRINT_MSG(fmt, ...) do { if (DEBUG) { fprintf(stderr, "%s: "fmt, __FUNCTION__, __VA_ARGS__); } } while (0) + +static void require(bool condition, const char *s) +{ + if (!condition) { + fprintf(stderr, "get_config: error: %s\n", s); + exit(1); + } +} + +static int _libinject_flock_asm(int fd, int op) { + // File lock with SYS_flock. We do this in assembly + // for portability - libc may not be available / match versions + // with the library we're building + int retval; +#if defined(__mips64__) + asm volatile( + "daddiu $a0, %1, 0\n" // Move fd to $a0 + "daddiu $a1, %2, 0\n" // Move op to $a1 + "li $v0, %3\n" // Load SYS_flock (the system call number) into $v0 + "syscall\n" // Make the system call + "move %0, $v0\n" // Move the result from $v0 to retval + : "=r" (retval) // Output + : "r" (fd), "r" (op), "i" (SYS_flock) // Inputs + : "v0", "a0", "a1" // Clobber list +); +#elif defined(__mips__) + asm volatile( + "move $a0, %1\n" // Correctly move fd (from C variable) to $a0 + "move $a1, %2\n" // Correctly move op (from C variable) to $a1 + "li $v0, %3\n" // Load the syscall number for flock into $v0 + "syscall\n" // Perform the syscall + "move %0, $v0" // Move the result from $v0 to retval + : "=r" (retval) // Output + : "r" (fd), "r" (op), "i" (SYS_flock) // Inputs; "i" for immediate syscall number + : "v0", "a0", "a1" // Clobber list +); +#elif defined(__arm__) + asm volatile( + "mov r0, %1\n" // Move fd to r0, the first argument for the system call + "mov r1, %2\n" // Move op to r1, the second argument for the system call + "mov r7, %3\n" // Move SYS_flock (the system call number) to r7 + "svc 0x00000000\n" // Make the system call + "mov %[result], r0" // Move the result from r0 to retval + : [result]"=r" (retval) // Output + : "r"(fd), "r"(op), "i"(SYS_flock) // Inputs + : "r0", "r1", "r7" // Clobber list +); +#elif defined(__aarch64__) // AArch64 + // XXX: using %w registers for 32-bit movs. This made the compiler + // happy but I'm not sure why we can't be operating on 64-bit ints + asm volatile( + "mov w0, %w1\n" // Move fd to w0, the first argument for the system call + "mov w1, %w2\n" // Move op to w1, the second argument for the system call + "mov x8, %3\n" // Move SYS_flock (the system call number) to x8 + "svc 0\n" // Make the system call (Supervisor Call) + "mov %w0, w0\n" // Move the result from w0 to retval + : "=r" (retval) // Output + : "r" (fd), "r" (op), "i" (SYS_flock) // Inputs + : "x0", "x1", "x8" // Clobber list +); +#elif defined(__x86_64__) // x86_64 + // XXX: movl's for 32-bit movs. This made the compiler + // happy but I'm not sure why we can't be operating on 64-bit ints + // I think it should be fine though + asm volatile( + "movl %1, %%edi\n" // Move fd to rdi (1st argument) + "movl %2, %%esi\n" // Move op to rsi (2nd argument) + "movl %3, %%eax\n" // Move SYS_flock to rax (syscall number) + "syscall\n" // Make the syscall + "movl %%eax, %0\n" // Move the result from rax to retval + : "=r" (retval) // Output + : "r" (fd), "r" (op), "i" (SYS_flock) // Inputs + : "rax", "rdi", "rsi" // Clobber list +); +#elif defined(__i386__) // x86 32-bit + asm volatile( + "movl %1, %%ebx\n" // Move fd to ebx + "movl %2, %%ecx\n" // Move op to ecx + "movl %3, %%eax\n" // Move SYS_flock to eax + "int $0x80\n" // Make the syscall + "movl %%eax, %0\n" // Move the result from eax to retval + : "=r" (retval) // Output + : "r" (fd), "r" (op), "i" (SYS_flock) // Inputs + : "eax", "ebx", "ecx" // Clobber list +); +#elif defined(__powerpc__) || defined(__powerpc64__) + asm volatile( + "mr 3, %1\n" // Move fd to r3 (1st argument) + "mr 4, %2\n" // Move op to r4 (2nd argument) + "li 0, %3\n" // Load SYS_flock (the system call number) into r0 + "sc\n" // Make the system call + "mr %0, 3\n" // Move the result from r3 to retval + : "=r" (retval) // Output + : "r" (fd), "r" (op), "i" (SYS_flock) // Inputs + : "r0", "r3", "r4" // Clobber list +); +#elif defined(__riscv) + asm volatile( + "mv a0, %1\n" // Move fd to a0 (1st argument) + "mv a1, %2\n" // Move op to a1 (2nd argument) + "li a7, %3\n" // Load SYS_flock (the system call number) into a7 + "ecall\n" // Make the system call + "mv %0, a0\n" // Move the result from a0 to retval + : "=r" (retval) // Output + : "r" (fd), "r" (op), "i" (SYS_flock) // Inputs + : "a0", "a1", "a7" // Clobber list +); +#elif defined(__loongarch64) + asm volatile( + "move $a0, %1\n" // Move fd to $a0 (1st argument) + "move $a1, %2\n" // Move op to $a1 (2nd argument) + "addi.d $a7, $zero, %3\n" // Load SYS_flock (the system call number) into $a7 + "syscall 0\n" // Make the system call + "move %0, $a0\n" // Move the result from $a0 to retval + : "=r" (retval) // Output + : "r" (fd), "r" (op), "i" (SYS_flock) // Inputs + : "a0", "a1", "a7" // Clobber list +); +#else +#error "Unsupported architecture" +#endif + return retval; +} + +static int _libinject_config_lock() { + int lockfd; + + lockfd = open(CONFIG_LOCK_FILE, O_CREAT | O_RDWR, 0644); + if (lockfd < 0) { + PRINT_MSG("Lock file open failed, creating cache dir %s\n", CONFIG_CACHE_DIR); + if (mkdir(CONFIG_CACHE_DIR, 0755) == -1 && errno != EEXIST) { + PRINT_MSG("Failed to create config cache dir %s\n", CONFIG_CACHE_DIR); + return -1; + } + + if (mount("tmpfs", CONFIG_CACHE_DIR, "tmpfs", 0, NULL) == -1) { + PRINT_MSG("Failed to mount tmpfs at %s\n", CONFIG_CACHE_DIR); + } + + lockfd = open(CONFIG_LOCK_FILE, O_CREAT | O_RDWR, 0644); + if (lockfd < 0) { + PRINT_MSG("Still couldn't open lock file %s\n", CONFIG_LOCK_FILE); + return -1; + } + } + + if (_libinject_flock_asm(lockfd, LOCK_EX) < 0) { + PRINT_MSG("Couldn't lock %s\n", CONFIG_LOCK_FILE); + close(lockfd); + return -1; + } + + return lockfd; +} + +static void _libinject_config_unlock(int lockfd) { + if (lockfd >= 0) { + _libinject_flock_asm(lockfd, LOCK_UN); + close(lockfd); + } +} + +static int _libinject_mkdir_p(const char *path) { + char *tmp = strdup(path); + if (!tmp) return -1; + + size_t len = strlen(tmp); + if (tmp[len - 1] == '/') { + tmp[len - 1] = '\0'; + } + + for (char *p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + if (mkdir(tmp, 0755) == -1 && errno != EEXIST) { + free(tmp); + return -1; + } + *p = '/'; + } + } + + if (mkdir(tmp, 0755) == -1 && errno != EEXIST) { + free(tmp); + return -1; + } + + free(tmp); + return 0; +} + +static int value_to_bool(char *value) { + printf("value_to_bool: value='%s'\n", value); + if (!strncmp(value, "False", 5) || !strncmp(value, "None", 4) || + !strncmp(value, "false", 5) || !strncmp(value, "", 1)) { + return 1; + } + return 0; +} + +int libinject_get_config(const char *key, char *output, unsigned long buf_size) { + unsigned long rv; + PRINT_MSG("Getting config key '%s'\n", key); + rv = portal_call3(GET_CONFIG_MAGIC, (unsigned long) key, (unsigned long) output, buf_size); + PRINT_MSG("Got config key '%s' with return value %lu\n", key, rv); + return (int) rv; +} + +int libinject_get_config_int(const char *config_key) { + char *str = malloc(64); + int result; + if(!libinject_get_config(config_key, str, 64)) { + result = 0; + } else { + result = atoi(str); + } + free(str); + return result; +} + +int libinject_get_config_bool(const char *config_key) { + char *str = malloc(64); + int result; + PRINT_MSG("Getting bool config key '%s'\n", config_key); + libinject_get_config(config_key, str, 64); + result = value_to_bool(str); + PRINT_MSG("Got bool config key '%s' with value %s (bool %d)\n", config_key, str, result); + free(str); + return result; +} + +#ifndef GET_CONFIG_LIBRARY_ONLY +int main(int argc, char *argv[]) { + char *buffer; + int rv; + require(argc == 2, "Usage: get_config "); + buffer = malloc(BUFFER_SIZE); + require(buffer != NULL, "Failed to allocate memory"); + rv = libinject_get_config(argv[1], buffer, BUFFER_SIZE); + buffer[BUFFER_SIZE - 1] = 0; + printf("%s", buffer); + return !value_to_bool(buffer); // Return 0 for true values, 1 for false +} +#endif diff --git a/guest-utils/scripts/get_config b/guest-utils/scripts/get_config deleted file mode 100644 index bac2866a3..000000000 --- a/guest-utils/scripts/get_config +++ /dev/null @@ -1,7 +0,0 @@ -#!/igloo/utils/sh -OUTPUT=$(/igloo/utils/send_hypercall get_config $@) -echo -n "$OUTPUT" -if [ "$OUTPUT" = "False" ] || [ "$OUTPUT" = "None" ] || [ "$OUTPUT" = "false" ] || [ "$OUTPUT" = "" ]; then - exit 1 -fi -exit 0 diff --git a/pyplugins/core/core.py b/pyplugins/core/core.py index e43e45887..5518e1767 100644 --- a/pyplugins/core/core.py +++ b/pyplugins/core/core.py @@ -39,6 +39,8 @@ from penguin import Plugin, yaml, plugins from penguin.defaults import vnc_password +GET_CONFIG_MAGIC = 0x6E7C04F0 + class Core(Plugin): """ @@ -152,9 +154,6 @@ def __init__(self) -> None: with open(os.path.join(self.outdir, "core_config.yaml"), "w") as f: f.write(yaml.dump(self.get_arg("conf").args)) - # Set up hypercall handler for config access from guest - plugins.send_hypercall.subscribe("get_config", self.get_config) - signal.signal(signal.SIGUSR1, self.graceful_shutdown) # Load the "timeout" plugin which is a misnomer - it's just going @@ -265,15 +264,18 @@ def uninit(self) -> None: # Tell the shutdown thread to exit if it was started self.shutdown_event.set() - def get_config(self, input: str) -> Tuple[int, str]: + @plugins.portalcall.portalcall(GET_CONFIG_MAGIC) + def portalcall_getconfig(self, key_ptr, output_ptr, buf_size): """ Config accessor used by the guest """ - keys = input.split('.') + key = yield from plugins.mem.read_str(key_ptr) + keys = key.split('.') current = self.config + self.logger.debug(f"get_config called for key: {key}") - for key in keys: - try: + try: + for key in keys: if isinstance(current, Mapping) and key in current: current = current[key] elif isinstance(current, Sequence) and not isinstance(current, str): @@ -281,11 +283,13 @@ def get_config(self, input: str) -> Tuple[int, str]: index = int(key) current = current[index] except (ValueError, IndexError): - return 0, "" + raise elif hasattr(current, key): current = getattr(current, key) else: - return 0, "" - except (KeyError, AttributeError, TypeError): - return 0, "" - return 1, str(current)[:0x1000] # send_hypercall has a 4096 byte output buffer + raise KeyError + value = str(current) + except (KeyError, AttributeError, TypeError): + value = "" + self.logger.debug(f"get_config found value {value} for key: {key}") + yield from plugins.mem.write_str(output_ptr, value[:buf_size-1]) diff --git a/pyplugins/interventions/nvram2.py b/pyplugins/interventions/nvram2.py index 9670baef1..2e049f533 100644 --- a/pyplugins/interventions/nvram2.py +++ b/pyplugins/interventions/nvram2.py @@ -76,7 +76,9 @@ def add_lib_inject_for_abi(config, abi, cache_dir): "-isystem", headers_dir, f"-DCONFIG_{libnvram_arch_name.upper()}=1", + "-DGET_CONFIG_LIBRARY_ONLY", "/igloo_static/libnvram/nvram.c", + "/igloo_static/guest-utils/native/get_config.c", "/igloo_static/guest-utils/ltrace/inject_ltrace.c", "--language", "c", diff --git a/src/penguin/penguin_config/structure.py b/src/penguin/penguin_config/structure.py index a2647b61e..4bd87fd04 100644 --- a/src/penguin/penguin_config/structure.py +++ b/src/penguin/penguin_config/structure.py @@ -300,6 +300,15 @@ class Core(PartialModelMixin, BaseModel): examples=[False, True], ), ] + init: Annotated[ + Optional[str], + Field( + None, + title="init script script", + description="Path to custom igloo init script to run during guest startup", + examples=["/igloo/utils/custom_init.sh", "scripts/my_init.sh"], + ), + ] EnvVal = _newtype( @@ -876,6 +885,14 @@ class Main(PartialModelMixin, BaseModel): static_files: StaticFiles plugins: Annotated[dict[str, Plugin], Field(title="Plugins")] network: Optional[Network] = None + internal: Annotated[ + Optional[dict], + Field( + None, + title="Internal runtime data", + description="Reserved for internal tool use - not part of the public API", + ), + ] Patch = create_partial_model(Main, recursive=True) From dceb4e061a4b740487dc412bf1c8b59bee9e245f Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Tue, 17 Feb 2026 18:03:24 -0500 Subject: [PATCH 04/11] progress on get_config portalcall --- guest-utils/ltrace/inject_ltrace.c | 95 ++++++++++++------------------ guest-utils/native/get_config.c | 1 - pyplugins/core/core.py | 19 ++++-- 3 files changed, 52 insertions(+), 63 deletions(-) diff --git a/guest-utils/ltrace/inject_ltrace.c b/guest-utils/ltrace/inject_ltrace.c index 9a1114151..e749ad336 100644 --- a/guest-utils/ltrace/inject_ltrace.c +++ b/guest-utils/ltrace/inject_ltrace.c @@ -46,62 +46,45 @@ __attribute__((constructor)) void igloo_start_ltrace(void) bool should_trace = true; // Check include list first - FILE *include_fp = popen("/igloo/utils/get_config core.ltrace.include 2>/dev/null", "r"); - if (include_fp) { - char included_cmds[1024]; - if (fgets(included_cmds, sizeof(included_cmds), include_fp)) { - if (included_cmds[strlen(included_cmds) - 1] == '\n') { - included_cmds[strlen(included_cmds) - 1] = '\0'; - } - - // If there's an include list, default to false and only trace if included - should_trace = false; - char *included_copy = strdup(included_cmds); - char *tok = strtok(included_copy, ","); - while (tok) { - if (!strcmp(tok, comm)) { - should_trace = true; - break; - } - tok = strtok(NULL, ","); - } - free(included_copy); - } - pclose(include_fp); - } - - // If we're not supposed to trace based on include list, return early - if (!should_trace) { - return; - } - - // Check exclude list - FILE *exclude_fp = popen("/igloo/utils/get_config core.ltrace.exclude 2>/dev/null", "r"); - if (exclude_fp) { - char excluded_cmds[1024]; - if (fgets(excluded_cmds, sizeof(excluded_cmds), exclude_fp)) { - if (excluded_cmds[strlen(excluded_cmds) - 1] == '\n') { - excluded_cmds[strlen(excluded_cmds) - 1] = '\0'; - } - - bool excluded = false; - char *excluded_copy = strdup(excluded_cmds); - char *tok = strtok(excluded_copy, ","); - while (tok) { - if (!strcmp(tok, comm)) { - excluded = true; - break; - } - tok = strtok(NULL, ","); - } - free(excluded_copy); - if (excluded) { - pclose(exclude_fp); - return; - } - } - pclose(exclude_fp); - } + char included_cmds[1024]; + if (libinject_get_config("core.ltrace.include", included_cmds, sizeof(included_cmds)) == 0) { + // If there's an include list, default to false and only trace if included + should_trace = false; + char *included_copy = strdup(included_cmds); + char *tok = strtok(included_copy, ","); + while (tok) { + if (!strcmp(tok, comm)) { + should_trace = true; + break; + } + tok = strtok(NULL, ","); + } + free(included_copy); + } + + // If we're not supposed to trace based on include list, return early + if (!should_trace) { + return; + } + + // Check exclude list + char excluded_cmds[1024]; + if (libinject_get_config("core.ltrace.exclude", excluded_cmds, sizeof(excluded_cmds)) == 0) { + bool excluded = false; + char *excluded_copy = strdup(excluded_cmds); + char *tok = strtok(excluded_copy, ","); + while (tok) { + if (!strcmp(tok, comm)) { + excluded = true; + break; + } + tok = strtok(NULL, ","); + } + free(excluded_copy); + if (excluded) { + return; + } + } if (fork()) { // In parent, wait for child to set up tracing and then continue to the diff --git a/guest-utils/native/get_config.c b/guest-utils/native/get_config.c index 0ea03f097..f446d3b9e 100644 --- a/guest-utils/native/get_config.c +++ b/guest-utils/native/get_config.c @@ -211,7 +211,6 @@ static int _libinject_mkdir_p(const char *path) { } static int value_to_bool(char *value) { - printf("value_to_bool: value='%s'\n", value); if (!strncmp(value, "False", 5) || !strncmp(value, "None", 4) || !strncmp(value, "false", 5) || !strncmp(value, "", 1)) { return 1; diff --git a/pyplugins/core/core.py b/pyplugins/core/core.py index 5518e1767..c6126a0e3 100644 --- a/pyplugins/core/core.py +++ b/pyplugins/core/core.py @@ -267,12 +267,16 @@ def uninit(self) -> None: @plugins.portalcall.portalcall(GET_CONFIG_MAGIC) def portalcall_getconfig(self, key_ptr, output_ptr, buf_size): """ - Config accessor used by the guest + Config accessor used by the guest. + + Returns: + int: 0 on success, -1 on error """ + self.logger.info("get_config called, reading key") key = yield from plugins.mem.read_str(key_ptr) keys = key.split('.') current = self.config - self.logger.debug(f"get_config called for key: {key}") + self.logger.info(f"get_config called for key: {key}") try: for key in keys: @@ -289,7 +293,10 @@ def portalcall_getconfig(self, key_ptr, output_ptr, buf_size): else: raise KeyError value = str(current) - except (KeyError, AttributeError, TypeError): - value = "" - self.logger.debug(f"get_config found value {value} for key: {key}") - yield from plugins.mem.write_str(output_ptr, value[:buf_size-1]) + self.logger.debug(f"get_config found value '{value}' for key: {key}") + yield from plugins.mem.write_str(output_ptr, value[:buf_size-1]) + return 0 # Success + except (KeyError, AttributeError, TypeError) as e: + self.logger.warning(f"get_config failed for key '{key}': {e}") + yield from plugins.mem.write_str(output_ptr, "") + return -1 # Error From c035e6874cddda2424d2a765897331c629f8b134 Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Wed, 18 Feb 2026 10:12:00 -0500 Subject: [PATCH 05/11] fixup bool get_config portalcall --- guest-utils/ltrace/inject_ltrace.c | 2 +- guest-utils/native/get_config.c | 13 ++++++++++--- pyplugins/core/core.py | 25 ++++++++++++------------- pyplugins/interventions/nvram2.py | 2 +- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/guest-utils/ltrace/inject_ltrace.c b/guest-utils/ltrace/inject_ltrace.c index e749ad336..916979db9 100644 --- a/guest-utils/ltrace/inject_ltrace.c +++ b/guest-utils/ltrace/inject_ltrace.c @@ -7,8 +7,8 @@ #include #include -int libinject_get_config_bool(const char *config_key); int libinject_get_config(const char *key, char *output, unsigned long buf_size); +int libinject_get_config_bool(const char *config_key); __attribute__((constructor)) void igloo_start_ltrace(void) { diff --git a/guest-utils/native/get_config.c b/guest-utils/native/get_config.c index f446d3b9e..f7b9351ba 100644 --- a/guest-utils/native/get_config.c +++ b/guest-utils/native/get_config.c @@ -213,9 +213,9 @@ static int _libinject_mkdir_p(const char *path) { static int value_to_bool(char *value) { if (!strncmp(value, "False", 5) || !strncmp(value, "None", 4) || !strncmp(value, "false", 5) || !strncmp(value, "", 1)) { - return 1; + return 0; } - return 0; + return 1; } int libinject_get_config(const char *key, char *output, unsigned long buf_size) { @@ -241,8 +241,15 @@ int libinject_get_config_int(const char *config_key) { int libinject_get_config_bool(const char *config_key) { char *str = malloc(64); int result; + int rv; PRINT_MSG("Getting bool config key '%s'\n", config_key); - libinject_get_config(config_key, str, 64); + rv = libinject_get_config(config_key, str, 64); + if (rv != 0) { + // Config key doesn't exist or failed to retrieve + PRINT_MSG("Failed to get bool config key '%s', returning false\n", config_key); + free(str); + return 0; + } result = value_to_bool(str); PRINT_MSG("Got bool config key '%s' with value %s (bool %d)\n", config_key, str, result); free(str); diff --git a/pyplugins/core/core.py b/pyplugins/core/core.py index c6126a0e3..9c66833e8 100644 --- a/pyplugins/core/core.py +++ b/pyplugins/core/core.py @@ -272,31 +272,30 @@ def portalcall_getconfig(self, key_ptr, output_ptr, buf_size): Returns: int: 0 on success, -1 on error """ - self.logger.info("get_config called, reading key") - key = yield from plugins.mem.read_str(key_ptr) - keys = key.split('.') + full_key = yield from plugins.mem.read_str(key_ptr) + keys = full_key.split('.') current = self.config - self.logger.info(f"get_config called for key: {key}") + self.logger.info(f"get_config called for key: {full_key}") try: - for key in keys: - if isinstance(current, Mapping) and key in current: - current = current[key] + for key_part in keys: + if isinstance(current, Mapping) and key_part in current: + current = current[key_part] elif isinstance(current, Sequence) and not isinstance(current, str): try: - index = int(key) + index = int(key_part) current = current[index] except (ValueError, IndexError): raise - elif hasattr(current, key): - current = getattr(current, key) + elif hasattr(current, key_part): + current = getattr(current, key_part) else: - raise KeyError + raise KeyError(f"Key '{key_part}' not found") value = str(current) - self.logger.debug(f"get_config found value '{value}' for key: {key}") + self.logger.info(f"get_config found value '{value}' for key: {full_key}") yield from plugins.mem.write_str(output_ptr, value[:buf_size-1]) return 0 # Success except (KeyError, AttributeError, TypeError) as e: - self.logger.warning(f"get_config failed for key '{key}': {e}") + self.logger.warning(f"get_config failed for key '{full_key}': {e}") yield from plugins.mem.write_str(output_ptr, "") return -1 # Error diff --git a/pyplugins/interventions/nvram2.py b/pyplugins/interventions/nvram2.py index 2e049f533..67da4a0ba 100644 --- a/pyplugins/interventions/nvram2.py +++ b/pyplugins/interventions/nvram2.py @@ -98,7 +98,7 @@ def add_lib_inject_for_abi(config, abi, cache_dir): ) # Create a hash of all relevant inputs for caching source_files_content = [] - for pattern in ["/igloo_static/libnvram/*.c", "/igloo_static/libnvram/*.h"]: + for pattern in ["/igloo_static/libnvram/*.c", "/igloo_static/libnvram/*.h", "/igloo_static/guest-utils/native/get_config.c", "/igloo_static/guest-utils/ltrace/inject_ltrace.c"]: for file_path in glob.glob(pattern): try: with open(file_path, 'rb') as f: From 0489637ebd37043a03e04c58ab8caced98e23f3c Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Fri, 20 Feb 2026 14:29:07 -0500 Subject: [PATCH 06/11] excise igloo_init out of env --- src/penguin/__main__.py | 2 +- src/penguin/defaults.py | 2 +- src/penguin/penguin_config/__init__.py | 2 +- src/penguin/penguin_config/structure.py | 11 +++++- .../penguin_config/versions/__init__.py | 4 +-- src/penguin/penguin_config/versions/v3.py | 35 +++++++++++++++++++ src/penguin/penguin_run.py | 7 ++-- src/resources/init.sh | 3 +- 8 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 src/penguin/penguin_config/versions/v3.py diff --git a/src/penguin/__main__.py b/src/penguin/__main__.py index 393f12378..c64e03429 100644 --- a/src/penguin/__main__.py +++ b/src/penguin/__main__.py @@ -60,7 +60,7 @@ def run_from_config(proj_dir, config_path, output_dir, timeout=None, verbose=Fal # config if necessary. If we don't have an init, go find a default, otherwise # use the one specified in the config. specified_init = None - if config.get("env", {}).get("igloo_init", None) is None: + if config.get("core", {}).get("igloo_init", None) is None: options = get_inits_from_proj(proj_dir) if len(options): logger.info( diff --git a/src/penguin/defaults.py b/src/penguin/defaults.py index 12f2e825f..d92d657ec 100644 --- a/src/penguin/defaults.py +++ b/src/penguin/defaults.py @@ -13,7 +13,7 @@ vnc_password: str = "IGLOOPassw0rd!" -default_version: int = 2 +default_version: int = 3 static_dir: str = "/igloo_static/" # XXX in config_patchers we append .0 to this - may need to update DEFAULT_KERNEL: str = "4.10" diff --git a/src/penguin/penguin_config/__init__.py b/src/penguin/penguin_config/__init__.py index 073033cf1..092b1a653 100644 --- a/src/penguin/penguin_config/__init__.py +++ b/src/penguin/penguin_config/__init__.py @@ -251,8 +251,8 @@ def load_config(proj_dir, path, validate=True, resolved_kernel=None, verbose=Fal # when loading a patch we don't need a completely valid config if validate: - _validate_config(config) _validate_config_version(config, path) + _validate_config(config) # Not required in schema as to allow for patches, but these really are required if config["core"].get("arch", None) is None: raise ValueError("No core.arch specified in config") diff --git a/src/penguin/penguin_config/structure.py b/src/penguin/penguin_config/structure.py index 4bd87fd04..df7680549 100644 --- a/src/penguin/penguin_config/structure.py +++ b/src/penguin/penguin_config/structure.py @@ -231,7 +231,7 @@ class Core(PartialModelMixin, BaseModel): ), ] version: Annotated[ - Literal["1.0.0", 2], + Literal["1.0.0", 2, 3], Field( title="Config format version", description="Version of the config file format", @@ -309,6 +309,15 @@ class Core(PartialModelMixin, BaseModel): examples=["/igloo/utils/custom_init.sh", "scripts/my_init.sh"], ), ] + igloo_init: Annotated[ + str, + Field( + None, + title="init to run after rehosting starts", + description="Path to init you expect to run in the system. This is the last thing executed by penguin during guest startup", + examples=["/sbin/init", "/sbin/preinit"], + ), + ] EnvVal = _newtype( diff --git a/src/penguin/penguin_config/versions/__init__.py b/src/penguin/penguin_config/versions/__init__.py index 96e1693b7..1a6dba017 100644 --- a/src/penguin/penguin_config/versions/__init__.py +++ b/src/penguin/penguin_config/versions/__init__.py @@ -1,3 +1,3 @@ -from . import v2 +from . import v2, v3 -CHANGELOG = (None, v2.V2) +CHANGELOG = (None, v2.V2, v3.V3) diff --git a/src/penguin/penguin_config/versions/v3.py b/src/penguin/penguin_config/versions/v3.py new file mode 100644 index 000000000..6aac389b9 --- /dev/null +++ b/src/penguin/penguin_config/versions/v3.py @@ -0,0 +1,35 @@ +class V3: + num = 3 + + change_description = """ + We expose the config via hypercalls and no longer configure the guest with env plugin. + igloo_init is now set in core instead of being set in env + """ + + fix_guide = """ + igloo_init is used by init.sh to launch the correct init program and is generated in static_patches/base.yaml + In a project generated with `penguin init` config v2 init.sh will check the environment for igloo_init + + To migrate: + 1. set `core.igloo_init` + + 2. update init.sh in static_patches/base.yaml to use get_config to obtain igloo_init: + ``` + #ADD BEFORE `if` check for igloo_init + igloo_init = $(/igloo/utils/get_config core.igloo_init) + if [ ! -z "${igloo_init}" ]; then + ``` + + The auto-fix (if you say Y in the next step) will just set `core.igloo_init`=`env.igloo_init` + `env.igloo_init` will be retained for backwards compability + """ + + example_old_config = dict( + env=dict( + igloo_init="/sbin/init" + ), + core=dict() + ) + + def auto_fix(config): + config["core"]["igloo_init"] = config["env"]["igloo_init"] diff --git a/src/penguin/penguin_run.py b/src/penguin/penguin_run.py index 88b683cc0..04663385d 100755 --- a/src/penguin/penguin_run.py +++ b/src/penguin/penguin_run.py @@ -120,10 +120,11 @@ def run_config( # An arugument setting a timeout overrides the config's timeout conf["plugins"]["core"]["timeout"] = timeout - if "igloo_init" not in conf["env"]: + if "igloo_init" not in conf["core"]: if init: - conf["env"]["igloo_init"] = init + conf["core"]["igloo_init"] = init else: + # This is from automated analyses, we can remove if/when we refactor env.py try: with open( os.path.join(*[os.path.dirname(conf_yaml), "base", "env.yaml"]), "r" @@ -133,7 +134,7 @@ def run_config( except FileNotFoundError: inits = [] raise RuntimeError( - f"No init binary is specified in configuration, set one in config's env section as igloo_init. Static analysis identified the following: {inits}" + f"No init binary is specified in configuration, set one in core as igloo_init. Static analysis identified the following: {inits}" ) if conf["env"]["igloo_init"] == "UNKNOWN_FIX_ME": logger.error("No init binary specified in config, and static analysis did not identify any candidates") diff --git a/src/resources/init.sh b/src/resources/init.sh index 0500f751c..cf6b8b590 100644 --- a/src/resources/init.sh +++ b/src/resources/init.sh @@ -15,9 +15,10 @@ if [ -d /igloo/init.d ]; then done fi +igloo_init = $(/igloo/utils/get_config core.igloo_init) if [ ! -z "${igloo_init}" ]; then echo '[IGLOO INIT] Running specified init binary'; exec "${igloo_init}" fi -echo "[IGLOO INIT] Fatal: no igloo_init specified in env. Abort" +echo "[IGLOO INIT] Fatal: no igloo_init specified in config. Abort" exit 1 From 35d9a12ad48ce1102b0f4fd85a44c1e0e972ab63 Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Fri, 20 Feb 2026 16:18:47 -0500 Subject: [PATCH 07/11] use core.init instead of core.igloo_init; update schema --- docs/schema_doc.md | 41 +++++++++++++++++++++-- src/penguin/penguin_config/gen_docs.py | 2 ++ src/penguin/penguin_config/structure.py | 17 ---------- src/penguin/penguin_config/versions/v3.py | 6 ++-- src/resources/init.sh | 2 +- 5 files changed, 44 insertions(+), 24 deletions(-) diff --git a/docs/schema_doc.md b/docs/schema_doc.md index dcd6445cd..107fed54d 100644 --- a/docs/schema_doc.md +++ b/docs/schema_doc.md @@ -140,10 +140,10 @@ true ||| |-|-| -|__Type__|boolean or list of string| +|__Type__|boolean or list of string or dict| |__Default__|`false`| -If true, run ltrace for entire system starting from init. If names of programs, enable ltrace only for those programs. +If true, run ltrace for entire system starting from init. If names of programs, enable ltrace only for those programs. If dict with 'include' and/or 'exclude' keys, specify programs to trace or exclude. ```yaml false @@ -157,6 +157,24 @@ true - lighttpd ``` +```yaml +include: +- lighttpd +``` + +```yaml +exclude: +- busybox +- sh +``` + +```yaml +exclude: +- busybox +include: +- lighttpd +``` + ### `core.gdbserver` Programs to run through gdbserver ||| @@ -275,7 +293,7 @@ my_shared_directory ||| |-|-| -|__Type__|`"1.0.0"` or `2`| +|__Type__|`"1.0.0"` or `2` or `3`| Version of the config file format @@ -407,6 +425,23 @@ false true ``` +### `core.init` init to run after rehosting starts + +||| +|-|-| +|__Type__|string| +|__Default__|`null`| + +Path to init you expect to run in the system. This is the last thing executed by penguin during guest startup + +```yaml +/sbin/init +``` + +```yaml +/sbin/preinit +``` + ## `patches` Patches ||| diff --git a/src/penguin/penguin_config/gen_docs.py b/src/penguin/penguin_config/gen_docs.py index 522a906bf..c57c50f19 100644 --- a/src/penguin/penguin_config/gen_docs.py +++ b/src/penguin/penguin_config/gen_docs.py @@ -59,6 +59,8 @@ def gen_docs_type_name(t): return " or ".join([gen_docs_literal_arg(a) for a in args]) elif og in (list, tuple): return "list of " + gen_docs_type_name(args[0]) + elif t is dict: + return "dict" elif t is int: return "integer" elif t is str: diff --git a/src/penguin/penguin_config/structure.py b/src/penguin/penguin_config/structure.py index df7680549..63b3d9585 100644 --- a/src/penguin/penguin_config/structure.py +++ b/src/penguin/penguin_config/structure.py @@ -301,15 +301,6 @@ class Core(PartialModelMixin, BaseModel): ), ] init: Annotated[ - Optional[str], - Field( - None, - title="init script script", - description="Path to custom igloo init script to run during guest startup", - examples=["/igloo/utils/custom_init.sh", "scripts/my_init.sh"], - ), - ] - igloo_init: Annotated[ str, Field( None, @@ -894,14 +885,6 @@ class Main(PartialModelMixin, BaseModel): static_files: StaticFiles plugins: Annotated[dict[str, Plugin], Field(title="Plugins")] network: Optional[Network] = None - internal: Annotated[ - Optional[dict], - Field( - None, - title="Internal runtime data", - description="Reserved for internal tool use - not part of the public API", - ), - ] Patch = create_partial_model(Main, recursive=True) diff --git a/src/penguin/penguin_config/versions/v3.py b/src/penguin/penguin_config/versions/v3.py index 6aac389b9..af7a0291e 100644 --- a/src/penguin/penguin_config/versions/v3.py +++ b/src/penguin/penguin_config/versions/v3.py @@ -16,11 +16,11 @@ class V3: 2. update init.sh in static_patches/base.yaml to use get_config to obtain igloo_init: ``` #ADD BEFORE `if` check for igloo_init - igloo_init = $(/igloo/utils/get_config core.igloo_init) + igloo_init = $(/igloo/utils/get_config core.init) if [ ! -z "${igloo_init}" ]; then ``` - The auto-fix (if you say Y in the next step) will just set `core.igloo_init`=`env.igloo_init` + The auto-fix (if you say Y in the next step) will just set `core.init`=`env.igloo_init` `env.igloo_init` will be retained for backwards compability """ @@ -32,4 +32,4 @@ class V3: ) def auto_fix(config): - config["core"]["igloo_init"] = config["env"]["igloo_init"] + config["core"]["init"] = config["env"]["igloo_init"] diff --git a/src/resources/init.sh b/src/resources/init.sh index cf6b8b590..f0456ad47 100644 --- a/src/resources/init.sh +++ b/src/resources/init.sh @@ -15,7 +15,7 @@ if [ -d /igloo/init.d ]; then done fi -igloo_init = $(/igloo/utils/get_config core.igloo_init) +igloo_init = $(/igloo/utils/get_config core.init) if [ ! -z "${igloo_init}" ]; then echo '[IGLOO INIT] Running specified init binary'; exec "${igloo_init}" From d1667e4990939e828888f3d686affcfedee5b11e Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Fri, 20 Feb 2026 17:00:48 -0500 Subject: [PATCH 08/11] config auto_fix: always notify user of backup --- src/penguin/penguin_config/__init__.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/penguin/penguin_config/__init__.py b/src/penguin/penguin_config/__init__.py index 092b1a653..9b6881c22 100644 --- a/src/penguin/penguin_config/__init__.py +++ b/src/penguin/penguin_config/__init__.py @@ -165,15 +165,19 @@ def format_paragraph(s): if click.confirm("Automatically apply fixes?", default=True): path_old = f"{path}.old" shutil.copyfile(path, path_old) - for version in changes: - version.auto_fix(config) - config["core"]["version"] = version.num - dump_config(config, path) - logger.info( - "Config updated." - f" Backup saved to '{path_old}'." - " Try running PENGUIN again." - ) + try: + for version in changes: + version.auto_fix(config) + config["core"]["version"] = version.num + dump_config(config, path) + logger.info( + "Config updated." + "Try running PENGUIN again." + ) + finally: + logger.info( + f"Backup saved to '{path_old}'." + ) sys.exit(1) From afd8f5829e6c10777d5637e2096a51d15fb0d5e7 Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Fri, 20 Feb 2026 17:39:29 -0500 Subject: [PATCH 09/11] use patch instead of config overwrite for init auto_fix --- src/penguin/penguin_config/__init__.py | 29 +++++++++++++++-------- src/penguin/penguin_config/versions/v3.py | 3 +++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/penguin/penguin_config/__init__.py b/src/penguin/penguin_config/__init__.py index 9b6881c22..20ce286f6 100644 --- a/src/penguin/penguin_config/__init__.py +++ b/src/penguin/penguin_config/__init__.py @@ -166,18 +166,27 @@ def format_paragraph(s): path_old = f"{path}.old" shutil.copyfile(path, path_old) try: + write_config = False for version in changes: - version.auto_fix(config) - config["core"]["version"] = version.num - dump_config(config, path) - logger.info( - "Config updated." - "Try running PENGUIN again." - ) + if hasattr(version, "make_patch"): + logger.info(f"auto fix for config {version.__name__} generates a patch") + patch = version.make_patch(config) + patch["core"]["version"] = version.num + patch_path = Path(path).parent / f"patch_ZZAUTO_{version.__name__}.yaml" + with open(patch_path, "w") as f: + yaml.dump(patch, f, default_flow_style=False) + logger.info(f"Wrote {patch_path.name}") + else: + version.auto_fix(config) + config["core"]["version"] = version.num + write_config = True + if write_config: + dump_config(config, path) + logger.info("Config updated.") finally: - logger.info( - f"Backup saved to '{path_old}'." - ) + if write_config: + logger.info(f"Backup saved to '{path_old}'.") + logger.info("Try running PENGUIN again.") sys.exit(1) diff --git a/src/penguin/penguin_config/versions/v3.py b/src/penguin/penguin_config/versions/v3.py index af7a0291e..012d20b43 100644 --- a/src/penguin/penguin_config/versions/v3.py +++ b/src/penguin/penguin_config/versions/v3.py @@ -33,3 +33,6 @@ class V3: def auto_fix(config): config["core"]["init"] = config["env"]["igloo_init"] + + def make_patch(config): + return {"core": {"init": config["env"]["igloo_init"]}} From 52a300dee42f2b714c92d288643d4a59342ca98e Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Tue, 31 Mar 2026 08:28:07 -0400 Subject: [PATCH 10/11] commit before switch --- pyplugins/core/core.py | 6 +++--- src/penguin/__main__.py | 4 ++-- src/penguin/penguin_config/versions/v3.py | 6 +++--- src/penguin/penguin_run.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pyplugins/core/core.py b/pyplugins/core/core.py index 9c66833e8..4767c5e80 100644 --- a/pyplugins/core/core.py +++ b/pyplugins/core/core.py @@ -275,7 +275,7 @@ def portalcall_getconfig(self, key_ptr, output_ptr, buf_size): full_key = yield from plugins.mem.read_str(key_ptr) keys = full_key.split('.') current = self.config - self.logger.info(f"get_config called for key: {full_key}") + self.logger.debug(f"get_config called for key: {full_key}") try: for key_part in keys: @@ -292,10 +292,10 @@ def portalcall_getconfig(self, key_ptr, output_ptr, buf_size): else: raise KeyError(f"Key '{key_part}' not found") value = str(current) - self.logger.info(f"get_config found value '{value}' for key: {full_key}") + self.logger.debug(f"get_config found value '{value}' for key: {full_key}") yield from plugins.mem.write_str(output_ptr, value[:buf_size-1]) return 0 # Success except (KeyError, AttributeError, TypeError) as e: - self.logger.warning(f"get_config failed for key '{full_key}': {e}") + self.logger.warning(f"get_config requested but failed for key '{full_key}': {e}") yield from plugins.mem.write_str(output_ptr, "") return -1 # Error diff --git a/src/penguin/__main__.py b/src/penguin/__main__.py index c64e03429..fc25bb057 100644 --- a/src/penguin/__main__.py +++ b/src/penguin/__main__.py @@ -60,7 +60,7 @@ def run_from_config(proj_dir, config_path, output_dir, timeout=None, verbose=Fal # config if necessary. If we don't have an init, go find a default, otherwise # use the one specified in the config. specified_init = None - if config.get("core", {}).get("igloo_init", None) is None: + if config.get("core", {}).get("init", None) is None: options = get_inits_from_proj(proj_dir) if len(options): logger.info( @@ -74,7 +74,7 @@ def run_from_config(proj_dir, config_path, output_dir, timeout=None, verbose=Fal specified_init = options[0] else: raise RuntimeError( - "Static analysis failed to identify an init script. Please specify one in your config under env.igloo_init" + "Static analysis failed to identify an init script. Please specify one in your config under core.init" ) try: diff --git a/src/penguin/penguin_config/versions/v3.py b/src/penguin/penguin_config/versions/v3.py index 012d20b43..8f51e0701 100644 --- a/src/penguin/penguin_config/versions/v3.py +++ b/src/penguin/penguin_config/versions/v3.py @@ -3,15 +3,15 @@ class V3: change_description = """ We expose the config via hypercalls and no longer configure the guest with env plugin. - igloo_init is now set in core instead of being set in env + The init used is now set in core instead of being set in env """ fix_guide = """ - igloo_init is used by init.sh to launch the correct init program and is generated in static_patches/base.yaml + The igloo_init variable in init.sh is used to launch the correct init program In a project generated with `penguin init` config v2 init.sh will check the environment for igloo_init To migrate: - 1. set `core.igloo_init` + 1. set `core.init` 2. update init.sh in static_patches/base.yaml to use get_config to obtain igloo_init: ``` diff --git a/src/penguin/penguin_run.py b/src/penguin/penguin_run.py index 04663385d..0ee3686e5 100755 --- a/src/penguin/penguin_run.py +++ b/src/penguin/penguin_run.py @@ -120,9 +120,9 @@ def run_config( # An arugument setting a timeout overrides the config's timeout conf["plugins"]["core"]["timeout"] = timeout - if "igloo_init" not in conf["core"]: + if "init" not in conf["core"]: if init: - conf["core"]["igloo_init"] = init + conf["core"]["init"] = init else: # This is from automated analyses, we can remove if/when we refactor env.py try: From 117153634154d4cb6ae36b64cbcae64db9878198 Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Tue, 12 May 2026 21:05:51 -0400 Subject: [PATCH 11/11] fixup portal call implementation --- guest-utils/ltrace/inject_ltrace.c | 34 +++++++++++++++---------- pyplugins/core/core.py | 33 ++++++++++++++---------- src/penguin/penguin_config/structure.py | 2 +- src/penguin/penguin_run.py | 6 ++--- src/resources/init.sh | 2 +- 5 files changed, 45 insertions(+), 32 deletions(-) diff --git a/guest-utils/ltrace/inject_ltrace.c b/guest-utils/ltrace/inject_ltrace.c index 916979db9..8dcadeb97 100644 --- a/guest-utils/ltrace/inject_ltrace.c +++ b/guest-utils/ltrace/inject_ltrace.c @@ -12,25 +12,14 @@ int libinject_get_config_bool(const char *config_key); __attribute__((constructor)) void igloo_start_ltrace(void) { - // Don't do anything if the user doesn't want to ltrace - if(!libinject_get_config_bool("core.ltrace")) { - return; - } - - // Open tty for output - FILE *tty = fopen(TTY_PATH, "w"); - setlinebuf(tty); - // Get PID pid_t pid = getpid(); char pid_buf[100]; sprintf(pid_buf, "%d", pid); - // Build argv and envp - char *const argv[] = { LTRACE_PATH, "-p", pid_buf, NULL }; - char *const envp[] = { NULL }; - - // Read command name + // Read comm before any portalcall so we can short-circuit ltrace recursion: + // this constructor also runs inside the ltrace child we spawn, and unlike + // the old env-based check there's no empty-envp escape hatch via portalcall. char comm[1024]; sprintf(comm, "/proc/%d/comm", pid); FILE *f_comm = fopen(comm, "r"); @@ -42,6 +31,23 @@ __attribute__((constructor)) void igloo_start_ltrace(void) comm[strlen(comm) - 1] = 0; } + if (!strcmp(comm, "ltrace")) { + return; + } + + // Don't do anything if the user doesn't want to ltrace + if(!libinject_get_config_bool("core.ltrace")) { + return; + } + + // Open tty for output + FILE *tty = fopen(TTY_PATH, "w"); + setlinebuf(tty); + + // Build argv and envp + char *const argv[] = { LTRACE_PATH, "-p", pid_buf, NULL }; + char *const envp[] = { NULL }; + // Don't do anything if the user doesn't want to ltrace this process bool should_trace = true; diff --git a/pyplugins/core/core.py b/pyplugins/core/core.py index 4767c5e80..34f35dff8 100644 --- a/pyplugins/core/core.py +++ b/pyplugins/core/core.py @@ -69,6 +69,12 @@ def __init__(self) -> None: conf = self.get_arg("conf") self.config = conf.args # since the config is an ArgsBox + # The schema accepts core.ltrace as a bare list of programs to trace; + # normalize to the dict form the guest-side include/exclude logic expects. + ltrace = self.config["core"].get("ltrace") + if isinstance(ltrace, list): + self.config["core"]["ltrace"] = {"include": ltrace} + telnet_port = self.get_arg("telnet_port") # Essential plugins are always loaded with core @@ -121,16 +127,13 @@ def __init__(self) -> None: # Warn if env is set for any of the old options. Can happen with old configs. legacy_env_vars = ["ROOT_SHELL", "SHARED_DIR", "STRACE", "IGLOO_LTRACE", "WWW", "PROJ_NAME"] - core_env = conf["core"].get("env", {}) + env = conf.get("env", {}) or {} - found_legacy_vars = [] - for var in legacy_env_vars: - if var in core_env: - found_legacy_vars.append(var) + found_legacy_vars = [var for var in legacy_env_vars if var in env] if found_legacy_vars: self.logger.warning( - f"Legacy environment variables found in core.env: {', '.join(found_legacy_vars)}. " + f"Legacy environment variables found in env: {', '.join(found_legacy_vars)}. " "This likely indicates you are running an old project and this message can safely be ignored." "However, if you have set them intentionally, be aware they will stop working in the future." ) @@ -282,20 +285,24 @@ def portalcall_getconfig(self, key_ptr, output_ptr, buf_size): if isinstance(current, Mapping) and key_part in current: current = current[key_part] elif isinstance(current, Sequence) and not isinstance(current, str): - try: - index = int(key_part) - current = current[index] - except (ValueError, IndexError): - raise + index = int(key_part) + current = current[index] elif hasattr(current, key_part): current = getattr(current, key_part) else: raise KeyError(f"Key '{key_part}' not found") - value = str(current) + # The guest only consumes scalars and lists-of-scalars (CSV). + # Dicts can't be parsed on the guest side, so treat them as not found. + if isinstance(current, Mapping): + raise KeyError(f"Key '{full_key}' resolves to a dict; only leaves are exposed") + if isinstance(current, (list, tuple)): + value = ",".join(str(x) for x in current) + else: + value = str(current) self.logger.debug(f"get_config found value '{value}' for key: {full_key}") yield from plugins.mem.write_str(output_ptr, value[:buf_size-1]) return 0 # Success - except (KeyError, AttributeError, TypeError) as e: + except (KeyError, AttributeError, TypeError, ValueError, IndexError) as e: self.logger.warning(f"get_config requested but failed for key '{full_key}': {e}") yield from plugins.mem.write_str(output_ptr, "") return -1 # Error diff --git a/src/penguin/penguin_config/structure.py b/src/penguin/penguin_config/structure.py index 63b3d9585..9290e480e 100644 --- a/src/penguin/penguin_config/structure.py +++ b/src/penguin/penguin_config/structure.py @@ -301,7 +301,7 @@ class Core(PartialModelMixin, BaseModel): ), ] init: Annotated[ - str, + Optional[str], Field( None, title="init to run after rehosting starts", diff --git a/src/penguin/penguin_run.py b/src/penguin/penguin_run.py index 0ee3686e5..caadebfd0 100755 --- a/src/penguin/penguin_run.py +++ b/src/penguin/penguin_run.py @@ -134,12 +134,12 @@ def run_config( except FileNotFoundError: inits = [] raise RuntimeError( - f"No init binary is specified in configuration, set one in core as igloo_init. Static analysis identified the following: {inits}" + f"No init binary is specified in configuration, set one in core.init. Static analysis identified the following: {inits}" ) - if conf["env"]["igloo_init"] == "UNKNOWN_FIX_ME": + if conf["core"]["init"] == "UNKNOWN_FIX_ME": logger.error("No init binary specified in config, and static analysis did not identify any candidates") raise RuntimeError( - "env.igloo_init in configuration is set to UNKNOWN_FIX_ME. This indicates that we could not find the correct init binary. Please determine the correct init binary and update the config value in static_files/base.yaml" + "core.init in configuration is set to UNKNOWN_FIX_ME. This indicates that we could not find the correct init binary. Please determine the correct init binary and update the config value in static_files/base.yaml" ) archend = conf["core"]["arch"] diff --git a/src/resources/init.sh b/src/resources/init.sh index f0456ad47..8e97d17f1 100644 --- a/src/resources/init.sh +++ b/src/resources/init.sh @@ -15,7 +15,7 @@ if [ -d /igloo/init.d ]; then done fi -igloo_init = $(/igloo/utils/get_config core.init) +igloo_init=$(/igloo/utils/get_config core.init) if [ ! -z "${igloo_init}" ]; then echo '[IGLOO INIT] Running specified init binary'; exec "${igloo_init}"