summaryrefslogtreecommitdiffstats
path: root/recipes-containers/vcontainer/files/vrunner.sh
diff options
context:
space:
mode:
Diffstat (limited to 'recipes-containers/vcontainer/files/vrunner.sh')
-rwxr-xr-xrecipes-containers/vcontainer/files/vrunner.sh229
1 files changed, 221 insertions, 8 deletions
diff --git a/recipes-containers/vcontainer/files/vrunner.sh b/recipes-containers/vcontainer/files/vrunner.sh
index b6455330..4058fe54 100755
--- a/recipes-containers/vcontainer/files/vrunner.sh
+++ b/recipes-containers/vcontainer/files/vrunner.sh
@@ -38,6 +38,13 @@ TARGET_ARCH="${VDKR_ARCH:-${VPDMN_ARCH:-aarch64}}"
38TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}" 38TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}"
39VERBOSE="${VDKR_VERBOSE:-${VPDMN_VERBOSE:-false}}" 39VERBOSE="${VDKR_VERBOSE:-${VPDMN_VERBOSE:-false}}"
40 40
41# Registry authentication config file (docker config.json / podman auth.json).
42# Can be set via $VDKR_CONFIG or $VPDMN_CONFIG in the environment, and is
43# overridden by the --config CLI flag below. The file is passed into the guest
44# over a dedicated read-only virtio-9p share and installed into the guest
45# container runtime's credential location by the init script.
46AUTH_CONFIG="${VDKR_CONFIG:-${VPDMN_CONFIG:-}}"
47
41# Runtime-specific settings (set after parsing --runtime) 48# Runtime-specific settings (set after parsing --runtime)
42set_runtime_config() { 49set_runtime_config() {
43 case "$RUNTIME" in 50 case "$RUNTIME" in
@@ -232,6 +239,12 @@ OPTIONS:
232 --network, -n Enable networking (slirp user-mode, outbound only) 239 --network, -n Enable networking (slirp user-mode, outbound only)
233 --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto) 240 --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto)
234 --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat. 241 --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat.
242 --config <path> Path to docker/podman auth config (config.json / auth.json).
243 Defaults to $VDKR_CONFIG or $VPDMN_CONFIG from environment.
244 The file is passed to the guest over a dedicated read-only
245 virtio-9p share and installed at /root/.docker/config.json
246 (vdkr) or /run/containers/0/auth.json (vpdmn). The host file
247 must be a regular file with mode 0600 or stricter.
235 --interactive, -it Run in interactive mode (connects terminal to container) 248 --interactive, -it Run in interactive mode (connects terminal to container)
236 --timeout <secs> QEMU timeout [default: 300] 249 --timeout <secs> QEMU timeout [default: 300]
237 --idle-timeout <s> Daemon idle timeout in seconds [default: 1800] 250 --idle-timeout <s> Daemon idle timeout in seconds [default: 1800]
@@ -406,6 +419,13 @@ while [ $# -gt 0 ]; do
406 REGISTRY_PASS="$2" 419 REGISTRY_PASS="$2"
407 shift 2 420 shift 2
408 ;; 421 ;;
422 --config)
423 # Path to a docker/podman config file (config.json / auth.json)
424 # Overrides $VDKR_CONFIG / $VPDMN_CONFIG. The file is mounted into
425 # the guest via a dedicated read-only virtio-9p share.
426 AUTH_CONFIG="$2"
427 shift 2
428 ;;
409 --interactive|-it) 429 --interactive|-it)
410 INTERACTIVE="true" 430 INTERACTIVE="true"
411 shift 431 shift
@@ -587,13 +607,63 @@ daemon_stop() {
587 local pid=$(cat "$DAEMON_PID_FILE") 607 local pid=$(cat "$DAEMON_PID_FILE")
588 log "INFO" "Stopping daemon (PID: $pid)..." 608 log "INFO" "Stopping daemon (PID: $pid)..."
589 609
590 # Send shutdown command via socket 610 # Send shutdown command via socket, then poll until the VM exits.
611 #
612 # The guest's graceful_shutdown() does sync + umount of
613 # /var/lib/containers/storage + blockdev --flushbufs + sync + sleep 2
614 # + reboot -f. Under load (e.g. tens of MB of just-imported layer
615 # blobs awaiting ext4 journal commit) this routinely takes 5-30
616 # seconds. A fixed 2-second wait followed by SIGTERM kills the
617 # guest mid-umount and leaves the state disk's ext4 journal
618 # half-committed: layer files have correct inode metadata but
619 # partially-unwritten data extents, and the next session's reads
620 # hit EOF or CRC failures during tar-split layer reassembly:
621 #
622 # Error: reading blob sha256:<hash>: EOF
623 # Error: reading blob sha256:<hash>: file integrity checksum
624 # failed for "<file>"
591 if [ -S "$DAEMON_SOCKET" ]; then 625 if [ -S "$DAEMON_SOCKET" ]; then
592 echo "===SHUTDOWN===" | socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true 626 echo "===SHUTDOWN===" | socat - "UNIX-CONNECT:$DAEMON_SOCKET" 2>/dev/null || true
593 sleep 2 627 # Poll up to 60s (120 * 0.5s). Generous enough to cover heavy
628 # ext4 journal commits; short enough that a truly hung guest
629 # doesn't block the caller indefinitely.
630 for _i in $(seq 1 120); do
631 kill -0 "$pid" 2>/dev/null || break
632 sleep 0.5
633 done
594 fi 634 fi
595 635
596 # If still running, kill it 636 # If still running after the graceful window, the guest didn't complete
637 # its graceful_shutdown() — meaning the state disk's ext4 journal may
638 # not have committed all pending writes from this session. Any escalation
639 # from here on risks leaving the disk image partially-written: layer
640 # files with correct inode metadata but unwritten data extents, which
641 # surface as "reading blob ...: EOF" or "file integrity checksum failed"
642 # errors on the *next* session's reads. Warn loudly so the operator can
643 # decide whether to start the next session with `memres restart --clean`.
644 if kill -0 "$pid" 2>/dev/null; then
645 log "WARN" "Guest did not exit within graceful window — state disk integrity may be compromised."
646 log "WARN" "If subsequent sessions report 'reading blob ...: EOF' or 'file integrity checksum failed',"
647 log "WARN" "discard state with: ${VCONTAINER_RUNTIME_NAME:-vrunner} memres restart --clean"
648 fi
649
650 # If still running after the graceful window, escalate via QMP quit.
651 # Functionally similar to SIGTERM at the QEMU-process level (both
652 # converge on qemu_system_killed and a block-layer flush), but goes
653 # through QEMU's monitor interface — the same path hv_idle_shutdown()
654 # uses. Keeps the two escalation paths consistent.
655 if kill -0 "$pid" 2>/dev/null; then
656 local qmp_sock="$DAEMON_SOCKET_DIR/qmp.sock"
657 if [ -S "$qmp_sock" ]; then
658 log "INFO" "Sending QMP quit..."
659 echo '{"execute":"qmp_capabilities"}{"execute":"quit"}' | \
660 socat - "UNIX-CONNECT:$qmp_sock" >/dev/null 2>&1 || true
661 sleep 2
662 fi
663 fi
664
665 # If QMP quit didn't take (or no QMP socket — older configs), fall
666 # back to SIGTERM.
597 if kill -0 "$pid" 2>/dev/null; then 667 if kill -0 "$pid" 2>/dev/null; then
598 log "INFO" "Sending SIGTERM..." 668 log "INFO" "Sending SIGTERM..."
599 kill "$pid" 2>/dev/null || true 669 kill "$pid" 2>/dev/null || true
@@ -847,6 +917,124 @@ fi
847TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$" 917TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$"
848mkdir -p "$TEMP_DIR" 918mkdir -p "$TEMP_DIR"
849 919
920# ============================================================================
921# Registry auth config (docker config.json / podman auth.json)
922# ============================================================================
923# The AUTH_CONFIG path (from $VDKR_CONFIG, $VPDMN_CONFIG, or --config) points
924# to a file containing container-registry credentials. For defence-in-depth we:
925# * reject non-regular files (symlinks, devices, directories)
926# * reject files readable by group/other (mode must be <= 0600)
927# * warn if the file is not owned by the invoking user
928# * copy it into a private per-invocation directory under $TEMP_DIR at 0400
929# * expose it to the guest via a *separate* read-only virtio-9p tag
930# ("${TOOL_NAME}_auth") mounted at /mnt/auth (not the generic /mnt/share
931# which holds input/output and is wiped between daemon commands)
932# * never pass the file contents or path on the kernel cmdline; only a flag
933# "${CMDLINE_PREFIX}_auth=1" to tell the init script to look at /mnt/auth
934# * rely on the existing $TEMP_DIR EXIT/INT/TERM trap to delete the copy
935#
936# The auth file is never logged (path is visible, but contents are not).
937AUTH_SHARE_DIR=""
938
939validate_auth_config() {
940 local path="$1"
941
942 # Resolve symlinks to the canonical path so the perm check applies to the
943 # actual file, but still require the *named* path to be a regular file
944 # (not a symlink pointing into sensitive areas like /proc/self/environ).
945 if [ -L "$path" ]; then
946 log "ERROR" "--config must not be a symlink: $path"
947 return 1
948 fi
949 if [ ! -e "$path" ]; then
950 log "ERROR" "--config file not found: $path"
951 return 1
952 fi
953 if [ ! -f "$path" ]; then
954 log "ERROR" "--config must be a regular file: $path"
955 return 1
956 fi
957 if [ ! -r "$path" ]; then
958 log "ERROR" "--config file is not readable: $path"
959 return 1
960 fi
961
962 # Size sanity: docker config.json / podman auth.json should be small.
963 # 1 MiB is already generous. Reject unusually large files to avoid
964 # accidentally shipping a large credential blob.
965 local size
966 size=$(stat -c %s "$path" 2>/dev/null || echo 0)
967 if [ "$size" -gt 1048576 ]; then
968 log "ERROR" "--config file is too large ($size bytes, max 1 MiB): $path"
969 return 1
970 fi
971 # Minimum valid JSON object "{}" is 2 bytes. Anything smaller (including a
972 # 0-byte truncation or a lone newline from "echo '' > file") can't be a
973 # real auth config; reject rather than silently shipping garbage.
974 if [ "$size" -lt 2 ]; then
975 log "ERROR" "--config file is empty or too small to be valid JSON: $path"
976 return 1
977 fi
978
979 # Permission check: must not be readable by group or world.
980 local mode
981 mode=$(stat -c %a "$path" 2>/dev/null || echo 0)
982 # stat %a emits octal without leading zero. Forbid any group/other bits.
983 case "$mode" in
984 400|600|200) ;;
985 *)
986 log "ERROR" "--config file has unsafe permissions ($mode); expected 0600 or 0400."
987 log "ERROR" "Fix with: chmod 600 \"$path\""
988 return 1
989 ;;
990 esac
991
992 # Ownership check: warn if file is not owned by the current user.
993 local uid owner
994 uid=$(id -u)
995 owner=$(stat -c %u "$path" 2>/dev/null || echo "")
996 if [ -n "$owner" ] && [ "$owner" != "$uid" ]; then
997 log "WARN" "--config file is not owned by current user (uid=$uid, owner=$owner)"
998 fi
999
1000 return 0
1001}
1002
1003# Stage the auth config into a dedicated read-only 9p share. Must be called
1004# AFTER $TEMP_DIR exists and AFTER hypervisor backend functions are sourced.
1005# Sets $AUTH_SHARE_DIR and appends to $HV_OPTS / $KERNEL_APPEND.
1006setup_auth_share() {
1007 [ -z "$AUTH_CONFIG" ] && return 0
1008
1009 if ! validate_auth_config "$AUTH_CONFIG"; then
1010 log "ERROR" "Refusing to stage $AUTH_CONFIG — see above."
1011 exit 1
1012 fi
1013
1014 AUTH_SHARE_DIR="$TEMP_DIR/auth_share"
1015 # 0700 so nothing outside our process can peek at the staged file.
1016 mkdir -p "$AUTH_SHARE_DIR"
1017 chmod 700 "$AUTH_SHARE_DIR"
1018
1019 # Always stage as config.json regardless of source filename — the guest
1020 # init script knows to look for this fixed name.
1021 if ! cp "$AUTH_CONFIG" "$AUTH_SHARE_DIR/config.json"; then
1022 log "ERROR" "Failed to stage auth config"
1023 exit 1
1024 fi
1025 chmod 400 "$AUTH_SHARE_DIR/config.json"
1026
1027 local auth_tag="${TOOL_NAME}_auth"
1028 hv_build_9p_opts "$AUTH_SHARE_DIR" "$auth_tag" "readonly=on"
1029 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_auth=1"
1030
1031 # Deliberately log the *fact* of staging, not the path contents or
1032 # credentials. The path itself is useful for debugging and appears in
1033 # --verbose mode only.
1034 log "INFO" "Registry auth config staged on read-only 9p share (tag=$auth_tag)"
1035 log "DEBUG" "Auth source: $AUTH_CONFIG"
1036}
1037
850cleanup() { 1038cleanup() {
851 if [ "$KEEP_TEMP" = "true" ]; then 1039 if [ "$KEEP_TEMP" = "true" ]; then
852 log "DEBUG" "Keeping temp directory: $TEMP_DIR" 1040 log "DEBUG" "Keeping temp directory: $TEMP_DIR"
@@ -956,11 +1144,15 @@ if [ "$BATCH_IMPORT" = "true" ]; then
956 fi 1144 fi
957 done 1145 done
958 1146
959 # Add final images command to show what was imported 1147 # Show what was imported (informational only).
1148 # IMPORTANT: Must not use 'exit' — the command runs inside PID 1 init's
1149 # eval, and exit kills init → kernel panic. The import chain runs in a
1150 # subshell so its exit code is captured without risk. The images listing
1151 # is best-effort and doesn't affect the result.
960 if [ "$RUNTIME" = "docker" ]; then 1152 if [ "$RUNTIME" = "docker" ]; then
961 COMPOUND_CMD="$COMPOUND_CMD && docker images" 1153 COMPOUND_CMD="( $COMPOUND_CMD ); docker images 2>/dev/null; true"
962 else 1154 else
963 COMPOUND_CMD="$COMPOUND_CMD && podman images" 1155 COMPOUND_CMD="( $COMPOUND_CMD ); podman images 2>/dev/null; true"
964 fi 1156 fi
965 1157
966 log "DEBUG" "Batch command: $COMPOUND_CMD" 1158 log "DEBUG" "Batch command: $COMPOUND_CMD"
@@ -1306,6 +1498,10 @@ if [ "$DAEMON_MODE" = "start" ]; then
1306 log "DEBUG" "CA certificate copied to shared folder" 1498 log "DEBUG" "CA certificate copied to shared folder"
1307 fi 1499 fi
1308 1500
1501 # Stage registry auth config (config.json / auth.json) on a dedicated
1502 # read-only 9p share. See setup_auth_share() for the security model.
1503 setup_auth_share
1504
1309 log "INFO" "Starting daemon..." 1505 log "INFO" "Starting daemon..."
1310 log "DEBUG" "PID file: $DAEMON_PID_FILE" 1506 log "DEBUG" "PID file: $DAEMON_PID_FILE"
1311 log "DEBUG" "Socket: $DAEMON_SOCKET" 1507 log "DEBUG" "Socket: $DAEMON_SOCKET"
@@ -1361,7 +1557,18 @@ if [ "$DAEMON_MODE" = "start" ]; then
1361 # Set up port forwards via backend (e.g., iptables for Xen) 1557 # Set up port forwards via backend (e.g., iptables for Xen)
1362 hv_setup_port_forwards 1558 hv_setup_port_forwards
1363 1559
1364 # Start host-side idle watchdog if timeout is set 1560 # Start host-side idle watchdog if timeout is set.
1561 #
1562 # The watchdog is a long-running background subshell that outlives
1563 # vrunner.sh itself. It MUST fully detach from the invoking shell's
1564 # stdio: when the caller (e.g. the vdkr CLI, or a test harness that
1565 # wraps vdkr in subprocess.run(capture_output=True)) reads
1566 # stdout/stderr via pipes, any inherited write-end fd in the
1567 # watchdog keeps those pipes open and blocks the caller's
1568 # communicate()/read until the daemon is stopped (up to
1569 # IDLE_TIMEOUT, default 30 minutes). Redirect all three fds so the
1570 # watchdog holds no descriptors from the caller, and disown it so
1571 # the shell's job table doesn't retain it either.
1365 if [ "$IDLE_TIMEOUT" -gt 0 ] 2>/dev/null; then 1572 if [ "$IDLE_TIMEOUT" -gt 0 ] 2>/dev/null; then
1366 ACTIVITY_FILE="$DAEMON_SOCKET_DIR/activity" 1573 ACTIVITY_FILE="$DAEMON_SOCKET_DIR/activity"
1367 touch "$ACTIVITY_FILE" 1574 touch "$ACTIVITY_FILE"
@@ -1395,7 +1602,8 @@ if [ "$DAEMON_MODE" = "start" ]; then
1395 exit 0 1602 exit 0
1396 fi 1603 fi
1397 done 1604 done
1398 ) & 1605 ) </dev/null >/dev/null 2>&1 &
1606 disown $! 2>/dev/null || true
1399 log "DEBUG" "Started host-side idle watchdog (timeout: ${IDLE_TIMEOUT}s)" 1607 log "DEBUG" "Started host-side idle watchdog (timeout: ${IDLE_TIMEOUT}s)"
1400 fi 1608 fi
1401 1609
@@ -1423,6 +1631,11 @@ if [ -n "$CA_CERT" ] && [ -f "$CA_CERT" ]; then
1423 log "DEBUG" "CA certificate available via 9p" 1631 log "DEBUG" "CA certificate available via 9p"
1424fi 1632fi
1425 1633
1634# Stage registry auth config (config.json / auth.json) on a dedicated read-only
1635# 9p share for non-daemon and batch-import modes. Safe to call when AUTH_CONFIG
1636# is empty — it no-ops. See setup_auth_share() for the security model.
1637setup_auth_share
1638
1426log "INFO" "Starting VM ($VCONTAINER_HYPERVISOR)..." 1639log "INFO" "Starting VM ($VCONTAINER_HYPERVISOR)..."
1427 1640
1428# Interactive mode runs VM in foreground with stdio connected 1641# Interactive mode runs VM in foreground with stdio connected