diff --git a/README.md b/README.md index 5e7a83c..7aa4438 100644 --- a/README.md +++ b/README.md @@ -180,16 +180,16 @@ Run a guest binary: ```bash # Interactive shell with recommended mounts + root identity -./kbox image -S alpine.ext4 -- /bin/sh -i +./kbox -S alpine.ext4 -- /bin/sh -i # Run a specific command -./kbox image -S alpine.ext4 -- /bin/ls -la / +./kbox -S alpine.ext4 -- /bin/ls -la / # Raw mount only (no /proc, /sys, /dev), for targeted commands -./kbox image -r alpine.ext4 -- /bin/cat /etc/os-release +./kbox -r alpine.ext4 -- /bin/cat /etc/os-release # Custom kernel cmdline, bind mount, explicit identity -./kbox image -r alpine.ext4 -k "mem=2048M loglevel=7" \ +./kbox -r alpine.ext4 -k "mem=2048M loglevel=7" \ -b /home/user/data:/mnt/data --change-id 1000:1000 -- /bin/sh -i ``` @@ -202,19 +202,19 @@ interactive mode regardless of terminal detection. ```bash # Auto (default): rewrite/trap for direct binaries, seccomp for shells -./kbox image -S alpine.ext4 -- /bin/ls / +./kbox -S alpine.ext4 -- /bin/ls / # Force seccomp (most compatible, handles fork+exec) -./kbox image -S alpine.ext4 --syscall-mode=seccomp -- /bin/sh -i +./kbox -S alpine.ext4 --syscall-mode=seccomp -- /bin/sh -i # Force trap (single-exec commands, SIGSYS dispatch) -./kbox image -r alpine.ext4 --syscall-mode=trap -- /bin/cat /etc/hostname +./kbox -r alpine.ext4 --syscall-mode=trap -- /bin/cat /etc/hostname # Force rewrite (patched syscall sites, fastest stat path) -./kbox image -r alpine.ext4 --syscall-mode=rewrite -- /opt/tests/bench-test 200 +./kbox -r alpine.ext4 --syscall-mode=rewrite -- /opt/tests/bench-test 200 ``` -Run `./kbox image --help` for the full option list. +Run `./kbox --help` for the full option list. ## Documentation diff --git a/docs/gdb-workflow.md b/docs/gdb-workflow.md index 90f18ce..1bdcd7c 100644 --- a/docs/gdb-workflow.md +++ b/docs/gdb-workflow.md @@ -24,7 +24,7 @@ gdb ./kbox (gdb) add-symbol-file /path/to/lkl/vmlinux (gdb) source scripts/gdb/kbox-gdb.py (gdb) kbox-lkl-load /path/to/lkl -(gdb) set args image -S rootfs.ext4 -- /bin/sh +(gdb) set args -S rootfs.ext4 -- /bin/sh (gdb) run ``` @@ -201,7 +201,7 @@ kbox forks a child (the tracee). GDB must follow the parent: When running under GDB with ASAN, disable LSAN (incompatible with ptrace): ```bash -ASAN_OPTIONS=detect_leaks=0 gdb --args ./kbox image -S alpine.ext4 -c /bin/sh +ASAN_OPTIONS=detect_leaks=0 gdb --args ./kbox -S alpine.ext4 -c /bin/sh ``` ## Coordinated Syscall Tracing diff --git a/docs/security-model.md b/docs/security-model.md index 631a611..ff443f8 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -28,7 +28,7 @@ Three deployment tiers, in ascending isolation strength: | Tier | Threat model | Setup | |------|-------------|-------| -| kbox alone | Trusted/semi-trusted code: build tools, test suites, static analysis, research, teaching | `./kbox image -S rootfs.ext4 -- /bin/sh -i` | +| kbox alone | Trusted/semi-trusted code: build tools, test suites, static analysis, research, teaching | `./kbox -S rootfs.ext4 -- /bin/sh -i` | | kbox + namespace/LSM | Agent tool execution with defense-in-depth: CI runners, automated code review | Wrap with `bwrap`, Landlock, or cgroup limits (adds containment and resource controls, not hardware isolation) | | outer sandbox + kbox | Untrusted code, multi-tenant: hostile payloads, student submissions, public-facing agent APIs | Run kbox inside a microVM (Firecracker, Cloud Hypervisor) for hardware-enforced isolation, or inside gVisor for userspace-kernel isolation | diff --git a/docs/web-observatory.md b/docs/web-observatory.md index 8f8061c..6ab72a3 100644 --- a/docs/web-observatory.md +++ b/docs/web-observatory.md @@ -29,13 +29,13 @@ process. make KBOX_HAS_WEB=1 BUILD=release # Launch with observatory on default port 8080 -./kbox image -S alpine.ext4 --web -- /bin/sh -i +./kbox -S alpine.ext4 --web -- /bin/sh -i # Custom port and bind address (e.g., access from outside a VM) -./kbox image -S alpine.ext4 --web=9090 --web-bind 0.0.0.0 -- /bin/sh -i +./kbox -S alpine.ext4 --web=9090 --web-bind 0.0.0.0 -- /bin/sh -i # JSON trace to stderr without HTTP server -./kbox image -S alpine.ext4 --trace-format json -- /bin/ls / +./kbox -S alpine.ext4 --trace-format json -- /bin/ls / ``` Open `http://127.0.0.1:8080/` in a browser. The dashboard shows: diff --git a/include/kbox/cli.h b/include/kbox/cli.h index 54f482e..2ea3430 100644 --- a/include/kbox/cli.h +++ b/include/kbox/cli.h @@ -11,10 +11,6 @@ #define KBOX_MAX_BIND_MOUNTS 32 #define KBOX_MAX_MOUNT_OPTS 16 -enum kbox_mode { - KBOX_MODE_IMAGE, -}; - enum kbox_syscall_mode { KBOX_SYSCALL_MODE_SECCOMP, KBOX_SYSCALL_MODE_TRAP, @@ -51,17 +47,10 @@ struct kbox_image_args { int extra_argc; /* count of extra_args */ }; -struct kbox_args { - enum kbox_mode mode; - union { - struct kbox_image_args image; - }; -}; - /* Parse command-line arguments. * Returns 0 on success, -1 on error (message printed to stderr). */ -int kbox_parse_args(int argc, char *argv[], struct kbox_args *out); +int kbox_parse_args(int argc, char *argv[], struct kbox_image_args *out); /* Print usage to stderr. */ void kbox_usage(const char *argv0); diff --git a/scripts/run-stress.sh b/scripts/run-stress.sh index 1948c72..da3600b 100755 --- a/scripts/run-stress.sh +++ b/scripts/run-stress.sh @@ -44,7 +44,7 @@ run_stress_test() printf " %-40s " "$name" # Check if the test binary exists in the rootfs. - if ! "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "test -x '$guest_path'" 2> /dev/null; then + if ! "$KBOX" -S "$ROOTFS" -- /bin/sh -c "test -x '$guest_path'" 2> /dev/null; then printf "${YELLOW}SKIP${NC} (not in rootfs)\n" SKIP=$((SKIP + 1)) return @@ -54,13 +54,13 @@ run_stress_test() RC=0 if [ -n "$TIMEOUT_CMD" ]; then - if "$TIMEOUT_CMD" "$TIMEOUT" "$KBOX" image -S "$ROOTFS" -- "$guest_path" $guest_args > "$OUTPUT" 2>&1; then + if "$TIMEOUT_CMD" "$TIMEOUT" "$KBOX" -S "$ROOTFS" -- "$guest_path" $guest_args > "$OUTPUT" 2>&1; then RC=0 else RC=$? fi else - if "$KBOX" image -S "$ROOTFS" -- "$guest_path" $guest_args > "$OUTPUT" 2>&1; then + if "$KBOX" -S "$ROOTFS" -- "$guest_path" $guest_args > "$OUTPUT" 2>&1; then RC=0 else RC=$? diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 1ca118f..bfd7e20 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -128,7 +128,7 @@ expect_output_count() guest_has_test() { test_prog="$1" - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "test -x /opt/tests/${test_prog}" \ + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "test -x /opt/tests/${test_prog}" \ 2> /dev/null } @@ -147,7 +147,7 @@ require_guest_test() rewrite_mode_probe=$(mktemp) rewrite_mode_state="broken" -if run_with_timeout "$KBOX" image -S "$ROOTFS" --syscall-mode=rewrite -- /bin/true \ +if run_with_timeout "$KBOX" -S "$ROOTFS" --syscall-mode=rewrite -- /bin/true \ > "$rewrite_mode_probe" 2>&1; then rewrite_mode_state="available" elif grep -q "rewrite mode is unsupported in x86_64 ASAN builds" \ @@ -165,166 +165,166 @@ echo "" echo "--- Basic execution ---" expect_success "boot-and-exit" \ - "$KBOX" image -S "$ROOTFS" -- /bin/true + "$KBOX" -S "$ROOTFS" -- /bin/true expect_output "ls-root" "bin" \ - "$KBOX" image -S "$ROOTFS" -- /bin/ls + "$KBOX" -S "$ROOTFS" -- /bin/ls expect_output "echo-hello" "hello" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "echo hello" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "echo hello" expect_output "cat-passwd" "root" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "cat /etc/passwd" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "cat /etc/passwd" # ---- File operations ---- echo "" echo "--- File operations ---" expect_success "mkdir-and-ls" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "mkdir -p /tmp/testdir && ls /tmp/testdir" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "mkdir -p /tmp/testdir && ls /tmp/testdir" expect_output "write-and-read" "testdata" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "echo testdata > /tmp/testfile && cat /tmp/testfile" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "echo testdata > /tmp/testfile && cat /tmp/testfile" expect_success "cp-file" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "echo data > /tmp/src && cp /tmp/src /tmp/dst && cat /tmp/dst" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "echo data > /tmp/src && cp /tmp/src /tmp/dst && cat /tmp/dst" # ---- Process and identity ---- echo "" echo "--- Process and identity ---" expect_output "uname-linux" "Linux" \ - "$KBOX" image -S "$ROOTFS" -- /bin/uname -s + "$KBOX" -S "$ROOTFS" -- /bin/uname -s expect_output "id-root" "uid=0" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "id" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "id" expect_output "pwd-root" "/" \ - "$KBOX" image -S "$ROOTFS" -- /bin/pwd + "$KBOX" -S "$ROOTFS" -- /bin/pwd expect_output "hostname" "" \ - "$KBOX" image -S "$ROOTFS" -- /bin/hostname + "$KBOX" -S "$ROOTFS" -- /bin/hostname # ---- Virtual filesystems ---- echo "" echo "--- Virtual filesystems ---" expect_output "proc-mounted" "PROC_OK" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "test -d /proc/self && echo PROC_OK" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "test -d /proc/self && echo PROC_OK" expect_output "proc-self-status" "Name:" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "cat /proc/self/status" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "cat /proc/self/status" expect_output "sys-mounted" "SYS_OK" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "test -d /sys/kernel && echo SYS_OK" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "test -d /sys/kernel && echo SYS_OK" # ---- FD and dup operations ---- echo "" echo "--- FD operations ---" expect_output "dup-via-shell" "hello" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "echo hello | cat" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "echo hello | cat" expect_output "pipe-chain" "abc" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "echo abc | grep abc" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "echo abc | grep abc" # ---- Directory operations ---- echo "" echo "--- Directory operations ---" expect_output "mkdir-nested" "c" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "mkdir -p /tmp/a/b/c && ls /tmp/a/b/" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "mkdir -p /tmp/a/b/c && ls /tmp/a/b/" expect_success "rmdir" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "mkdir /tmp/rmd && rmdir /tmp/rmd && ! test -d /tmp/rmd" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "mkdir /tmp/rmd && rmdir /tmp/rmd && ! test -d /tmp/rmd" expect_success "rename-file" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "echo x > /tmp/old && mv /tmp/old /tmp/new && cat /tmp/new" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "echo x > /tmp/old && mv /tmp/old /tmp/new && cat /tmp/new" expect_success "unlink-file" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "echo x > /tmp/del && rm /tmp/del && ! test -f /tmp/del" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "echo x > /tmp/del && rm /tmp/del && ! test -f /tmp/del" # ---- Navigation ---- echo "" echo "--- Navigation ---" expect_output "chdir-pwd" "/tmp" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "cd /tmp && pwd" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "cd /tmp && pwd" expect_output "chdir-root-ls" "passwd" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "cd / && ls etc/" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "cd / && ls etc/" expect_output "workdir-flag" "/tmp" \ - "$KBOX" image -S "$ROOTFS" -w /tmp -- /bin/pwd + "$KBOX" -S "$ROOTFS" -w /tmp -- /bin/pwd # ---- Identity (extended) ---- echo "" echo "--- Identity (extended) ---" expect_output "id-root-flag" "uid=0(root) gid=0(root)" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "id" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "id" expect_output "whoami-root" "root" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "whoami" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "whoami" # ---- Metadata ---- echo "" echo "--- Metadata ---" expect_success "stat-file" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "stat /etc/passwd" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "stat /etc/passwd" expect_success "test-file-exists" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "test -f /etc/passwd" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "test -f /etc/passwd" expect_success "test-dir-exists" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "test -d /tmp" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "test -d /tmp" # ---- Procfs data ---- echo "" echo "--- Procfs data ---" expect_output "proc-stat-cpu" "cpu " \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "head -1 /proc/stat" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "head -1 /proc/stat" expect_output "proc-meminfo" "MemTotal" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "head -1 /proc/meminfo" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "head -1 /proc/meminfo" expect_output "proc-self-fd" "FD_OK" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "test -d /proc/self/fd && echo FD_OK" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "test -d /proc/self/fd && echo FD_OK" # ---- Time ---- echo "" echo "--- Time ---" expect_output "date-runs" "" \ - "$KBOX" image -S "$ROOTFS" -- /bin/date + "$KBOX" -S "$ROOTFS" -- /bin/date # ---- Pipe and I/O ---- echo "" echo "--- Pipe and I/O ---" expect_output "pipe-simple" "hello" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "echo hello | cat" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "echo hello | cat" expect_output "pipe-grep" "match" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "echo match | grep match" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "echo match | grep match" expect_output "pipe-wc" "3" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "printf 'a\nb\nc\n' | wc -l" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "printf 'a\nb\nc\n' | wc -l" expect_success "redirect-append" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "echo a > /tmp/ap && echo b >> /tmp/ap && test \$(wc -l < /tmp/ap) -eq 2" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "echo a > /tmp/ap && echo b >> /tmp/ap && test \$(wc -l < /tmp/ap) -eq 2" # ---- Permission and access ---- echo "" echo "--- Permissions ---" expect_success "chmod-test" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "echo x > /tmp/ch && chmod 755 /tmp/ch && test -x /tmp/ch" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "echo x > /tmp/ch && chmod 755 /tmp/ch && test -x /tmp/ch" expect_success "umask-test" \ - "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "umask 022" + "$KBOX" -S "$ROOTFS" -- /bin/sh -c "umask 022" # ---- Guest test programs (if available) ---- echo "" @@ -333,17 +333,17 @@ echo "--- Guest test programs ---" for test_prog in dup-test clock-test signal-test path-escape-test errno-test; do if guest_has_test "$test_prog"; then expect_success "$test_prog" \ - "$KBOX" image -S "$ROOTFS" -- "/opt/tests/${test_prog}" + "$KBOX" -S "$ROOTFS" -- "/opt/tests/${test_prog}" else printf " %-40s ${YELLOW}SKIP${NC} (not in rootfs)\n" "$test_prog" SKIP=$((SKIP + 1)) fi done -if "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "test -x /opt/tests/clone3-test" 2> /dev/null; then +if "$KBOX" -S "$ROOTFS" -- /bin/sh -c "test -x /opt/tests/clone3-test" 2> /dev/null; then expect_output_count "clone3-test" \ "kbox: clone3 denied: namespace flags" 9 \ - "$KBOX" image --forward-verbose -S "$ROOTFS" --syscall-mode=seccomp \ + "$KBOX" --forward-verbose -S "$ROOTFS" --syscall-mode=seccomp \ -- "/opt/tests/clone3-test" else printf " %-40s ${YELLOW}SKIP${NC} (not in rootfs)\n" "clone3-test" @@ -352,7 +352,7 @@ fi if require_guest_test "process-vm-deny-test"; then expect_output "process-vm-deny-test" "PASS: process_vm_readv denied" \ - "$KBOX" image -S "$ROOTFS" --syscall-mode=seccomp \ + "$KBOX" -S "$ROOTFS" --syscall-mode=seccomp \ -- "/opt/tests/process-vm-deny-test" fi @@ -362,13 +362,13 @@ echo "--- Rewrite security ---" if [ "$rewrite_mode_state" = "available" ]; then if require_guest_test "jit-spray-test"; then expect_output "jit-spray-test" "PASS: jit_spray_boundary" \ - "$KBOX" image -S "$ROOTFS" --syscall-mode=rewrite \ + "$KBOX" -S "$ROOTFS" --syscall-mode=rewrite \ -- "/opt/tests/jit-spray-test" fi if require_guest_test "jit-alias-test"; then expect_output "jit-alias-test" "PASS: jit_alias_blocked" \ - "$KBOX" image -S "$ROOTFS" --syscall-mode=rewrite \ + "$KBOX" -S "$ROOTFS" --syscall-mode=rewrite \ -- "/opt/tests/jit-alias-test" fi elif [ "$rewrite_mode_state" = "unsupported" ]; then @@ -386,12 +386,12 @@ echo "" echo "--- Networking ---" # Check if kbox was built with SLIRP support by testing --net flag. -if "$KBOX" image -S "$ROOTFS" --net -- /bin/true 2> /dev/null; then +if "$KBOX" -S "$ROOTFS" --net -- /bin/true 2> /dev/null; then for test_prog in net-dns-test; do - if "$KBOX" image -S "$ROOTFS" --net -- /bin/sh -c "test -x /opt/tests/${test_prog}" \ + if "$KBOX" -S "$ROOTFS" --net -- /bin/sh -c "test -x /opt/tests/${test_prog}" \ 2> /dev/null; then expect_success "$test_prog" \ - "$KBOX" image -S "$ROOTFS" --net -- "/opt/tests/${test_prog}" + "$KBOX" -S "$ROOTFS" --net -- "/opt/tests/${test_prog}" else printf " %-40s ${YELLOW}SKIP${NC} (not in rootfs)\n" "$test_prog" SKIP=$((SKIP + 1)) @@ -399,17 +399,17 @@ if "$KBOX" image -S "$ROOTFS" --net -- /bin/true 2> /dev/null; then done expect_output "net-ping-gateway" "bytes from" \ - "$KBOX" image -S "$ROOTFS" --net -- /bin/sh -c "ping -c 1 -W 3 10.0.2.2" + "$KBOX" -S "$ROOTFS" --net -- /bin/sh -c "ping -c 1 -W 3 10.0.2.2" expect_output "net-resolv-conf" "nameserver" \ - "$KBOX" image -S "$ROOTFS" --net -- /bin/sh -c "cat /etc/resolv.conf" + "$KBOX" -S "$ROOTFS" --net -- /bin/sh -c "cat /etc/resolv.conf" # wget test (outbound TCP via SLIRP) # Check that DNS resolves and TCP connects. The HTTP response may # fail (busybox wget vs chunked encoding / virtual hosting), so # we check for the "Connecting to" line which proves DNS + TCP. expect_output "net-wget-external" "Connecting to" \ - "$KBOX" image -S "$ROOTFS" --net -- /bin/sh -c "wget -S -O /dev/null http://www.google.com/ 2>&1 || true" + "$KBOX" -S "$ROOTFS" --net -- /bin/sh -c "wget -S -O /dev/null http://www.google.com/ 2>&1 || true" else for t in net-dns-test net-ping-gateway net-resolv-conf net-wget-external; do printf " %-40s ${YELLOW}SKIP${NC} (no SLIRP support)\n" "$t" diff --git a/src/cli.c b/src/cli.c index 1bd2c2c..19dc9cc 100644 --- a/src/cli.c +++ b/src/cli.c @@ -24,7 +24,7 @@ enum { OPT_HELP, }; -static const struct option image_longopts[] = { +static const struct option longopts[] = { {"root-dir", required_argument, NULL, 'r'}, {"recommended-root", required_argument, NULL, 'R'}, {"system-root", required_argument, NULL, 'S'}, @@ -50,13 +50,13 @@ static const struct option image_longopts[] = { {NULL, 0, NULL, 0}, }; -static const char image_shortopts[] = "r:R:S:t:p:w:c:k:m:b:0n"; +static const char shortopts[] = "r:R:S:t:p:w:c:k:m:b:0nh"; void kbox_usage(const char *argv0) { fprintf( stderr, - "Usage: %s image [OPTIONS]\n" + "Usage: %s [OPTIONS] [-- COMMAND [ARGS...]]\n" "\n" "Boot a Linux kernel from a rootfs disk image.\n" "\n" @@ -85,36 +85,37 @@ void kbox_usage(const char *argv0) " --web-bind ADDR Bind address for web (default: " "127.0.0.1)\n" " --trace-format FMT Trace output format (json)\n" - " --help Show this help\n", + " -h, --help Show this help\n", argv0); } -static void image_defaults(struct kbox_image_args *img) +int kbox_parse_args(int argc, char *argv[], struct kbox_image_args *img) { + int c; + bool command_from_option = false; + + if (!img) + return -1; + memset(img, 0, sizeof(*img)); + + if (argc < 2) { + kbox_usage(argv[0]); + return -1; + } + img->fs_type = "ext4"; - img->part = 0; img->work_dir = "/"; img->command = "/bin/sh"; img->cmdline = "mem=1024M loglevel=4"; img->mount_profile = KBOX_MOUNT_FULL; img->syscall_mode = KBOX_SYSCALL_MODE_AUTO; -} - -static int parse_image_args(int argc, - char *argv[], - struct kbox_image_args *img, - const char *argv0) -{ - int c; - image_defaults(img); - - /* Reset getopt state for subcommand parsing */ + /* Reset getopt state; kbox_parse_args may be called more than once + * (e.g. across unit tests). */ optind = 0; - while ((c = getopt_long(argc, argv, image_shortopts, image_longopts, - NULL)) != -1) { + while ((c = getopt_long(argc, argv, shortopts, longopts, NULL)) != -1) { switch (c) { case 'r': img->root_dir = optarg; @@ -148,6 +149,7 @@ static int parse_image_args(int argc, break; case 'c': img->command = optarg; + command_from_option = true; break; case 'k': img->cmdline = optarg; @@ -257,61 +259,35 @@ static int parse_image_args(int argc, return -1; } break; + case 'h': case OPT_HELP: - kbox_usage(argv0); + kbox_usage(argv[0]); return -1; default: - kbox_usage(argv0); + kbox_usage(argv[0]); return -1; } } if (!img->root_dir) { fprintf(stderr, "error: one of -r, -R, or -S is required\n"); - kbox_usage(argv0); + kbox_usage(argv[0]); return -1; } /* Capture remaining arguments after getopt (i.e., after --). */ if (optind < argc) { - img->command = argv[optind]; - if (optind + 1 < argc) { - img->extra_args = (const char *const *) &argv[optind + 1]; - img->extra_argc = argc - optind - 1; + if (command_from_option) { + img->extra_args = (const char *const *) &argv[optind]; + img->extra_argc = argc - optind; + } else { + img->command = argv[optind]; + if (optind + 1 < argc) { + img->extra_args = (const char *const *) &argv[optind + 1]; + img->extra_argc = argc - optind - 1; + } } } return 0; } - -int kbox_parse_args(int argc, char *argv[], struct kbox_args *out) -{ - if (!out) - return -1; - - memset(out, 0, sizeof(*out)); - - if (argc < 2) { - kbox_usage(argv[0]); - return -1; - } - - const char *subcmd = argv[1]; - - if (strcmp(subcmd, "image") == 0) { - out->mode = KBOX_MODE_IMAGE; - /* Shift argv past the subcommand name so getopt sees "kbox" followed by - * the image-specific flags. - */ - return parse_image_args(argc - 1, argv + 1, &out->image, argv[0]); - } - - if (strcmp(subcmd, "--help") == 0 || strcmp(subcmd, "-h") == 0) { - kbox_usage(argv[0]); - return -1; - } - - fprintf(stderr, "unknown subcommand: %s\n", subcmd); - kbox_usage(argv[0]); - return -1; -} diff --git a/src/main.c b/src/main.c index c4f0412..473940a 100644 --- a/src/main.c +++ b/src/main.c @@ -1,32 +1,16 @@ /* SPDX-License-Identifier: MIT */ -/* kbox entry point. - * - * Parses CLI arguments and dispatches to the appropriate mode handler. - */ - -#include -#include +/* kbox entry point: parse CLI arguments and boot the rootfs image. */ #include "kbox/cli.h" #include "kbox/image.h" int main(int argc, char *argv[]) { - struct kbox_args args; - int ret; + struct kbox_image_args args; if (kbox_parse_args(argc, argv, &args) < 0) return 1; - switch (args.mode) { - case KBOX_MODE_IMAGE: - ret = kbox_run_image(&args.image); - break; - default: - fprintf(stderr, "unsupported mode\n"); - return 1; - } - - return ret < 0 ? 1 : 0; + return kbox_run_image(&args) < 0 ? 1 : 0; } diff --git a/tests/unit/test-cli.c b/tests/unit/test-cli.c index d84c5ff..edd6f21 100644 --- a/tests/unit/test-cli.c +++ b/tests/unit/test-cli.c @@ -26,24 +26,56 @@ int kbox_parse_syscall_mode(const char *value, enum kbox_syscall_mode *out) static void test_parse_args_partition_valid(void) { - char *argv[] = {"kbox", "image", "-r", "rootfs.ext4", "-p", "7"}; - struct kbox_args args; + char *argv[] = {"kbox", "-r", "rootfs.ext4", "-p", "7"}; + struct kbox_image_args args; - ASSERT_EQ(kbox_parse_args(6, argv, &args), 0); - ASSERT_EQ(args.mode, KBOX_MODE_IMAGE); - ASSERT_EQ(args.image.part, 7); + ASSERT_EQ(kbox_parse_args(5, argv, &args), 0); + ASSERT_EQ(args.part, 7); } static void test_parse_args_partition_overflow_rejected(void) { - char *argv[] = {"kbox", "image", "-r", "rootfs.ext4", "-p", "4294967296"}; - struct kbox_args args; + char *argv[] = {"kbox", "-r", "rootfs.ext4", "-p", "4294967296"}; + struct kbox_image_args args; + + ASSERT_EQ(kbox_parse_args(5, argv, &args), -1); +} + +static void test_parse_args_help_short_rejected(void) +{ + char *argv[] = {"kbox", "-h"}; + struct kbox_image_args args; + + ASSERT_EQ(kbox_parse_args(2, argv, &args), -1); +} + +static void test_parse_args_positional_command_parsed(void) +{ + char *argv[] = {"kbox", "-r", "rootfs.ext4", "--", "/bin/ls", "-l"}; + struct kbox_image_args args; + + ASSERT_EQ(kbox_parse_args(6, argv, &args), 0); + ASSERT_STREQ(args.command, "/bin/ls"); + ASSERT_EQ(args.extra_argc, 1); + ASSERT_STREQ(args.extra_args[0], "-l"); +} + +static void test_parse_args_command_flag_keeps_extra_args(void) +{ + char *argv[] = {"kbox", "-r", "rootfs.ext4", "-c", "/bin/date", "--", "-u"}; + struct kbox_image_args args; - ASSERT_EQ(kbox_parse_args(6, argv, &args), -1); + ASSERT_EQ(kbox_parse_args(7, argv, &args), 0); + ASSERT_STREQ(args.command, "/bin/date"); + ASSERT_EQ(args.extra_argc, 1); + ASSERT_STREQ(args.extra_args[0], "-u"); } void test_cli_init(void) { TEST_REGISTER(test_parse_args_partition_valid); TEST_REGISTER(test_parse_args_partition_overflow_rejected); + TEST_REGISTER(test_parse_args_help_short_rejected); + TEST_REGISTER(test_parse_args_positional_command_parsed); + TEST_REGISTER(test_parse_args_command_flag_keeps_extra_args); }