From 17531ffc68c4a77fdbe97690b9868e95e22b3b6d Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Mon, 10 Nov 2025 09:28:17 -0500 Subject: [PATCH 1/9] 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 d94a0888b..02adab3ad 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") @@ -150,6 +153,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 @@ -259,3 +265,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 b96fa2748a84ac4026fdb4d72f8311ac2f3a8328 Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Mon, 10 Nov 2025 17:12:55 -0500 Subject: [PATCH 2/9] 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 02adab3ad..90b70f6f6 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 --------------- @@ -84,10 +81,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( @@ -122,15 +116,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( @@ -138,12 +138,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 de4a757f5..e159c345d 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 5c62ed68ab1e30ba06ec40aea3d451261c6210a6 Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Thu, 20 Nov 2025 13:35:30 -0500 Subject: [PATCH 3/9] 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 90b70f6f6..1bdbc56bf 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): """ @@ -151,9 +153,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 @@ -264,15 +263,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): @@ -280,11 +282,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 67f3352416c5190e2fa05a1ab3fa01972becb5bc Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Tue, 17 Feb 2026 18:03:24 -0500 Subject: [PATCH 4/9] 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 1bdbc56bf..5772baae6 100644 --- a/pyplugins/core/core.py +++ b/pyplugins/core/core.py @@ -266,12 +266,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: @@ -288,7 +292,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 4d55cde13640debc5ec74e73f6a54228b215922d Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Wed, 18 Feb 2026 10:12:00 -0500 Subject: [PATCH 5/9] 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 5772baae6..45e52d15e 100644 --- a/pyplugins/core/core.py +++ b/pyplugins/core/core.py @@ -271,31 +271,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 935486c976794b6a286647b4943fe02b05622c6a Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Fri, 20 Feb 2026 14:29:07 -0500 Subject: [PATCH 6/9] 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 677d75b5a..de44655ff 100644 --- a/src/penguin/__main__.py +++ b/src/penguin/__main__.py @@ -59,7 +59,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 7a9c11b95..c1456f9f2 100644 --- a/src/penguin/penguin_config/__init__.py +++ b/src/penguin/penguin_config/__init__.py @@ -239,8 +239,8 @@ def load_config(proj_dir, path, validate=True, resolved_kernel=None): # 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 b9b9f6dda..6d04f19bd 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}" ) archend = conf["core"]["arch"] 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 6c74d3f29b3bed4c24812a1b7dba35b9846a4cde Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Fri, 20 Feb 2026 16:18:47 -0500 Subject: [PATCH 7/9] 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 c1c5565f03d784a3afacab8dac269c80ed82a937 Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Fri, 20 Feb 2026 17:00:48 -0500 Subject: [PATCH 8/9] 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 c1456f9f2..9dea76aa7 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 56e531d7df73f78990f989405cfcd75b61195757 Mon Sep 17 00:00:00 2001 From: Zak Estrada Date: Fri, 20 Feb 2026 17:39:29 -0500 Subject: [PATCH 9/9] 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 9dea76aa7..d7e1915e1 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"]}}