diff --git a/quickemu b/quickemu index 4b01dad..e13da01 100755 --- a/quickemu +++ b/quickemu @@ -318,6 +318,76 @@ function efi_vars() { } function configure_cpu() { + local add_cpu_flag + add_cpu_flag() { + local flag="${1}" + # Skip empty flags + [[ -z "${flag}" ]] && return + # Defensive check: ensure CPU is initialized before appending + if [[ -z "${CPU}" ]]; then + echo "ERROR: add_cpu_flag called before CPU variable initialization" + exit 1 + fi + # Auto-correct flag format by prepending comma if missing + if [[ "${flag}" != ,* ]]; then + flag=",${flag}" + fi + local needle="${flag#,}" + # Validate flag is not malformed + if [[ "${needle}" =~ ^[+-]{2} ]] || [[ "${needle}" == "+" ]] || [[ "${needle}" == "-" ]]; then + echo "ERROR: Malformed CPU flag: '${needle}' (empty flag name or multiple prefix characters)" + exit 1 + fi + # Check for exact duplicate + if [[ ",${CPU}," == *",${needle},"* ]]; then + return + fi + # Check for conflicting flag with opposite prefix (+/-) or unprefixed variant + # Strip both prefix (+/-) and value assignment (=...) to get the base flag name + local base_flag="${needle#+}" + base_flag="${base_flag#-}" + base_flag="${base_flag%%=*}" + if [[ "${needle}" == +* ]]; then + # Check if -flag or unprefixed flag exists + if [[ ",${CPU}," == *",-${base_flag}"* ]]; then + echo "WARNING: Conflicting CPU flags detected: -${base_flag} already set, ignoring +${base_flag}" + return + fi + if [[ ",${CPU}," == *",${base_flag}"[,=]* ]]; then + echo "WARNING: Conflicting CPU flags detected: ${base_flag} already set, ignoring +${base_flag}" + return + fi + elif [[ "${needle}" == -* ]]; then + # Check if +flag or unprefixed flag exists + if [[ ",${CPU}," == *",+${base_flag}"* ]]; then + echo "WARNING: Conflicting CPU flags detected: +${base_flag} already set, ignoring -${base_flag}" + return + fi + if [[ ",${CPU}," == *",${base_flag}"[,=]* ]]; then + echo "WARNING: Conflicting CPU flags detected: ${base_flag} already set, ignoring -${base_flag}" + return + fi + else + # Unprefixed flag - check if +flag or -flag exists + # Also check for conflicting value assignments (e.g., flag=on vs flag=off) + local needle_base="${needle%%=*}" + if [[ ",${CPU}," == *",+${needle_base}"* ]]; then + echo "WARNING: Conflicting CPU flags detected: +${needle_base} already set, ignoring ${needle}" + return + fi + if [[ ",${CPU}," == *",-${needle_base}"* ]]; then + echo "WARNING: Conflicting CPU flags detected: -${needle_base} already set, ignoring ${needle}" + return + fi + # Check for value assignment conflicts (e.g., flag=on already exists, trying to add flag=off) + if [[ "${needle}" == *=* ]] && [[ ",${CPU}," == *",${needle_base}="* ]]; then + echo "WARNING: Conflicting CPU flags detected: ${needle_base}=... already set, ignoring ${needle}" + return + fi + fi + CPU+="${flag}" + } + HOST_CPU_CORES=$(get_nproc) HOST_CPU_MODEL=$(get_cpu_info '^Model name:') HOST_CPU_SOCKETS=$(get_cpu_info 'Socket') @@ -429,11 +499,13 @@ function configure_cpu() { if [ "${QEMU_ACCEL}" == "tcg" ]; then # TCG can emulate these features if [ "${HOST_CPU_VENDOR}" != "GenuineIntel" ] && [ -z "${HYPERVISOR}" ]; then - CPU+=",+avx2,+sse4.2" + add_cpu_flag ",+avx2" + add_cpu_flag ",+sse4.2" fi elif check_cpu_flag sse4_2 && check_cpu_flag avx2; then if [ "${HOST_CPU_VENDOR}" != "GenuineIntel" ] && [ -z "${HYPERVISOR}" ]; then - CPU+=",+avx2,+sse4.2" + add_cpu_flag ",+avx2" + add_cpu_flag ",+sse4.2" fi else echo "ERROR! macOS ${macos_release} requires a CPU with SSE 4.2 and AVX2 support." @@ -445,11 +517,11 @@ function configure_cpu() { if [ "${QEMU_ACCEL}" == "tcg" ]; then # TCG can emulate this feature if [ "${HOST_CPU_VENDOR}" != "GenuineIntel" ] && [ -z "${HYPERVISOR}" ]; then - CPU+=",+sse4.2" + add_cpu_flag ",+sse4.2" fi elif check_cpu_flag sse4_2; then if [ "${HOST_CPU_VENDOR}" != "GenuineIntel" ] && [ -z "${HYPERVISOR}" ]; then - CPU+=",+sse4.2" + add_cpu_flag ",+sse4.2" fi else echo "ERROR! macOS ${macos_release} requires a CPU with SSE 4.2 support." @@ -460,11 +532,11 @@ function configure_cpu() { if [ "${QEMU_ACCEL}" == "tcg" ]; then # TCG can emulate this feature if [ "${HOST_CPU_VENDOR}" != "GenuineIntel" ] && [ -z "${HYPERVISOR}" ]; then - CPU+=",+sse4.1" + add_cpu_flag ",+sse4.1" fi elif check_cpu_flag sse4_1; then if [ "${HOST_CPU_VENDOR}" != "GenuineIntel" ] && [ -z "${HYPERVISOR}" ]; then - CPU+=",+sse4.1" + add_cpu_flag ",+sse4.1" fi else echo "ERROR! macOS ${macos_release} requires a CPU with SSE 4.1 support." @@ -478,7 +550,9 @@ function configure_cpu() { mca mce mmx movbe mpx msr mtrr nx pae pat pcid pge pse popcnt pse36 \ rdrand rdtscp sep smep syscall tsc tsc_adjust vaes vbmi2 vmx vpclmulqdq \ x2apic xgetbv1 xsave xsaveopt; do - CPU+=$(configure_cpu_flag "${FLAG}") + local cpu_flag="$(configure_cpu_flag "${FLAG}")" + # Only call add_cpu_flag if configure_cpu_flag returned a non-empty result + [[ -n "${cpu_flag}" ]] && add_cpu_flag "${cpu_flag}" done fi @@ -512,7 +586,7 @@ function configure_cpu() { esac if [ "${HOST_CPU_VENDOR}" == "AuthenticAMD" ] && [ "${guest_os}" != "macos" ]; then - CPU+=",topoext" + add_cpu_flag ",+topoext" fi if [ -z "${cpu_cores}" ]; then @@ -702,7 +776,7 @@ function configure_bios() { # https://bugzilla.redhat.com/show_bug.cgi?id=1929357#c5 # TODO: Check if macOS should use 'edk2-i386-vars.fd' - if [ -n "${EFI_CODE}" ] || [ ! -e "${EFI_CODE}" ]; then + if [ -z "${EFI_CODE}" ] || [ ! -e "${EFI_CODE}" ]; then # Architecture-specific firmware paths if [ "${ARCH_VM}" == "aarch64" ]; then # ARM64/AArch64 firmware (AAVMF) @@ -1005,9 +1079,17 @@ function check_display_gl_support() { "${QEMU}" -M none -display "dbus,gl=${gl_mode}" >"$tmpfile" 2>&1 & local qemu_pid=$! - sleep 0.1 - kill -9 $qemu_pid 2>/dev/null - wait $qemu_pid 2>/dev/null + sleep 0.2 + # Try graceful termination first + if kill -0 "$qemu_pid" 2>/dev/null; then + kill -TERM "$qemu_pid" 2>/dev/null + sleep 0.15 + # Force kill if still running + if kill -0 "$qemu_pid" 2>/dev/null; then + kill -KILL "$qemu_pid" 2>/dev/null + fi + fi + wait "$qemu_pid" 2>/dev/null local test_output=$(cat "$tmpfile" 2>/dev/null) rm -f "$tmpfile" @@ -1131,7 +1213,7 @@ function configure_display() { # Try and coerce the display resolution for Linux guests only. # Check if the device supports xres/yres parameters - if [[ "${DISPLAY_DEVICE}" =~ ^(virtio-(vga|gpu-pci|gpu-gl-pci|vga-gl)|qxl|qxl-vga|bochs-display)$ ]] || [[ "${DISPLAY_DEVICE}" =~ ,virgl=on ]]; then + if [[ "${DISPLAY_DEVICE}" =~ ^(virtio-(vga|gpu-pci|vga-gl|gpu-gl-pci)|qxl|qxl-vga|bochs-display)$ ]] || [[ "${DISPLAY_DEVICE}" =~ ,virgl=on ]]; then VIDEO="${VIDEO},xres=${X_RES},yres=${Y_RES}" echo " @ (${X_RES} x ${Y_RES})" else @@ -1347,6 +1429,8 @@ function vm_boot() { USB_HOST_PASSTHROUGH_CONTROLLER="qemu-xhci" VGA="" VIDEO="" + # Set QEMU_VER_LONG for backward compatibility with user scripts that may reference it + QEMU_VER_LONG="${QEMU_VER_RAW}" KERNEL_NAME="$(uname -s)" KERNEL_NODE="$(uname -n | cut -d'.' -f 1)" @@ -2179,6 +2263,8 @@ if [ -z "${QEMU_VER_MAJOR}" ]; then fi QEMU_VER_MINOR=${QEMU_VER_MINOR:-0} QEMU_VER_SHORT=$((QEMU_VER_MAJOR * 10 + QEMU_VER_MINOR)) +# QEMU_VER_LONG maintained for backward compatibility +QEMU_VER_LONG="${QEMU_VER_RAW}" if [ "${QEMU_VER_SHORT}" -lt 60 ]; then echo "ERROR! QEMU 6.0.0 or newer is required, detected ${QEMU_VER_RAW}." exit 1 @@ -2376,6 +2462,8 @@ if [ -n "${VM}" ] && [ -e "${VM}" ]; then fi QEMU_VER_MINOR=${QEMU_VER_MINOR:-0} QEMU_VER_SHORT=$((QEMU_VER_MAJOR * 10 + QEMU_VER_MINOR)) + # QEMU_VER_LONG maintained for backward compatibility + QEMU_VER_LONG="${QEMU_VER_RAW}" if [ "${QEMU_VER_SHORT}" -lt 60 ]; then echo "ERROR! QEMU 6.0.0 or newer is required, detected ${QEMU_VER_RAW}." exit 1 diff --git a/quickget b/quickget index cca4f10..a0611b5 100755 --- a/quickget +++ b/quickget @@ -1205,21 +1205,80 @@ function check_hash() { iso="${VM_PATH}/${1}" fi hash="${2}" + # Validate no leading/trailing whitespace in hash parameter + if [[ "${hash}" =~ ^[[:space:]] ]] || [[ "${hash}" =~ [[:space:]]$ ]]; then + echo "ERROR! Invalid hash format for ${iso}: leading or trailing whitespace detected." + echo " Hash parameter: '${hash}'" + exit 1 + fi if [[ "${hash}" == *":"* ]]; then hash_prefix="${hash%%:*}" hash="${hash#*:}" + # Normalize algorithm prefix to lowercase for case-insensitive matching (Bash 3.2 compatible) + hash_prefix="$(echo "${hash_prefix}" | tr '[:upper:]' '[:lower:]')" + # Validate that prefix is not empty + if [ -z "${hash_prefix}" ]; then + echo "ERROR! Invalid hash format for ${iso}: empty algorithm prefix (found '${2}')." + echo " Expected format: 'algorithm:hash' (e.g., 'sha256:abc123...')" + exit 1 + fi + # Validate no internal whitespace in algorithm prefix + if [[ "${hash_prefix}" =~ [[:space:]] ]]; then + echo "ERROR! Invalid hash format for ${iso}: algorithm prefix contains whitespace." + echo " Hash parameter: '${2}'" + exit 1 + fi + # Validate that the remaining hash does not contain additional colons + if [[ "${hash}" == *":"* ]]; then + echo "ERROR! Invalid hash format for ${iso}: multiple colons found." + exit 1 + fi + # Validate hash is not empty after removing prefix + if [ -z "${hash}" ]; then + echo "ERROR! Invalid hash format for ${iso}: empty hash value after prefix '${hash_prefix}:'." + exit 1 + fi fi if [ -z "${hash}" ]; then echo "WARNING! Empty hash value, not checking ${iso} hash." return fi + # Validate hash contains only hexadecimal characters (no internal whitespace or invalid chars) + if [[ ! "${hash}" =~ ^[0-9a-fA-F]+$ ]]; then + echo "ERROR! Invalid hash format for ${iso}: hash contains non-hexadecimal characters." + echo " Hash value: '${hash}'" + exit 1 + fi + # Validate hash length matches algorithm + local hash_len=${#hash} if [ -n "${hash_prefix}" ]; then case "${hash_prefix}" in - md5) hash_algo=md5sum;; - sha1) hash_algo=sha1sum;; - sha256) hash_algo=sha256sum;; - sha512) hash_algo=sha512sum;; - b2sum|blake2|blake2b) hash_algo=b2sum;; + md5) hash_algo=md5sum + if [ ${hash_len} -ne 32 ]; then + echo "ERROR! MD5 hash must be 32 characters, got ${hash_len}: ${hash}" + exit 1 + fi;; + sha1) hash_algo=sha1sum + if [ ${hash_len} -ne 40 ]; then + echo "ERROR! SHA1 hash must be 40 characters, got ${hash_len}: ${hash}" + exit 1 + fi;; + sha256) hash_algo=sha256sum + if [ ${hash_len} -ne 64 ]; then + echo "ERROR! SHA256 hash must be 64 characters, got ${hash_len}: ${hash}" + exit 1 + fi;; + sha512) hash_algo=sha512sum + if [ ${hash_len} -ne 128 ]; then + echo "ERROR! SHA512 hash must be 128 characters, got ${hash_len}: ${hash}" + exit 1 + fi;; + b2sum|blake2|blake2b) hash_algo=b2sum + # BLAKE2b supports variable lengths from 256-512 bits (64-128 hex chars) + if [ ${hash_len} -lt 64 ] || [ ${hash_len} -gt 128 ] || [ $((hash_len % 2)) -ne 0 ]; then + echo "ERROR! BLAKE2b hash must be 64-128 characters (even length), got ${hash_len}: ${hash}" + exit 1 + fi;; *) echo "WARNING! Unknown hash algorithm '${hash_prefix}', not checking ${iso} hash." return;; esac @@ -1239,7 +1298,7 @@ function check_hash() { case "${hash_algo}" in md5sum|sha1sum) - echo "WARNING! ${hash_algo} is a weak hash algorithm; prefer sha256, sha512, or b2sum." + echo "WARNING! ${hash_algo} is a weak hash algorithm. Prefer sha256, sha512, or b2sum for better security." ;; esac @@ -1255,7 +1314,7 @@ function check_hash() { fi echo -n "Checking ${iso} with ${hash_algo}... " - if ! echo "${hash} ${iso}" | ${hash_algo} --check --status; then + if ! printf '%s %s\n' "${hash}" "${iso}" | ${hash_algo} --check --status; then echo "ERROR!" echo "${iso} doesn't match ${hash}. Try running 'quickget' again." exit 1 @@ -1493,7 +1552,8 @@ function make_vm_config() { elif [[ "${IMAGE_FILE}" =~ (riscv64) ]]; then ARCH_HINT="riscv64" elif [[ "${HOST_ARCH}" == "aarch64" && "${OS}" != "macos" && "${OS}" != windows* ]]; then - # Use host architecture for Linux guests when available + # Use host architecture for Linux guests that support aarch64 + # Add more distributions to this list as they add aarch64 support case "${OS}" in archboot|alpine|debian|fedora|ubuntu*) ARCH_HINT="aarch64";; esac @@ -1692,18 +1752,47 @@ function get_archboot() { local LATEST_URL="${URL}/${HOST_ARCH}/latest/iso" local ISO_PATTERN="${EDITION}" - # Adjust pattern: "default" edition doesn't have a suffix in filename + # "local" edition is intended for locally available ISOs, not remote downloads + if [ "${EDITION}" == "local" ]; then + echo "ERROR! Archboot edition 'local' refers to a local ISO and cannot be downloaded." + echo " Valid downloadable editions are: default, latest" + echo " For local ISOs, reference them in your .conf file:" + echo " fixed_iso=\"/path/to/archboot-local-${HOST_ARCH}.iso\"" + exit 1 + fi + + # Adjust pattern: match actual archboot filename format with word boundaries + # - x86_64 default: archboot-YYYY.MM.DD-HH.MM-X.XX.X-arch1-1-x86_64.iso + # - x86_64 latest: archboot-YYYY.MM.DD-HH.MM-X.XX.X-arch1-1-latest-x86_64.iso + # - aarch64 default: archboot-YYYY.MM.DD-HH.MM-X.XX.X-X-aarch64-ARCH-aarch64.iso + # - aarch64 latest: archboot-YYYY.MM.DD-HH.MM-X.XX.X-X-aarch64-ARCH-latest-aarch64.iso + # Use specific patterns to avoid substring matching between editions if [ "${EDITION}" == "default" ]; then - ISO_PATTERN="ARCH-${HOST_ARCH}.iso\"" + if [ "${HOST_ARCH}" == "x86_64" ]; then + # Match arch1-1-x86_64.iso but not arch1-1-latest-x86_64.iso or arch1-1-local-x86_64.iso + ISO_PATTERN="arch1-1-${HOST_ARCH}\.iso\"$" + else + # aarch64 uses uppercase ARCH in the pattern + # Match ARCH-aarch64.iso but not ARCH-latest-aarch64.iso or ARCH-local-aarch64.iso + ISO_PATTERN="ARCH-${HOST_ARCH}\.iso\"$" + fi + elif [ "${EDITION}" == "latest" ]; then + if [ "${HOST_ARCH}" == "x86_64" ]; then + ISO_PATTERN="arch1-1-latest-${HOST_ARCH}\.iso\"$" + else + # aarch64 uses uppercase ARCH in the pattern + ISO_PATTERN="ARCH-latest-${HOST_ARCH}\.iso\"$" + fi else - ISO_PATTERN="${EDITION}-${HOST_ARCH}.iso\"" + echo "ERROR! Invalid archboot edition '${EDITION}'. Only 'default' and 'latest' are supported." + exit 1 fi # Get the ISO filename from the iso directory - ISO=$(web_pipe "${LATEST_URL}/" | grep -o 'href="[^"]*\.iso"' | grep "${ISO_PATTERN}" | head -n 1 | cut -d'"' -f2 | sed 's|.*/||') + ISO=$(web_pipe "${LATEST_URL}/" | grep -o 'href="[^"]*\.iso"' | grep -E "${ISO_PATTERN}" | head -n 1 | cut -d'"' -f2 | sed 's|.*/||') if [ -z "${ISO}" ]; then - echo "ERROR! Could not find Archboot ${EDITION} ISO for ${HOST_ARCH}" + echo "ERROR! Could not find Archboot ${EDITION} ISO for ${HOST_ARCH} at ${LATEST_URL}/" exit 1 fi