From 8758d4b37310a44b3b09815752a07bbdddb60b8c Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:06:21 +0100 Subject: [PATCH] Name the project restriction that refuses an Incus launch A project with restricted=true blocks security.nesting and every security.syscalls.intercept.* key by default, and Incus rejects the whole launch over one of them. The retry loop then tried three more images with the same config and failed identically each time, so the output was four copies of an error about an image that was never the problem, ending in a hint to copy the image by hand. Report the refused key and project instead, with the project set calls that allow them, and skip the image retries. Exit 229 so the trace says what happened rather than "General error". --- .github/workflows/verify-split-files.yml | 5 +++ api/exitcodes.func | 3 +- incus/backend.func | 29 ++++++++++++++ tests/incus-restricted-project.sh | 50 ++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/incus-restricted-project.sh diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index cf1bada..9bf2398 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -46,6 +46,7 @@ on: - "tests/install-cleanup.sh" - "tests/console-hangup.sh" - "tests/container-removal-offer.sh" + - "tests/incus-restricted-project.sh" - "tests/incus-target.sh" - "tests/incus-storage-space.sh" - "tests/release-stamp-pending.sh" @@ -92,6 +93,7 @@ on: - "tests/install-cleanup.sh" - "tests/console-hangup.sh" - "tests/container-removal-offer.sh" + - "tests/incus-restricted-project.sh" - "tests/incus-target.sh" - "tests/incus-storage-space.sh" - "tests/release-stamp-pending.sh" @@ -190,6 +192,9 @@ jobs: - name: Container removal offer tests run: bash tests/container-removal-offer.sh + - name: Incus restricted project tests + run: bash tests/incus-restricted-project.sh + - name: Incus target tests run: bash tests/incus-target.sh diff --git a/api/exitcodes.func b/api/exitcodes.func index 075c24b..266ab80 100644 --- a/api/exitcodes.func +++ b/api/exitcodes.func @@ -187,6 +187,7 @@ explain_exit_code() { 226) echo "Proxmox: VM disk import or post-creation setup failed" ;; 227) echo "Proxmox: Container created but failed to start" ;; 228) echo "Proxmox: Cluster filesystem /etc/pve not available" ;; + 229) echo "Incus: Project restrictions forbid a config the container needs" ;; 231) echo "Proxmox: LXC stack too old for the template, or its upgrade failed" ;; # --- Tools & Addon Scripts (232-238) --- @@ -251,7 +252,7 @@ categorize_error() { 28 | 124 | 211) echo "timeout" ;; 214 | 217 | 219 | 224) echo "storage" ;; 100 | 101 | 102 | 127 | 160 | 161 | 162 | 255) echo "dependency" ;; - 126 | 152) echo "permission" ;; + 126 | 152 | 229) echo "permission" ;; 128 | 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;; 200 | 209 | 210 | 212 | 213 | 215 | 216 | 218 | 220 | 221 | 222 | 223 | 225 | 227 | 228 | 231) echo "proxmox" ;; 150 | 151 | 153 | 154) echo "service" ;; diff --git a/incus/backend.func b/incus/backend.func index f3f9d4a..c880d27 100644 --- a/incus/backend.func +++ b/incus/backend.func @@ -353,6 +353,22 @@ _incus_prepare_install_exports() { _HOST_LOGFILE="${BUILD_LOG:-$INCUS_BUILD_LOG}" } +# A restricted project refuses the whole launch over a single config key -- +# restricted.containers.nesting and restricted.containers.interception both +# default to block -- so retrying other images cannot help. Incus names the key +# and the project it refused; echo them as "|". +_incus_launch_refusal() { + local out="$1" rest="" key="" project="" q='"' + [[ "$out" == *"is forbidden"* && "$out" == *"for config $q"* ]] || return 1 + rest="${out##*for config $q}" + key="${rest%%$q*}" + if [[ "$rest" == *"of project $q"* ]]; then + project="${rest#*of project $q}" + project="${project%%$q*}" + fi + printf '%s|%s\n' "$key" "${project:-default}" +} + incus_create_lxc_container() { LOGFILE="${INCUS_BUILD_LOG}" @@ -451,7 +467,20 @@ incus_create_lxc_container() { echo "user: $(id -un) uid=$(id -u)" } >>"$LOGFILE" + local launch_mark + launch_mark="$(wc -l <"$LOGFILE" 2>/dev/null || echo 0)" if ! incus launch "${image_name}" "${CT_NAME}" "${launch_args[@]}" >>"$LOGFILE" 2>&1; then + local refusal="" + refusal="$(_incus_launch_refusal "$(tail -n +$((launch_mark + 1)) "$LOGFILE" 2>/dev/null)")" || refusal="" + if [[ -n "$refusal" ]]; then + local refused_project="${refusal#*|}" + msg_error "Project '${refused_project}' forbids ${refusal%|*}" + echo -e "${YW}Allow it on the host, then run the script again:${CL}" + echo -e " incus project set ${refused_project} restricted.containers.nesting allow" + echo -e " incus project set ${refused_project} restricted.containers.interception allow" + echo -e "${YW}Log: ${BL}${LOGFILE}${CL}" + exit 229 + fi # Retry alternate candidates once if primary pull failed local retry_img="" launched=0 tried=" ${image_name} " for retry_img in "images:${IMAGE_OS}/${IMAGE_VERSION}/cloud" \ diff --git a/tests/incus-restricted-project.sh b/tests/incus-restricted-project.sh new file mode 100644 index 0000000..1ab1434 --- /dev/null +++ b/tests/incus-restricted-project.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^_incus_launch_refusal[(][)] [{]/,/^}/' "$CORE_DIR/incus/backend.func")" + +passed=0 failed=0 +expect() { + if [[ "$2" == "$3" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + failed=$((failed + 1)) + fi +} + +# Verbatim from the telemetry record of 2026-11-02, a host whose default +# project carries restricted=true. +MKNOD='Launching debian-2 +Error: Failed instance creation: Failed checking if instance creation allowed: Invalid value "true" for config "security.syscalls.intercept.mknod" on container "debian-2" of project "default": Container syscall interception is forbidden' + +expect "the refused key and project are named" \ + "security.syscalls.intercept.mknod|default" "$(_incus_launch_refusal "$MKNOD")" + +NESTING='Error: Failed instance creation: Failed checking if instance creation allowed: Invalid value "true" for config "security.nesting" on container "ct-1" of project "lab": Container nesting is forbidden' +expect "nesting is caught in a non-default project" \ + "security.nesting|lab" "$(_incus_launch_refusal "$NESTING")" + +# The retry loop appends each attempt, so the helper reads the newest one. +expect "the last attempt wins" \ + "security.nesting|lab" "$(_incus_launch_refusal "$MKNOD +$NESTING")" + +# Without the project clause the key still has to come through. +NOPROJ='Error: Invalid value "true" for config "security.nesting": Container nesting is forbidden' +expect "a missing project clause falls back" \ + "security.nesting|default" "$(_incus_launch_refusal "$NOPROJ")" + +# An image problem must still reach the retry loop. +PULL='Error: Failed instance creation: Failed getting image: not found' +expect "an image failure is not a refusal" "" "$(_incus_launch_refusal "$PULL")" +expect " and it reports no refusal" 1 "$(_incus_launch_refusal "$PULL" >/dev/null 2>&1; echo $?)" + +# "is forbidden" alone is not enough; a config key has to be named. +expect "a forbidden message without a key is not a refusal" 1 \ + "$(_incus_launch_refusal 'Error: Container nesting is forbidden' >/dev/null 2>&1; echo $?)" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0))