diff --git a/.ls-lint.yml b/.ls-lint.yml index 71581b3c6ba..f2ed04c94cf 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -32,6 +32,8 @@ ignore: - hack/common.inc.sh - pkg/cidata/cidata.TEMPLATE.d - pkg/cidata/cidata.TEMPLATE.d/util/compare_version.sh +- pkg/cidata/cidata.TEMPLATE.d/util/escape_fstab.sh +- pkg/cidata/cidata.TEMPLATE.d/util/unescape_fstab.sh - pkg/cidata/wincidata.TEMPLATE.d - website # "ubuntu-24.04" does not follow the kebab-case diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot.Linux/04-persistent-data-volume.sh b/pkg/cidata/cidata.TEMPLATE.d/boot.Linux/04-persistent-data-volume.sh index 36f63689246..fb3bcf578b2 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/boot.Linux/04-persistent-data-volume.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot.Linux/04-persistent-data-volume.sh @@ -25,7 +25,7 @@ for DIR in ${DATADIRS}; do MNTDEV="$(echo "${LINE}" | awk '{print $1}')" # unmangle " \t\n\\#" # https://github.com/torvalds/linux/blob/v6.6/fs/proc_namespace.c#L89 - MNTPNT="$(echo "${LINE}" | awk '{print $2}' | sed -e 's/\\040/ /g; s/\\011/\t/g; s/\\012/\n/g; s/\\134/\\/g; s/\\043/#/g')" + MNTPNT="$(echo "${LINE}" | awk '{print $2}' | unescape_fstab.sh)" # Ignore if MNTPNT is neither DIR nor a parent directory of DIR. # It is not a parent if MNTPNT doesn't start with DIR, or the first # character after DIR isn't a slash. diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot.Linux/05-lima-mounts.sh b/pkg/cidata/cidata.TEMPLATE.d/boot.Linux/05-lima-mounts.sh index adc7d2f79ba..b6fee307d1b 100755 --- a/pkg/cidata/cidata.TEMPLATE.d/boot.Linux/05-lima-mounts.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot.Linux/05-lima-mounts.sh @@ -10,6 +10,21 @@ if ! [[ ${LIMA_CIDATA_VMTYPE} == "vz" && ${LIMA_CIDATA_MOUNTTYPE} == "virtiofs" exit 0 fi +# cloud-init's cc_mounts writes the mount point into /etc/fstab without octal-escaping it, so a +# space/tab in the path makes an unparsable line that mount(8) silently skips via nofail. +# cc_mounts already created the directory from the unescaped value, so just repair the fstab +# syntax (see util/escape_fstab.sh) and (re)mount. +# https://github.com/lima-vm/lima/issues/5136 +# https://github.com/abiosoft/colima/issues/1471 +if grep -q virtiofs /etc/fstab; then + escape_fstab.sh /etc/fstab.lima.tmp && + cat /etc/fstab.lima.tmp >/etc/fstab && rm -f /etc/fstab.lima.tmp + # Mount entries cc_mounts skipped due to the previously broken line (already-mounted ones + # are a no-op). On Oracle Linux the virtiofs module is installed later in + # 30-install-packages.sh (REMOUNT_VIRTIOFS=1 remounts then), so tolerate failure here. + mount -t virtiofs -a || true +fi + # Update fstab entries and unmount/remount the volumes with secontext options # when selinux is enabled in kernel if [ -d /sys/fs/selinux ]; then @@ -42,7 +57,9 @@ if [ -d /sys/fs/selinux ]; then fi sed -i -e "$line""s/comment=cloudconfig/comment=cloudconfig,context=\"$label\"/g" /etc/fstab if [[ ${MOUNT_OPTIONS} != *"$label"* ]]; then - MOUNT_POINT=$(awk -v line="$line" 'NR==line {print $2}' /etc/fstab) + # fstab stores the mount point octal-escaped (e.g. space = "\040"); decode + # it before passing the path to mount(8). + MOUNT_POINT=$(awk -v line="$line" 'NR==line {print $2}' /etc/fstab | unescape_fstab.sh) OPTIONS=$(awk -v line="$line" 'NR==line {print $4}' /etc/fstab) ######################################################### diff --git a/pkg/cidata/cidata.TEMPLATE.d/util/escape_fstab.sh b/pkg/cidata/cidata.TEMPLATE.d/util/escape_fstab.sh new file mode 100755 index 00000000000..cab1861b6f5 --- /dev/null +++ b/pkg/cidata/cidata.TEMPLATE.d/util/escape_fstab.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright The Lima Authors +# SPDX-License-Identifier: Apache-2.0 + +# Read an /etc/fstab on stdin and write it to stdout with the mount-point field +# octal-escaped (per fstab(5)) for cloud-config virtiofs entries whose path +# contains a space or tab. cloud-init's cc_mounts writes the mount point verbatim, +# so a space/tab produces an unparsable line that mount(8) silently skips via the +# nofail option. Fields are tab-separated, so -F'\t' isolates the mount point; +# already-escaped paths have no literal space/tab, so the transformation is +# idempotent (and stays correct once cloud-init escapes the field itself). +# +# See: +# https://github.com/lima-vm/lima/issues/5136 +# https://github.com/abiosoft/colima/issues/1471 +# https://github.com/canonical/cloud-init/issues/3603 (cc_mounts does not escape) +# https://github.com/canonical/cloud-init/issues/6911 (the upstream cloud-init fix) + +set -eu + +: "${SELFTEST:=}" +if [ -n "${SELFTEST}" ]; then + unset SELFTEST + tab=$(printf '\t') + check() { + local desc=$1 input=$2 want=$3 got + got=$(printf '%s\n' "${input}" | "$0") + if [ "${got}" = "${want}" ]; then + echo "ok: ${desc}" + else + echo "FAIL: ${desc}" >&2 + printf ' want: %q\n got: %q\n' "${want}" "${got}" >&2 + return 1 + fi + } + echo >&2 "=== Running tests ===" + check "space in the mount point is escaped" \ + "tag${tab}/tmp/dir with spaces${tab}virtiofs${tab}rw,nofail,comment=cloudconfig${tab}0${tab}0" \ + "tag${tab}/tmp/dir\\040with\\040spaces${tab}virtiofs${tab}rw,nofail,comment=cloudconfig${tab}0${tab}0" + check "already-escaped path is unchanged (idempotent)" \ + "tag${tab}/tmp/dir\\040with\\040spaces${tab}virtiofs${tab}rw,nofail,comment=cloudconfig${tab}0${tab}0" \ + "tag${tab}/tmp/dir\\040with\\040spaces${tab}virtiofs${tab}rw,nofail,comment=cloudconfig${tab}0${tab}0" + check "path without whitespace is unchanged" \ + "tag${tab}/mnt/nospace${tab}virtiofs${tab}rw,nofail,comment=cloudconfig${tab}0${tab}0" \ + "tag${tab}/mnt/nospace${tab}virtiofs${tab}rw,nofail,comment=cloudconfig${tab}0${tab}0" + check "backslash is escaped before the space" \ + "tag${tab}/a b\\c${tab}virtiofs${tab}rw,comment=cloudconfig${tab}0${tab}0" \ + "tag${tab}/a\\040b\\134c${tab}virtiofs${tab}rw,comment=cloudconfig${tab}0${tab}0" + check "entry without comment=cloudconfig is unchanged" \ + "tag${tab}/tmp/dir with spaces${tab}virtiofs${tab}rw${tab}0${tab}0" \ + "tag${tab}/tmp/dir with spaces${tab}virtiofs${tab}rw${tab}0${tab}0" + check "non-virtiofs entry is unchanged" \ + "/dev/sda1${tab}/data dir${tab}ext4${tab}defaults,comment=cloudconfig${tab}0${tab}0" \ + "/dev/sda1${tab}/data dir${tab}ext4${tab}defaults,comment=cloudconfig${tab}0${tab}0" + echo >&2 "=== All tests passed ===" + exit 0 +fi + +awk -F'\t' 'BEGIN { OFS = "\t" } + $3 == "virtiofs" && $4 ~ /comment=cloudconfig/ && $2 ~ /[ \t]/ { + p = $2 + gsub(/\\/, "\\134", p) # backslash first so introduced escapes are not re-escaped + gsub(/ /, "\\040", p) + gsub(/\t/, "\\011", p) + $2 = p + } + { print }' diff --git a/pkg/cidata/cidata.TEMPLATE.d/util/unescape_fstab.sh b/pkg/cidata/cidata.TEMPLATE.d/util/unescape_fstab.sh new file mode 100755 index 00000000000..05e97131403 --- /dev/null +++ b/pkg/cidata/cidata.TEMPLATE.d/util/unescape_fstab.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright The Lima Authors +# SPDX-License-Identifier: Apache-2.0 + +# Read a string on stdin and write it to stdout with the octal escapes used in +# /etc/fstab and /proc/mounts decoded to their literal characters: +# "\040" -> space, "\011" -> tab, "\012" -> newline, "\134" -> backslash, +# "\043" -> "#". The inverse of util/escape_fstab.sh. +# https://github.com/torvalds/linux/blob/v6.6/fs/proc_namespace.c#L89 + +set -eu + +: "${SELFTEST:=}" +if [ -n "${SELFTEST}" ]; then + unset SELFTEST + tab=$(printf '\t') + check() { + local desc=$1 input=$2 want=$3 got + got=$(printf '%s\n' "${input}" | "$0") + if [ "${got}" = "${want}" ]; then + echo "ok: ${desc}" + else + echo "FAIL: ${desc}" >&2 + printf ' want: %q\n got: %q\n' "${want}" "${got}" >&2 + return 1 + fi + } + echo >&2 "=== Running tests ===" + check "spaces are decoded" '/tmp/dir\040with\040spaces' "/tmp/dir with spaces" + check "tab is decoded" '/a\011b' "/a${tab}b" + check "newline is decoded" 'a\012b' "$(printf 'a\nb')" + check "backslash is decoded" '/a\134b' '/a\b' + check "hash is decoded" '/a\043b' "/a#b" + check "a string without escapes is unchanged" "/mnt/nospace" "/mnt/nospace" + check "mixed escapes are decoded" '/a\040b\134c' '/a b\c' + echo >&2 "=== All tests passed ===" + exit 0 +fi + +sed -e 's/\\040/ /g; s/\\011/\t/g; s/\\012/\n/g; s/\\134/\\/g; s/\\043/#/g'