summaryrefslogtreecommitdiffstats
path: root/recipes-containers/vcontainer
diff options
context:
space:
mode:
Diffstat (limited to 'recipes-containers/vcontainer')
-rw-r--r--recipes-containers/vcontainer/README.md103
-rwxr-xr-xrecipes-containers/vcontainer/files/vcontainer-common.sh122
-rwxr-xr-xrecipes-containers/vcontainer/files/vcontainer-init-common.sh54
-rwxr-xr-xrecipes-containers/vcontainer/files/vdkr-init.sh58
-rwxr-xr-xrecipes-containers/vcontainer/files/vpdmn-init.sh61
-rw-r--r--recipes-containers/vcontainer/files/vrunner-backend-qemu.sh10
-rw-r--r--recipes-containers/vcontainer/files/vrunner-backend-xen.sh16
-rwxr-xr-xrecipes-containers/vcontainer/files/vrunner.sh229
-rw-r--r--recipes-containers/vcontainer/vcontainer-initramfs-create.inc29
-rw-r--r--recipes-containers/vcontainer/vcontainer-tarball.bb63
-rw-r--r--recipes-containers/vcontainer/vdkr-rootfs-image.bb3
-rw-r--r--recipes-containers/vcontainer/vpdmn-rootfs-image.bb24
12 files changed, 696 insertions, 76 deletions
diff --git a/recipes-containers/vcontainer/README.md b/recipes-containers/vcontainer/README.md
index 76d54d18..e44616f4 100644
--- a/recipes-containers/vcontainer/README.md
+++ b/recipes-containers/vcontainer/README.md
@@ -268,6 +268,109 @@ vdkr pull alpine:latest
268vdkr images # Shows alpine:latest 268vdkr images # Shows alpine:latest
269``` 269```
270 270
271## Registry Login and Configuration
272
273For authenticated registries:
274
275```bash
276# Login (interactive password prompt)
277vdkr login --username myuser https://registry.example.com/
278
279# Pull from the registry
280vdkr pull registry.example.com/myimage:latest
281```
282
283Set a default registry so you don't need to specify the full URL each time:
284
285```bash
286# Set default registry (persisted across sessions)
287vdkr vconfig registry registry.example.com
288
289# Now pulls try the default registry first, then Docker Hub
290vdkr pull myimage:latest
291
292# One-off override without changing the default
293vdkr --registry other.registry.com pull myimage:latest
294
295# Clear default registry
296vdkr vconfig registry --reset
297```
298
299**Note:** The `--registry` flag is a vdkr option that sets the default
300registry for pulls. For `login`, pass the registry URL as a positional
301argument after the login flags:
302
303```bash
304# Correct:
305vdkr login --username myuser https://registry.example.com/
306
307# Wrong (--registry is consumed by vdkr, login gets no URL):
308vdkr --registry https://registry.example.com/ login --username myuser
309```
310
311**TLS certificates:** The vdkr/vpdmn rootfs images include common
312intermediate certificates (Let's Encrypt E8/R11) to handle registries
313that don't send the full certificate chain. For self-signed registries,
314use `--secure-registry --ca-cert`:
315
316```bash
317vdkr --secure-registry --ca-cert /path/to/ca.crt pull myimage
318```
319
320### Passing an existing docker/podman auth file (`--config`)
321
322If you already have credentials set up on the host (for example, from
323running `docker login` locally), you can pass the resulting auth file
324straight through into the emulated environment instead of re-entering
325credentials with `--registry-user`/`--registry-pass`:
326
327```bash
328# Docker (vdkr): uses ~/.docker/config.json by default
329vdkr --config ~/.docker/config.json pull registry.example.com/myimage
330
331# Podman (vpdmn): uses $XDG_RUNTIME_DIR/containers/auth.json
332vpdmn --config $XDG_RUNTIME_DIR/containers/auth.json pull registry.example.com/myimage
333```
334
335The path can also be supplied via environment:
336
337```bash
338export VDKR_CONFIG=$HOME/.docker/config.json
339vdkr pull registry.example.com/myimage
340```
341
342(`VPDMN_CONFIG` is honoured identically by `vpdmn`.)
343
344**What the file ends up as inside the VM:**
345
346| Tool | Target path | Notes |
347| ----- | --------------------------------- | ------------------------------------------------- |
348| vdkr | `/root/.docker/config.json` | Mode 0600; containing dir 0700 |
349| vpdmn | `/run/containers/0/auth.json` | Mode 0600; `$REGISTRY_AUTH_FILE` exported |
350
351**Security model.** The credential file is treated as secret material:
352
353- The host-side file **must** be a regular file with mode `0600` or `0400`.
354 World/group-readable files are rejected outright. Symlinks are rejected.
355 Files larger than 1 MiB are rejected.
356- On the host it is copied into a per-invocation private directory under
357 `$TMPDIR/vdkr-$$/auth_share` (mode 0700; file mode 0400) and removed
358 automatically by the `EXIT`/`INT`/`TERM` trap when `vrunner.sh` exits.
359- It is exposed to the guest on a **dedicated** virtio-9p share whose
360 mount tag (`vdkr_auth` / `vpdmn_auth`) is distinct from the general
361 `*_share` share used for input/output. The guest mounts it **read-only**
362 at `/mnt/auth`, copies it into the runtime's credential location, then
363 **unmounts** `/mnt/auth` so nothing in the VM retains an open reference
364 to the host staging directory.
365- Nothing about the file appears on the kernel command line. Only a
366 boolean flag (`docker_auth=1` / `podman_auth=1`) is passed so the guest
367 init script knows to look on the auth share.
368- When both `--config` and `--registry-user`/`--registry-pass` are
369 supplied, `--config` wins and a NOTE is logged.
370- `--config` is NOT forwarded into container workloads (it only reaches
371 the container engine's credential store); containers themselves never
372 see `/mnt/auth`.
373
271## Volume Mounts 374## Volume Mounts
272 375
273Mount host directories into containers using `-v` (requires memory resident mode): 376Mount host directories into containers using `-v` (requires memory resident mode):
diff --git a/recipes-containers/vcontainer/files/vcontainer-common.sh b/recipes-containers/vcontainer/files/vcontainer-common.sh
index c7160860..f389941c 100755
--- a/recipes-containers/vcontainer/files/vcontainer-common.sh
+++ b/recipes-containers/vcontainer/files/vcontainer-common.sh
@@ -327,40 +327,54 @@ normalize_arch_from_oci() {
327 esac 327 esac
328} 328}
329 329
330# Check if OCI directory contains a multi-architecture Image Index 330# Resolve the file containing platform manifests in an OCI directory.
331# Usage: is_oci_image_index <oci_dir> 331# Handles two layouts:
332# Returns: 0 if multi-arch, 1 if single-arch or not OCI 332# Flat: index.json directly contains manifests with platform info
333is_oci_image_index() { 333# Nested: index.json → single image index blob → platform manifests
334# (skopeo-compatible format)
335# Usage: _resolve_oci_platform_file <oci_dir>
336# Prints: path to the file containing platform manifests, or empty
337_resolve_oci_platform_file() {
334 local oci_dir="$1" 338 local oci_dir="$1"
335 339
336 [ -f "$oci_dir/index.json" ] || return 1 340 [ -f "$oci_dir/index.json" ] || return 1
337 341
338 # Check if index.json has manifests with platform info 342 # Flat layout: platform info directly in index.json
339 # Multi-arch images have "platform" object in manifest entries
340 if grep -q '"platform"' "$oci_dir/index.json" 2>/dev/null; then 343 if grep -q '"platform"' "$oci_dir/index.json" 2>/dev/null; then
341 # Also verify there are multiple manifests 344 echo "$oci_dir/index.json"
342 local manifest_count=$(grep -c '"digest"' "$oci_dir/index.json" 2>/dev/null || echo "0")
343 [ "$manifest_count" -gt 1 ] && return 0
344
345 # Single manifest with platform info is also a valid Image Index
346 # (could be a multi-arch image built with only one arch so far)
347 return 0 345 return 0
348 fi 346 fi
349 347
348 # Nested layout: index.json has a single entry with image.index mediaType
349 if grep -q 'image\.index' "$oci_dir/index.json" 2>/dev/null; then
350 local index_digest=$(grep -o '"sha256:[a-f0-9]*"' "$oci_dir/index.json" 2>/dev/null | head -1 | tr -d '"' | sed 's/sha256://')
351 if [ -n "$index_digest" ] && [ -f "$oci_dir/blobs/sha256/$index_digest" ]; then
352 if grep -q '"platform"' "$oci_dir/blobs/sha256/$index_digest" 2>/dev/null; then
353 echo "$oci_dir/blobs/sha256/$index_digest"
354 return 0
355 fi
356 fi
357 fi
358
350 return 1 359 return 1
351} 360}
352 361
362# Check if OCI directory contains a multi-architecture Image Index
363# Usage: is_oci_image_index <oci_dir>
364# Returns: 0 if multi-arch, 1 if single-arch or not OCI
365is_oci_image_index() {
366 _resolve_oci_platform_file "$1" >/dev/null 2>&1
367}
368
353# Get list of available platforms in a multi-arch OCI Image Index 369# Get list of available platforms in a multi-arch OCI Image Index
354# Usage: get_oci_platforms <oci_dir> 370# Usage: get_oci_platforms <oci_dir>
355# Returns: Space-separated list of architectures (e.g., "arm64 amd64") 371# Returns: Space-separated list of architectures (e.g., "arm64 amd64")
356get_oci_platforms() { 372get_oci_platforms() {
357 local oci_dir="$1" 373 local oci_dir="$1"
374 local platform_file
375 platform_file=$(_resolve_oci_platform_file "$oci_dir") || return 1
358 376
359 [ -f "$oci_dir/index.json" ] || return 1 377 grep -o '"architecture"[[:space:]]*:[[:space:]]*"[^"]*"' "$platform_file" 2>/dev/null | \
360
361 # Extract architecture values from platform objects
362 # Format: "platform": { "architecture": "arm64", "os": "linux" }
363 grep -o '"architecture"[[:space:]]*:[[:space:]]*"[^"]*"' "$oci_dir/index.json" 2>/dev/null | \
364 sed 's/.*"\([^"]*\)"$/\1/' | \ 378 sed 's/.*"\([^"]*\)"$/\1/' | \
365 tr '\n' ' ' | sed 's/ $//' 379 tr '\n' ' ' | sed 's/ $//'
366} 380}
@@ -373,38 +387,34 @@ select_platform_manifest() {
373 local oci_dir="$1" 387 local oci_dir="$1"
374 local target_arch="$2" 388 local target_arch="$2"
375 389
376 [ -f "$oci_dir/index.json" ] || return 1
377
378 # Normalize target arch to OCI convention 390 # Normalize target arch to OCI convention
379 local oci_arch=$(normalize_arch_to_oci "$target_arch") 391 local oci_arch=$(normalize_arch_to_oci "$target_arch")
380 392
381 # Parse index.json to find manifest with matching platform 393 # Resolve the file containing platform manifests (flat or nested)
394 local manifest_index
395 manifest_index=$(_resolve_oci_platform_file "$oci_dir") || return 1
396
397 # Parse the manifest index to find manifest with matching platform
382 # This is done without jq using grep/sed for portability 398 # This is done without jq using grep/sed for portability
383 local in_manifest=0 399 local in_manifest=0
384 local current_digest="" 400 local current_digest=""
385 local current_arch="" 401 local current_arch=""
386 local matched_digest="" 402 local matched_digest=""
387 403
388 # Read index.json line by line
389 while IFS= read -r line; do 404 while IFS= read -r line; do
390 # Track when we're inside a manifest entry
391 if echo "$line" | grep -q '"manifests"'; then 405 if echo "$line" | grep -q '"manifests"'; then
392 in_manifest=1 406 in_manifest=1
393 continue 407 continue
394 fi 408 fi
395 409
396 if [ "$in_manifest" = "1" ]; then 410 if [ "$in_manifest" = "1" ]; then
397 # Extract digest
398 if echo "$line" | grep -q '"digest"'; then 411 if echo "$line" | grep -q '"digest"'; then
399 current_digest=$(echo "$line" | sed 's/.*"sha256:\([a-f0-9]*\)".*/\1/') 412 current_digest=$(echo "$line" | sed 's/.*"sha256:\([a-f0-9]*\)".*/\1/')
400 fi 413 fi
401 414
402 # Extract architecture from platform
403 # Handle both formats: "architecture": "arm64" or {"architecture": "arm64", ...}
404 if echo "$line" | grep -q '"architecture"'; then 415 if echo "$line" | grep -q '"architecture"'; then
405 current_arch=$(echo "$line" | sed 's/.*"architecture"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') 416 current_arch=$(echo "$line" | sed 's/.*"architecture"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
406 417
407 # Check if this matches our target
408 if [ "$current_arch" = "$oci_arch" ]; then 418 if [ "$current_arch" = "$oci_arch" ]; then
409 matched_digest="$current_digest" 419 matched_digest="$current_digest"
410 OCI_SELECTED_PLATFORM="$current_arch" 420 OCI_SELECTED_PLATFORM="$current_arch"
@@ -412,13 +422,12 @@ select_platform_manifest() {
412 fi 422 fi
413 fi 423 fi
414 424
415 # Reset on closing brace (end of manifest entry)
416 if echo "$line" | grep -q '^[[:space:]]*}'; then 425 if echo "$line" | grep -q '^[[:space:]]*}'; then
417 current_digest="" 426 current_digest=""
418 current_arch="" 427 current_arch=""
419 fi 428 fi
420 fi 429 fi
421 done < "$oci_dir/index.json" 430 done < "$manifest_index"
422 431
423 if [ -n "$matched_digest" ]; then 432 if [ -n "$matched_digest" ]; then
424 echo "$matched_digest" 433 echo "$matched_digest"
@@ -718,6 +727,11 @@ ${BOLD}GLOBAL OPTIONS:${NC}
718 --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto) 727 --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto)
719 --no-registry Disable baked-in default registry (use images as-is) 728 --no-registry Disable baked-in default registry (use images as-is)
720 --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat. 729 --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat.
730 --config <path> Registry auth file (docker config.json / podman auth.json)
731 Defaults to \$VDKR_CONFIG / \$VPDMN_CONFIG. The file must be
732 mode 0600 or stricter; it is passed to the guest over a
733 dedicated read-only virtio-9p share and never appears on
734 the kernel cmdline.
721 --verbose, -v Enable verbose output 735 --verbose, -v Enable verbose output
722 --help, -h Show this help 736 --help, -h Show this help
723 737
@@ -857,6 +871,7 @@ build_runner_args() {
857 [ -n "$CA_CERT" ] && args+=("--ca-cert" "$CA_CERT") 871 [ -n "$CA_CERT" ] && args+=("--ca-cert" "$CA_CERT")
858 [ -n "$REGISTRY_USER" ] && args+=("--registry-user" "$REGISTRY_USER") 872 [ -n "$REGISTRY_USER" ] && args+=("--registry-user" "$REGISTRY_USER")
859 [ -n "$REGISTRY_PASS" ] && args+=("--registry-pass" "$REGISTRY_PASS") 873 [ -n "$REGISTRY_PASS" ] && args+=("--registry-pass" "$REGISTRY_PASS")
874 [ -n "$AUTH_CONFIG" ] && args+=("--config" "$AUTH_CONFIG")
860 875
861 # Xen: pass exit grace period 876 # Xen: pass exit grace period
862 [ -n "${VXN_EXIT_GRACE_PERIOD:-}" ] && args+=("--exit-grace-period" "$VXN_EXIT_GRACE_PERIOD") 877 [ -n "${VXN_EXIT_GRACE_PERIOD:-}" ] && args+=("--exit-grace-period" "$VXN_EXIT_GRACE_PERIOD")
@@ -880,6 +895,11 @@ SECURE_REGISTRY="false"
880CA_CERT="" 895CA_CERT=""
881REGISTRY_USER="" 896REGISTRY_USER=""
882REGISTRY_PASS="" 897REGISTRY_PASS=""
898# Registry auth config file. Env-var default depends on which CLI wrapper is
899# in use (vdkr → $VDKR_CONFIG, vpdmn → $VPDMN_CONFIG), then falls back to the
900# other for convenience when sharing a single host-side file. Overridden by
901# the --config CLI flag below.
902AUTH_CONFIG="${VDKR_CONFIG:-${VPDMN_CONFIG:-}}"
883COMMAND="" 903COMMAND=""
884COMMAND_ARGS=() 904COMMAND_ARGS=()
885 905
@@ -977,6 +997,13 @@ while [ $# -gt 0 ]; do
977 REGISTRY_PASS="$2" 997 REGISTRY_PASS="$2"
978 shift 2 998 shift 2
979 ;; 999 ;;
1000 --config)
1001 # Path to a docker/podman registry auth file (config.json / auth.json).
1002 # Overrides $VDKR_CONFIG / $VPDMN_CONFIG. Forwarded to vrunner.sh --config,
1003 # which validates the file and stages it on a dedicated read-only 9p share.
1004 AUTH_CONFIG="$2"
1005 shift 2
1006 ;;
980 -it|--interactive) 1007 -it|--interactive)
981 INTERACTIVE="true" 1008 INTERACTIVE="true"
982 shift 1009 shift
@@ -1858,11 +1885,11 @@ case "$COMMAND" in
1858 1885
1859 # Check for multi-architecture OCI Image Index 1886 # Check for multi-architecture OCI Image Index
1860 if is_oci_image_index "$INPUT_PATH"; then 1887 if is_oci_image_index "$INPUT_PATH"; then
1861 local available_platforms=$(get_oci_platforms "$INPUT_PATH") 1888 available_platforms=$(get_oci_platforms "$INPUT_PATH")
1862 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Multi-arch OCI detected. Available: $available_platforms" >&2 1889 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Multi-arch OCI detected. Available: $available_platforms" >&2
1863 1890
1864 # Select manifest for target architecture 1891 # Select manifest for target architecture
1865 local manifest_digest=$(select_platform_manifest "$INPUT_PATH" "$TARGET_ARCH") 1892 manifest_digest=$(select_platform_manifest "$INPUT_PATH" "$TARGET_ARCH")
1866 if [ -z "$manifest_digest" ]; then 1893 if [ -z "$manifest_digest" ]; then
1867 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Architecture $TARGET_ARCH not found in multi-arch image" >&2 1894 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Architecture $TARGET_ARCH not found in multi-arch image" >&2
1868 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Available platforms: $available_platforms" >&2 1895 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Available platforms: $available_platforms" >&2
@@ -1870,7 +1897,7 @@ case "$COMMAND" in
1870 exit 1 1897 exit 1
1871 fi 1898 fi
1872 1899
1873 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Selected platform: $OCI_SELECTED_PLATFORM (from multi-arch image)" >&2 1900 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} Selected platform: $(normalize_arch_to_oci "$TARGET_ARCH")/linux (from multi-arch image)" >&2
1874 1901
1875 # Extract single-platform OCI to temp directory 1902 # Extract single-platform OCI to temp directory
1876 TEMP_OCI_DIR=$(mktemp -d) 1903 TEMP_OCI_DIR=$(mktemp -d)
@@ -2196,9 +2223,14 @@ case "$COMMAND" in
2196 2223
2197 login) 2224 login)
2198 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "login" 2225 [ "${VCONTAINER_HYPERVISOR:-}" = "xen" ] && vxn_unsupported "login"
2199 # Login to registry - may need credentials via stdin 2226 # Login needs interactive stdin for password prompt.
2200 # For non-interactive: runtime login -u user -p pass registry 2227 # Use daemon-interactive mode (same as vshell/exec -it).
2201 run_runtime_command "$VCONTAINER_RUNTIME_CMD login ${COMMAND_ARGS[*]}" 2228 if daemon_is_running; then
2229 RUNNER_ARGS=$(build_runner_args)
2230 "$RUNNER" $RUNNER_ARGS --daemon-interactive -- "$VCONTAINER_RUNTIME_CMD login ${COMMAND_ARGS[*]}"
2231 else
2232 run_runtime_command "$VCONTAINER_RUNTIME_CMD login ${COMMAND_ARGS[*]}"
2233 fi
2202 ;; 2234 ;;
2203 2235
2204 logout) 2236 logout)
@@ -2483,12 +2515,20 @@ case "$COMMAND" in
2483 STORAGE_CMD="${COMMAND_ARGS[0]}" 2515 STORAGE_CMD="${COMMAND_ARGS[0]}"
2484 fi 2516 fi
2485 2517
2518 # When --state-dir is passed, scan its parent as the storage root
2519 # (STATE_DIR is an arch subdir like ~/.vpdmn-test/x86_64, so the
2520 # parent ~/.vpdmn-test/ is the root containing all arch dirs).
2521 VSTORAGE_ROOT="$DEFAULT_STATE_DIR"
2522 if [ -n "$STATE_DIR" ]; then
2523 VSTORAGE_ROOT="$(dirname "$STATE_DIR")"
2524 fi
2525
2486 case "$STORAGE_CMD" in 2526 case "$STORAGE_CMD" in
2487 list) 2527 list)
2488 echo "$VCONTAINER_RUNTIME_NAME storage directories:" 2528 echo "$VCONTAINER_RUNTIME_NAME storage directories:"
2489 echo "" 2529 echo ""
2490 found=0 2530 found=0
2491 for state_dir in "$DEFAULT_STATE_DIR"/*/; do 2531 for state_dir in "$VSTORAGE_ROOT"/*/; do
2492 [ -d "$state_dir" ] || continue 2532 [ -d "$state_dir" ] || continue
2493 found=1 2533 found=1
2494 instance=$(basename "$state_dir") 2534 instance=$(basename "$state_dir")
@@ -2517,8 +2557,8 @@ case "$COMMAND" in
2517 fi 2557 fi
2518 2558
2519 # Total size 2559 # Total size
2520 if [ -d "$DEFAULT_STATE_DIR" ] && [ $found -gt 0 ]; then 2560 if [ -d "$VSTORAGE_ROOT" ] && [ $found -gt 0 ]; then
2521 total=$(du -sh "$DEFAULT_STATE_DIR" 2>/dev/null | cut -f1) 2561 total=$(du -sh "$VSTORAGE_ROOT" 2>/dev/null | cut -f1)
2522 echo "Total: $total" 2562 echo "Total: $total"
2523 fi 2563 fi
2524 ;; 2564 ;;
@@ -2531,7 +2571,7 @@ case "$COMMAND" in
2531 2571
2532 df) 2572 df)
2533 # Detailed breakdown 2573 # Detailed breakdown
2534 for state_dir in "$DEFAULT_STATE_DIR"/*/; do 2574 for state_dir in "$VSTORAGE_ROOT"/*/; do
2535 [ -d "$state_dir" ] || continue 2575 [ -d "$state_dir" ] || continue
2536 instance=$(basename "$state_dir") 2576 instance=$(basename "$state_dir")
2537 echo "${BOLD}$instance${NC}:" 2577 echo "${BOLD}$instance${NC}:"
@@ -2552,7 +2592,7 @@ case "$COMMAND" in
2552 arch="${COMMAND_ARGS[1]:-}" 2592 arch="${COMMAND_ARGS[1]:-}"
2553 if [ "$arch" = "--all" ]; then 2593 if [ "$arch" = "--all" ]; then
2554 # Stop any running memres first 2594 # Stop any running memres first
2555 for pid_file in "$DEFAULT_STATE_DIR"/*/daemon.pid; do 2595 for pid_file in "$VSTORAGE_ROOT"/*/daemon.pid; do
2556 [ -f "$pid_file" ] || continue 2596 [ -f "$pid_file" ] || continue
2557 pid=$(cat "$pid_file" 2>/dev/null) 2597 pid=$(cat "$pid_file" 2>/dev/null)
2558 if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then 2598 if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then
@@ -2561,11 +2601,11 @@ case "$COMMAND" in
2561 fi 2601 fi
2562 done 2602 done
2563 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Removing all storage directories..." 2603 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Removing all storage directories..."
2564 rm -rf "$DEFAULT_STATE_DIR" 2604 rm -rf "$VSTORAGE_ROOT"
2565 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} All storage cleaned." 2605 echo -e "${GREEN}[$VCONTAINER_RUNTIME_NAME]${NC} All storage cleaned."
2566 elif [ -n "$arch" ]; then 2606 elif [ -n "$arch" ]; then
2567 # Clean specific arch 2607 # Clean specific arch
2568 clean_dir="$DEFAULT_STATE_DIR/$arch" 2608 clean_dir="$VSTORAGE_ROOT/$arch"
2569 if [ -d "$clean_dir" ]; then 2609 if [ -d "$clean_dir" ]; then
2570 # Stop memres if running 2610 # Stop memres if running
2571 if [ -f "$clean_dir/daemon.pid" ]; then 2611 if [ -f "$clean_dir/daemon.pid" ]; then
diff --git a/recipes-containers/vcontainer/files/vcontainer-init-common.sh b/recipes-containers/vcontainer/files/vcontainer-init-common.sh
index ab8762b2..3bd70e75 100755
--- a/recipes-containers/vcontainer/files/vcontainer-init-common.sh
+++ b/recipes-containers/vcontainer/files/vcontainer-init-common.sh
@@ -156,6 +156,7 @@ parse_cmdline() {
156 RUNTIME_INTERACTIVE="0" 156 RUNTIME_INTERACTIVE="0"
157 RUNTIME_DAEMON="0" 157 RUNTIME_DAEMON="0"
158 RUNTIME_9P="0" # virtio-9p available for fast I/O 158 RUNTIME_9P="0" # virtio-9p available for fast I/O
159 RUNTIME_AUTH="0" # registry auth config (config.json / auth.json) available on dedicated 9p share
159 RUNTIME_IDLE_TIMEOUT="1800" # Default: 30 minutes 160 RUNTIME_IDLE_TIMEOUT="1800" # Default: 30 minutes
160 161
161 for param in $(cat /proc/cmdline); do 162 for param in $(cat /proc/cmdline); do
@@ -187,6 +188,9 @@ parse_cmdline() {
187 ${VCONTAINER_RUNTIME_PREFIX}_9p=*) 188 ${VCONTAINER_RUNTIME_PREFIX}_9p=*)
188 RUNTIME_9P="${param#${VCONTAINER_RUNTIME_PREFIX}_9p=}" 189 RUNTIME_9P="${param#${VCONTAINER_RUNTIME_PREFIX}_9p=}"
189 ;; 190 ;;
191 ${VCONTAINER_RUNTIME_PREFIX}_auth=*)
192 RUNTIME_AUTH="${param#${VCONTAINER_RUNTIME_PREFIX}_auth=}"
193 ;;
190 esac 194 esac
191 done 195 done
192 196
@@ -263,6 +267,56 @@ mount_input_disk() {
263} 267}
264 268
265# ============================================================================ 269# ============================================================================
270# Registry auth share (docker config.json / podman auth.json)
271# ============================================================================
272# The host stages a validated credential file on a *dedicated* read-only 9p
273# share tagged "${VCONTAINER_RUNTIME_NAME}_auth" (e.g. "vdkr_auth" or
274# "vpdmn_auth"). That tag is separate from the general ${VCONTAINER_SHARE_NAME}
275# used for input/output so credentials can't leak into storage.tar outputs or
276# be overwritten by daemon_send_with_input.
277#
278# We mount read-only, nosuid, nodev, noexec at /mnt/auth. Callers are expected
279# to copy the credential file into the runtime's canonical location with
280# restrictive permissions and then call unmount_auth_share() so the guest
281# filesystem no longer has an open reference to the host-side file.
282
283AUTH_SHARE_TAG=""
284AUTH_SHARE_MOUNT="/mnt/auth"
285
286mount_auth_share() {
287 if [ "$RUNTIME_AUTH" != "1" ]; then
288 return 1
289 fi
290
291 AUTH_SHARE_TAG="${VCONTAINER_RUNTIME_NAME}_auth"
292 mkdir -p "$AUTH_SHARE_MOUNT"
293
294 # trans/version/cache match the existing 9p share mount. Add:
295 # ro - guest can't mutate the host-side staging directory
296 # nosuid - no setuid binaries can be executed from the share
297 # nodev - no device nodes honoured even if crafted
298 # noexec - no code can execute from the share (auth.json is pure data)
299 if mount -t 9p \
300 -o trans=${NINE_P_TRANSPORT},version=9p2000.L,cache=none,ro,nosuid,nodev,noexec \
301 "$AUTH_SHARE_TAG" "$AUTH_SHARE_MOUNT" 2>/dev/null; then
302 log "Mounted auth 9p share at $AUTH_SHARE_MOUNT (tag: $AUTH_SHARE_TAG, ro)"
303 return 0
304 fi
305
306 log "WARNING: Could not mount auth 9p share ($AUTH_SHARE_TAG)"
307 RUNTIME_AUTH="0"
308 return 1
309}
310
311unmount_auth_share() {
312 if mountpoint -q "$AUTH_SHARE_MOUNT" 2>/dev/null; then
313 umount "$AUTH_SHARE_MOUNT" 2>/dev/null || \
314 umount -l "$AUTH_SHARE_MOUNT" 2>/dev/null || true
315 fi
316 rmdir "$AUTH_SHARE_MOUNT" 2>/dev/null || true
317}
318
319# ============================================================================
266# Network Configuration 320# Network Configuration
267# ============================================================================ 321# ============================================================================
268 322
diff --git a/recipes-containers/vcontainer/files/vdkr-init.sh b/recipes-containers/vcontainer/files/vdkr-init.sh
index e1e869b2..4ad50668 100755
--- a/recipes-containers/vcontainer/files/vdkr-init.sh
+++ b/recipes-containers/vcontainer/files/vdkr-init.sh
@@ -26,6 +26,10 @@
26# docker_registry_ca=1 CA certificate available in /mnt/share/ca.crt 26# docker_registry_ca=1 CA certificate available in /mnt/share/ca.crt
27# docker_registry_user=<user> Registry username for authentication 27# docker_registry_user=<user> Registry username for authentication
28# docker_registry_pass=<base64> Base64-encoded registry password 28# docker_registry_pass=<base64> Base64-encoded registry password
29# docker_auth=1 A pre-built docker config.json is available
30# on a dedicated read-only 9p share tagged
31# "vdkr_auth" (mounted at /mnt/auth). Takes
32# precedence over docker_registry_user/pass.
29# 33#
30# Version: 2.5.0 34# Version: 2.5.0
31 35
@@ -159,6 +163,55 @@ EOF
159 fi 163 fi
160} 164}
161 165
166# Install a user-supplied docker config.json from the dedicated read-only
167# auth 9p share (mounted at /mnt/auth by mount_auth_share). This takes
168# precedence over credentials supplied via docker_registry_user/pass.
169#
170# Security posture:
171# * File is read from a read-only 9p share with a separate tag ("vdkr_auth")
172# so it cannot leak into /mnt/share outputs.
173# * Target is written with mode 0600 and the parent dir with mode 0700.
174# * We unmount /mnt/auth immediately after copying so neither the dockerd
175# runtime nor user workloads in the VM have an open reference to the
176# host-side staging directory.
177install_auth_config() {
178 if [ "$RUNTIME_AUTH" != "1" ]; then
179 return 0
180 fi
181
182 if ! mount_auth_share; then
183 log "WARNING: docker_auth=1 was set but the auth 9p share did not mount"
184 return 1
185 fi
186
187 local src="$AUTH_SHARE_MOUNT/config.json"
188 if [ ! -f "$src" ]; then
189 log "WARNING: expected $src on auth share but file is missing"
190 unmount_auth_share
191 return 1
192 fi
193
194 mkdir -p /root/.docker
195 chmod 700 /root/.docker
196
197 if cp "$src" /root/.docker/config.json 2>/dev/null; then
198 chmod 600 /root/.docker/config.json
199 log "Installed registry auth config at /root/.docker/config.json"
200 if [ -n "$DOCKER_REGISTRY_USER" ] || [ -n "$DOCKER_REGISTRY_PASS" ]; then
201 log "NOTE: --config takes precedence over --registry-user/--registry-pass"
202 fi
203 else
204 log "ERROR: failed to copy auth config to /root/.docker/config.json"
205 unmount_auth_share
206 return 1
207 fi
208
209 # Release the host-side share so credentials aren't still addressable
210 # through /mnt/auth for the lifetime of the VM.
211 unmount_auth_share
212 return 0
213}
214
162# ============================================================================ 215# ============================================================================
163# Docker-Specific Functions 216# Docker-Specific Functions
164# ============================================================================ 217# ============================================================================
@@ -684,6 +737,11 @@ parse_secure_registry_config
684# Install CA certificate for secure registry 737# Install CA certificate for secure registry
685install_registry_ca 738install_registry_ca
686 739
740# Install user-supplied docker config.json from the dedicated auth 9p share.
741# Must run AFTER install_registry_ca so that --config takes precedence when
742# both mechanisms are used.
743install_auth_config
744
687# Start containerd and dockerd (Docker-specific) 745# Start containerd and dockerd (Docker-specific)
688start_containerd 746start_containerd
689start_dockerd 747start_dockerd
diff --git a/recipes-containers/vcontainer/files/vpdmn-init.sh b/recipes-containers/vcontainer/files/vpdmn-init.sh
index 7f661102..2036ed39 100755
--- a/recipes-containers/vcontainer/files/vpdmn-init.sh
+++ b/recipes-containers/vcontainer/files/vpdmn-init.sh
@@ -20,6 +20,12 @@
20# podman_output=<type> Output type: text, tar, storage (default: text) 20# podman_output=<type> Output type: text, tar, storage (default: text)
21# podman_state=<type> State type: none, disk (default: none) 21# podman_state=<type> State type: none, disk (default: none)
22# podman_network=1 Enable networking (configure eth0, DNS) 22# podman_network=1 Enable networking (configure eth0, DNS)
23# podman_auth=1 A pre-built registry auth file (docker config.json
24# schema, "auths" block) is available on a dedicated
25# read-only 9p share tagged "vpdmn_auth" (mounted at
26# /mnt/auth). Installed as /run/containers/0/auth.json
27# (the rootful podman default), and exported via
28# $REGISTRY_AUTH_FILE.
23# 29#
24# Version: 1.1.0 30# Version: 1.1.0
25# 31#
@@ -97,6 +103,57 @@ verify_podman() {
97 fi 103 fi
98} 104}
99 105
106# Install a user-supplied registry auth file from the dedicated read-only
107# auth 9p share (mounted at /mnt/auth by mount_auth_share). Podman accepts
108# the same "auths" JSON schema as docker config.json, so we can copy directly.
109#
110# Canonical rootful path is /run/containers/0/auth.json; we also export
111# $REGISTRY_AUTH_FILE so it works regardless of podman's search order.
112#
113# Security posture matches vdkr-init.sh install_auth_config:
114# * Source is a separate read-only 9p tag ("vpdmn_auth") so it cannot leak
115# into /mnt/share outputs.
116# * Target has mode 0600; containing dir has mode 0700.
117# * /mnt/auth is unmounted immediately after copy so user workloads in the
118# VM have no open reference to the host-side staging directory.
119install_auth_config() {
120 if [ "$RUNTIME_AUTH" != "1" ]; then
121 return 0
122 fi
123
124 if ! mount_auth_share; then
125 log "WARNING: podman_auth=1 was set but the auth 9p share did not mount"
126 return 1
127 fi
128
129 local src="$AUTH_SHARE_MOUNT/config.json"
130 if [ ! -f "$src" ]; then
131 log "WARNING: expected $src on auth share but file is missing"
132 unmount_auth_share
133 return 1
134 fi
135
136 # Rootful podman's default auth path
137 local auth_dir="/run/containers/0"
138 local auth_file="$auth_dir/auth.json"
139
140 mkdir -p "$auth_dir"
141 chmod 700 "$auth_dir"
142
143 if cp "$src" "$auth_file" 2>/dev/null; then
144 chmod 600 "$auth_file"
145 export REGISTRY_AUTH_FILE="$auth_file"
146 log "Installed registry auth config at $auth_file"
147 else
148 log "ERROR: failed to copy auth config to $auth_file"
149 unmount_auth_share
150 return 1
151 fi
152
153 unmount_auth_share
154 return 0
155}
156
100# Podman is daemonless - nothing to stop 157# Podman is daemonless - nothing to stop
101stop_runtime_daemons() { 158stop_runtime_daemons() {
102 : 159 :
@@ -190,6 +247,10 @@ configure_networking
190# Verify podman is available (no daemon to start) 247# Verify podman is available (no daemon to start)
191verify_podman 248verify_podman
192 249
250# Install user-supplied auth config from the dedicated auth 9p share, if any.
251# Done before command execution so pulls/logins have credentials available.
252install_auth_config
253
193# Handle daemon mode or single command execution 254# Handle daemon mode or single command execution
194if [ "$RUNTIME_DAEMON" = "1" ]; then 255if [ "$RUNTIME_DAEMON" = "1" ]; then
195 run_daemon_mode 256 run_daemon_mode
diff --git a/recipes-containers/vcontainer/files/vrunner-backend-qemu.sh b/recipes-containers/vcontainer/files/vrunner-backend-qemu.sh
index 87054876..3ea73cc7 100644
--- a/recipes-containers/vcontainer/files/vrunner-backend-qemu.sh
+++ b/recipes-containers/vcontainer/files/vrunner-backend-qemu.sh
@@ -188,10 +188,16 @@ hv_start_vm_background() {
188 local log_file="$2" 188 local log_file="$2"
189 local timeout_val="$3" 189 local timeout_val="$3"
190 190
191 # Fully detach stdio from the invoking shell. In daemon mode this
192 # process outlives vrunner.sh, and if the CLI that invoked us was
193 # wrapped by something that pipes stdout/stderr (e.g. a test harness
194 # using subprocess.run(capture_output=True)), any inherited fd here
195 # would block the parent's read/communicate() call until QEMU exits.
196 # Redirect fd 0 from /dev/null and fd 1/fd 2 to the log file.
191 if [ -n "$timeout_val" ]; then 197 if [ -n "$timeout_val" ]; then
192 timeout $timeout_val $HV_CMD $HV_OPTS -append "$kernel_append" > "$log_file" 2>&1 & 198 timeout $timeout_val $HV_CMD $HV_OPTS -append "$kernel_append" </dev/null > "$log_file" 2>&1 &
193 else 199 else
194 $HV_CMD $HV_OPTS -append "$kernel_append" > "$log_file" 2>&1 & 200 $HV_CMD $HV_OPTS -append "$kernel_append" </dev/null > "$log_file" 2>&1 &
195 fi 201 fi
196 HV_VM_PID=$! 202 HV_VM_PID=$!
197} 203}
diff --git a/recipes-containers/vcontainer/files/vrunner-backend-xen.sh b/recipes-containers/vcontainer/files/vrunner-backend-xen.sh
index 55e87bd1..f490f91c 100644
--- a/recipes-containers/vcontainer/files/vrunner-backend-xen.sh
+++ b/recipes-containers/vcontainer/files/vrunner-backend-xen.sh
@@ -397,13 +397,23 @@ hv_start_vm_background() {
397 # Monitor process: stays alive while domain exists. 397 # Monitor process: stays alive while domain exists.
398 # vcontainer-common.sh checks /proc/$pid → alive means daemon running. 398 # vcontainer-common.sh checks /proc/$pid → alive means daemon running.
399 # When domain dies (xl destroy, guest reboot), monitor exits. 399 # When domain dies (xl destroy, guest reboot), monitor exits.
400 #
401 # Detach the monitor's stdio from the invoking shell: in daemon
402 # mode this process outlives vrunner.sh, and any inherited fd
403 # would keep a pipe-wrapped caller (e.g. subprocess.run with
404 # capture_output=True) blocked in communicate() until the domain
405 # exits. Redirect fd 0/1/2 and disown.
400 local _domname="$HV_DOMNAME" 406 local _domname="$HV_DOMNAME"
401 (while xl list "$_domname" >/dev/null 2>&1; do sleep 10; done) & 407 (while xl list "$_domname" >/dev/null 2>&1; do sleep 10; done) \
408 </dev/null >/dev/null 2>&1 &
402 HV_VM_PID=$! 409 HV_VM_PID=$!
410 disown $! 2>/dev/null || true
403 else 411 else
404 # Ephemeral mode: capture guest console (hvc0) to log file 412 # Ephemeral mode: capture guest console (hvc0) to log file
405 # so the monitoring loop in vrunner.sh can see output markers 413 # so the monitoring loop in vrunner.sh can see output markers.
406 stdbuf -oL xl console "$HV_DOMNAME" >> "$log_file" 2>&1 & 414 # Detach stdin so the background reader doesn't hold the caller's
415 # fd 0 open.
416 stdbuf -oL xl console "$HV_DOMNAME" </dev/null >> "$log_file" 2>&1 &
407 _XEN_CONSOLE_PID=$! 417 _XEN_CONSOLE_PID=$!
408 log "DEBUG" "Console capture started (PID: $_XEN_CONSOLE_PID)" 418 log "DEBUG" "Console capture started (PID: $_XEN_CONSOLE_PID)"
409 fi 419 fi
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
diff --git a/recipes-containers/vcontainer/vcontainer-initramfs-create.inc b/recipes-containers/vcontainer/vcontainer-initramfs-create.inc
index 3e22bdb1..68011abc 100644
--- a/recipes-containers/vcontainer/vcontainer-initramfs-create.inc
+++ b/recipes-containers/vcontainer/vcontainer-initramfs-create.inc
@@ -44,12 +44,6 @@ python () {
44 d.setVarFlag('do_compile', 'nostamp', '1') 44 d.setVarFlag('do_compile', 'nostamp', '1')
45 d.setVarFlag('do_deploy', 'nostamp', '1') 45 d.setVarFlag('do_deploy', 'nostamp', '1')
46 46
47 # Conditionally set mcdepends when our multiconfig is configured
48 # (avoids parse errors when BBMULTICONFIG is not set, e.g. yocto-check-layer)
49 mc = d.getVar('VCONTAINER_MULTICONFIG')
50 bbmulticonfig = (d.getVar('BBMULTICONFIG') or "").split()
51 if mc in bbmulticonfig:
52 d.setVarFlag('do_compile', 'mcdepends', 'mc:%s::virtual/kernel:do_deploy' % mc)
53} 47}
54 48
55# Only populate native sysroot, skip target sysroot to avoid libgcc conflicts 49# Only populate native sysroot, skip target sysroot to avoid libgcc conflicts
@@ -58,12 +52,17 @@ INHIBIT_DEFAULT_DEPS = "1"
58# Dependencies: 52# Dependencies:
59# 1. The tiny initramfs image (produces cpio.gz) 53# 1. The tiny initramfs image (produces cpio.gz)
60# 2. The multiconfig rootfs image (produces squashfs) 54# 2. The multiconfig rootfs image (produces squashfs)
61# 3. The kernel from main build 55# 3. The kernel (deployed transitively via image.bbclass do_build)
62# 56#
63# Both initramfs and rootfs images are in the same multiconfig 57# Both image recipes inherit core-image/image.bbclass which has:
64do_compile[depends] = "${VCONTAINER_RUNTIME}-tiny-initramfs-image:do_image_complete" 58# do_build[depends] += "virtual/kernel:do_deploy"
65do_compile[depends] += "${VCONTAINER_RUNTIME}-rootfs-image:do_image_complete" 59# By depending on do_build (not do_image_complete), we ensure the
66# mcdepends set conditionally in anonymous python below 60# kernel is deployed to MC_DEPLOY before do_compile copies it.
61# Using do_image_complete cut the chain short — it runs before
62# do_build, so virtual/kernel:do_deploy was not guaranteed to have
63# run, causing missing kernel on sstate-accelerated builds.
64do_compile[depends] = "${VCONTAINER_RUNTIME}-tiny-initramfs-image:do_build"
65do_compile[depends] += "${VCONTAINER_RUNTIME}-rootfs-image:do_build"
67 66
68S = "${UNPACKDIR}" 67S = "${UNPACKDIR}"
69B = "${WORKDIR}/build" 68B = "${WORKDIR}/build"
@@ -141,14 +140,18 @@ do_compile() {
141 # ========================================================================= 140 # =========================================================================
142 # PART 3: COPY KERNEL 141 # PART 3: COPY KERNEL
143 # ========================================================================= 142 # =========================================================================
143 # Use the multiconfig's deploy directory (same as initramfs/rootfs),
144 # not DEPLOY_DIR_IMAGE which may point to the main config's deploy
145 # directory. The kernel is built as a dependency of the rootfs image
146 # within the same multiconfig.
144 bbnote "Copying kernel image..." 147 bbnote "Copying kernel image..."
145 KERNEL_FILE="${DEPLOY_DIR_IMAGE}/${KERNEL_IMAGETYPE_INITRAMFS}" 148 KERNEL_FILE="${MC_DEPLOY}/${KERNEL_IMAGETYPE_INITRAMFS}"
146 if [ -f "${KERNEL_FILE}" ]; then 149 if [ -f "${KERNEL_FILE}" ]; then
147 cp "${KERNEL_FILE}" ${B}/kernel 150 cp "${KERNEL_FILE}" ${B}/kernel
148 KERNEL_SIZE=$(stat -c%s ${B}/kernel) 151 KERNEL_SIZE=$(stat -c%s ${B}/kernel)
149 bbnote "Kernel copied: ${KERNEL_SIZE} bytes ($(expr ${KERNEL_SIZE} / 1024 / 1024)MB)" 152 bbnote "Kernel copied: ${KERNEL_SIZE} bytes ($(expr ${KERNEL_SIZE} / 1024 / 1024)MB)"
150 else 153 else
151 bbwarn "Kernel not found at ${KERNEL_FILE}" 154 bbwarn "Kernel not found at ${KERNEL_FILE} — check that the vruntime multiconfig kernel is built"
152 fi 155 fi
153} 156}
154 157
diff --git a/recipes-containers/vcontainer/vcontainer-tarball.bb b/recipes-containers/vcontainer/vcontainer-tarball.bb
index ee3c1147..f943cfd0 100644
--- a/recipes-containers/vcontainer/vcontainer-tarball.bb
+++ b/recipes-containers/vcontainer/vcontainer-tarball.bb
@@ -50,6 +50,15 @@ TOOLCHAIN_TARGET_TASK = ""
50TARGET_ARCH = "none" 50TARGET_ARCH = "none"
51TARGET_OS = "none" 51TARGET_OS = "none"
52 52
53# Must use ${SDK_ARCH}-${SDKPKGSUFFIX} (e.g. x86_64-nativesdk) to match
54# the pattern explicitly listed in SSTATE_ARCHS, which SPDX's find_jsonld()
55# searches when locating the static recipe SPDX document generated by
56# do_create_recipe_spdx. Using ${SDK_ARCH}_${SDK_OS} (x86_64_linux) is NOT
57# in SSTATE_ARCHS and causes do_create_spdx to fatal with "Could not find a
58# static SPDX document named static-vcontainer-tarball".
59# See: meta/classes-global/sstate.bbclass SSTATE_ARCHS definition and
60# buildtools-tarball.bb for the reference pattern.
61
53# Host tools to include via SDK 62# Host tools to include via SDK
54# Note: nativesdk-qemu-vcontainer is a minimal QEMU without OpenGL/virgl 63# Note: nativesdk-qemu-vcontainer is a minimal QEMU without OpenGL/virgl
55# to avoid mesa -> llvm -> clang build dependency chain 64# to avoid mesa -> llvm -> clang build dependency chain
@@ -66,7 +75,7 @@ SDK_TITLE = "vcontainer tools (vdkr/vpdmn)"
66 75
67# SDK configuration (same pattern as buildtools-tarball) 76# SDK configuration (same pattern as buildtools-tarball)
68MULTIMACH_TARGET_SYS = "${SDK_ARCH}-nativesdk${SDK_VENDOR}-${SDK_OS}" 77MULTIMACH_TARGET_SYS = "${SDK_ARCH}-nativesdk${SDK_VENDOR}-${SDK_OS}"
69PACKAGE_ARCH = "${SDK_ARCH}_${SDK_OS}" 78PACKAGE_ARCH = "${SDK_ARCH}-${SDKPKGSUFFIX}"
70PACKAGE_ARCHS = "" 79PACKAGE_ARCHS = ""
71SDK_PACKAGE_ARCHS += "vcontainer-dummy-${SDKPKGSUFFIX}" 80SDK_PACKAGE_ARCHS += "vcontainer-dummy-${SDKPKGSUFFIX}"
72 81
@@ -131,6 +140,25 @@ VCONTAINER_ARCHITECTURES ?= "x86_64 aarch64"
131 140
132# Conditionally set mcdepends based on available multiconfigs 141# Conditionally set mcdepends based on available multiconfigs
133# (avoids parse errors when BBMULTICONFIG is not set, e.g. yocto-check-layer) 142# (avoids parse errors when BBMULTICONFIG is not set, e.g. yocto-check-layer)
143#
144# Two layers of dependency per arch:
145#
146# 1. initramfs-create:do_deploy
147# Task ordering — guarantees the rootfs.img / kernel / initramfs are
148# staged under tmp-<mc>/deploy/images/<machine>/<tool>/<arch>/ before
149# do_populate_sdk reads them.
150#
151# 2. rootfs-image:do_image_complete
152# Defence-in-depth for sstate consistency. Without this, a rootfs
153# content change (e.g. adding netavark, switching iptables -> nftables)
154# would only invalidate the tarball's sstate hash if it propagates
155# cleanly through rootfs-image:do_build -> initramfs-create:do_compile
156# -> initramfs-create:do_deploy -> mcdepends. Any break in that chain
157# (the DEPLOY_DIR-input sstate pattern is one known way to get a stale
158# hit) re-introduces the netavark-stale-tarball failure mode. Listing
159# the rootfs-image task directly puts its hash in our chain regardless
160# of intermediate propagation, and costs nothing if the chain was
161# already healthy.
134python () { 162python () {
135 bbmulticonfig = (d.getVar('BBMULTICONFIG') or "").split() 163 bbmulticonfig = (d.getVar('BBMULTICONFIG') or "").split()
136 mcdeps = [] 164 mcdeps = []
@@ -138,6 +166,8 @@ python () {
138 if mc in bbmulticonfig: 166 if mc in bbmulticonfig:
139 mcdeps.append('mc::%s:vdkr-initramfs-create:do_deploy' % mc) 167 mcdeps.append('mc::%s:vdkr-initramfs-create:do_deploy' % mc)
140 mcdeps.append('mc::%s:vpdmn-initramfs-create:do_deploy' % mc) 168 mcdeps.append('mc::%s:vpdmn-initramfs-create:do_deploy' % mc)
169 mcdeps.append('mc::%s:vdkr-rootfs-image:do_image_complete' % mc)
170 mcdeps.append('mc::%s:vpdmn-rootfs-image:do_image_complete' % mc)
141 if mcdeps: 171 if mcdeps:
142 d.setVarFlag('do_populate_sdk', 'mcdepends', ' '.join(mcdeps)) 172 d.setVarFlag('do_populate_sdk', 'mcdepends', ' '.join(mcdeps))
143 173
@@ -307,8 +337,9 @@ Quick Start:
307Architectures included: ${ARCHITECTURES} 337Architectures included: ${ARCHITECTURES}
308 338
309Contents: 339Contents:
310 init-env.sh - Environment setup script 340 init-env.sh - Environment setup script (interactive bash)
311 vdkr, vdkr-<arch> - Docker CLI wrapper 341 environment-setup-ci - CI environment (for yocto-autobuilder-helper)
342 vdkr, vdkr-<arch> - Docker CLI wrapper
312 vpdmn, vpdmn-<arch> - Podman CLI wrapper 343 vpdmn, vpdmn-<arch> - Podman CLI wrapper
313 vrunner.sh - Shared QEMU runner 344 vrunner.sh - Shared QEMU runner
314 vcontainer-common.sh - Shared CLI code 345 vcontainer-common.sh - Shared CLI code
@@ -399,6 +430,32 @@ ENVEOF
399 # Create init-env.sh symlink for convenience 430 # Create init-env.sh symlink for convenience
400 ln -sf environment-setup-${REAL_MULTIMACH_TARGET_SYS} ${SDK_OUTPUT}/${SDKPATH}/init-env.sh 431 ln -sf environment-setup-${REAL_MULTIMACH_TARGET_SYS} ${SDK_OUTPUT}/${SDKPATH}/init-env.sh
401 432
433 # -----------------------------------------------------------------------
434 # CI/AutoBuilder environment script
435 # -----------------------------------------------------------------------
436 # yocto-autobuilder-helper's enable_tools_tarball() parses environment
437 # scripts line-by-line in Python. It only honours lines starting with
438 # "export " at column 0, only substitutes $PATH, and treats "unset " at
439 # column 0 as a removal. It does NOT evaluate shell expressions like
440 # $(...) or variable references like $FOO.
441 #
442 # Rather than adding conditional logic to the interactive bash script,
443 # generate a separate flat file with baked-in absolute paths that the
444 # AB parser can consume directly. SDK relocation rewrites these paths
445 # at install time just like the primary environment-setup-* script.
446 ci_script=${SDK_OUTPUT}/${SDKPATH}/environment-setup-ci
447 cat > $ci_script <<CISCRIPT
448# vcontainer CI environment — for yocto-autobuilder-helper
449# Flat export lines with absolute paths; no shell logic.
450# SDK relocation rewrites these paths at install time.
451export VCONTAINER_DIR="${SDKPATH}"
452export OECORE_NATIVE_SYSROOT="${SDKPATHNATIVE}"
453export PATH="${SDKPATH}:${SDKPATHNATIVE}/usr/bin:/usr/bin:/bin:\$PATH"
454# Clean up - unset to avoid confusing other Yocto tools' >> $script
455unset OECORE_NATIVE_SYSROOT
456CISCRIPT
457 chmod 755 $ci_script
458
402 # Create version file 459 # Create version file
403 echo "vcontainer SDK version: ${PV}" > ${SDK_OUTPUT}/${SDKPATH}/version.txt 460 echo "vcontainer SDK version: ${PV}" > ${SDK_OUTPUT}/${SDKPATH}/version.txt
404 echo "Built: $(date)" >> ${SDK_OUTPUT}/${SDKPATH}/version.txt 461 echo "Built: $(date)" >> ${SDK_OUTPUT}/${SDKPATH}/version.txt
diff --git a/recipes-containers/vcontainer/vdkr-rootfs-image.bb b/recipes-containers/vcontainer/vdkr-rootfs-image.bb
index e2921ec7..fb5a3718 100644
--- a/recipes-containers/vcontainer/vdkr-rootfs-image.bb
+++ b/recipes-containers/vcontainer/vdkr-rootfs-image.bb
@@ -47,6 +47,8 @@ inherit core-image
47# We need Docker and container tools 47# We need Docker and container tools
48# Note: runc is explicitly listed because vruntime distro sets 48# Note: runc is explicitly listed because vruntime distro sets
49# VIRTUAL-RUNTIME_container_runtime="" to avoid runc/crun conflicts. 49# VIRTUAL-RUNTIME_container_runtime="" to avoid runc/crun conflicts.
50# Note: skopeo is required inside the guest for batch import
51# (skopeo copy oci:... containers-storage:...).
50IMAGE_INSTALL = " \ 52IMAGE_INSTALL = " \
51 packagegroup-core-boot \ 53 packagegroup-core-boot \
52 docker-moby \ 54 docker-moby \
@@ -58,6 +60,7 @@ IMAGE_INSTALL = " \
58 iptables \ 60 iptables \
59 util-linux \ 61 util-linux \
60 kernel-modules \ 62 kernel-modules \
63 ca-certificates \
61" 64"
62 65
63# No extra features needed 66# No extra features needed
diff --git a/recipes-containers/vcontainer/vpdmn-rootfs-image.bb b/recipes-containers/vcontainer/vpdmn-rootfs-image.bb
index 8808e6b2..909e1298 100644
--- a/recipes-containers/vcontainer/vpdmn-rootfs-image.bb
+++ b/recipes-containers/vcontainer/vpdmn-rootfs-image.bb
@@ -42,6 +42,8 @@ inherit core-image
42# Podman is daemonless - no containerd required! 42# Podman is daemonless - no containerd required!
43# Note: crun is explicitly listed because vruntime distro sets 43# Note: crun is explicitly listed because vruntime distro sets
44# VIRTUAL-RUNTIME_container_runtime="" to avoid runc/crun conflicts. 44# VIRTUAL-RUNTIME_container_runtime="" to avoid runc/crun conflicts.
45# Note: skopeo is required inside the guest for batch import
46# (skopeo copy oci:... containers-storage:...).
45IMAGE_INSTALL = " \ 47IMAGE_INSTALL = " \
46 packagegroup-core-boot \ 48 packagegroup-core-boot \
47 podman \ 49 podman \
@@ -52,7 +54,7 @@ IMAGE_INSTALL = " \
52 aardvark-dns \ 54 aardvark-dns \
53 busybox \ 55 busybox \
54 iproute2 \ 56 iproute2 \
55 iptables \ 57 nftables \
56 util-linux \ 58 util-linux \
57 ca-certificates \ 59 ca-certificates \
58" 60"
@@ -121,12 +123,22 @@ EOF
121 123
122 # Create containers.conf for podman engine settings 124 # Create containers.conf for podman engine settings
123 cat > ${IMAGE_ROOTFS}/etc/containers/containers.conf << 'EOF' 125 cat > ${IMAGE_ROOTFS}/etc/containers/containers.conf << 'EOF'
124[engine]
125# Location of helper binaries (netavark, aardvark-dns)
126helper_binaries_dir = ["/usr/libexec/podman"]
127
128[network] 126[network]
129# Use netavark as the network backend
130network_backend = "netavark" 127network_backend = "netavark"
131EOF 128EOF
129
130 # Prevent libnss_systemd segfaults — systemd is not running in the
131 # vruntime VM (busybox init), but libnss_systemd.so is installed as
132 # a dependency. Override nsswitch.conf to use only files/compat.
133 cat > ${IMAGE_ROOTFS}/etc/nsswitch.conf << 'EOF'
134passwd: files
135group: files
136shadow: files
137hosts: files dns
138networks: files
139protocols: files
140services: files
141ethers: files
142rpc: files
143EOF
132} 144}