diff options
Diffstat (limited to 'recipes-containers/vcontainer/files/vrunner.sh')
| -rwxr-xr-x | recipes-containers/vcontainer/files/vrunner.sh | 229 |
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}}" | |||
| 38 | TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}" | 38 | TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}" |
| 39 | VERBOSE="${VDKR_VERBOSE:-${VPDMN_VERBOSE:-false}}" | 39 | VERBOSE="${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. | ||
| 46 | AUTH_CONFIG="${VDKR_CONFIG:-${VPDMN_CONFIG:-}}" | ||
| 47 | |||
| 41 | # Runtime-specific settings (set after parsing --runtime) | 48 | # Runtime-specific settings (set after parsing --runtime) |
| 42 | set_runtime_config() { | 49 | set_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 | |||
| 847 | TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$" | 917 | TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$" |
| 848 | mkdir -p "$TEMP_DIR" | 918 | mkdir -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). | ||
| 937 | AUTH_SHARE_DIR="" | ||
| 938 | |||
| 939 | validate_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. | ||
| 1006 | setup_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 | |||
| 850 | cleanup() { | 1038 | cleanup() { |
| 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" |
| 1424 | fi | 1632 | fi |
| 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. | ||
| 1637 | setup_auth_share | ||
| 1638 | |||
| 1426 | log "INFO" "Starting VM ($VCONTAINER_HYPERVISOR)..." | 1639 | log "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 |
