Skip to content

fix(mounts): escape special characters in fstab mount paths#6911

Open
ekalinin wants to merge 1 commit into
canonical:mainfrom
ekalinin:fix/mounts-escape-fstab-paths
Open

fix(mounts): escape special characters in fstab mount paths#6911
ekalinin wants to merge 1 commit into
canonical:mainfrom
ekalinin:fix/mounts-escape-fstab-paths

Conversation

@ekalinin

@ekalinin ekalinin commented Jun 21, 2026

Copy link
Copy Markdown

Mount points containing spaces (and tabs/newlines/backslashes) are now
octal-escaped (\040, \011, \012, \134) when written to /etc/fstab, so
mount -a can parse them. Directories are still created with real characters.

LP: #1861903
Fixes GH-3603

Proposed Commit Message

fix(mounts): escape special characters in fstab mount paths

Mount points containing spaces (and tabs/newlines/backslashes) are now
octal-escaped (\040, \011, \012, \134) when written to /etc/fstab, so
`mount -a` can parse them. Directories are still created with real
characters.

LP: #1861903
Fixes GH-3603

Additional Context

Fixes GH-3603 (migrated from Launchpad LP: #1861903).

cc_mounts built each /etc/fstab line with a bare "\t".join(entry), so any
whitespace inside a field (most commonly a space in the mount point) was written
verbatim. Because fstab fields are whitespace-separated, the resulting line is
unparseable and mount -a fails with parse error at line N -- ignored. The
documented user workaround (\040 in the YAML) didn't help either: the literal
characters \040 ended up as the directory name instead of a space.

This change octal-escapes the four characters significant in an fstab field
(backslash \134, space \040, tab \011, newline \012), applied only to
the fs_spec and fs_file fields — the other fields never contain characters
that need escaping. Backslash is escaped first so the escapes we introduce are
not re-escaped. This matches util-linux's mangle/unmangle behaviour.

Notes:
- The escape/unescape helpers (escape_fstab_field/unescape_fstab_field)
live in cloudinit.util; util.mounts() now reuses unescape_fstab_field
instead of its previous spaces-only replace("\040", " "), so it also
decodes escaped tabs, newlines and backslashes from /proc/mounts.
- The mount-point directory is still created from the unescaped config value
(util.ensure_dir), so the real directory contains a real space.
- parse_fstab() unescapes the device token before using it as the dedup key,
keeping the module idempotent (a re-run won't add a duplicate entry).
- A docs example (cc_mounts/example3.yaml) and a note about automatic
escaping were added.

Test Steps

Reproduces the bug and verifies the fix on a live system. No extra hardware is
needed: a network fs_spec (contains :) is kept by cc_mounts without the
device existing, and noauto keeps mount -a from touching it at boot, so the
test isolates the fstab-escaping behaviour.

  1. Boot an instance with user-data.yaml:
 #cloud-config
 mounts:
   - [ "server.example:/export", "/mnt/Cdrom Drive", nfs, "defaults,noauto", "0", "0" ]
$ lxc launch ubuntu:noble test-mounts --config=user.user-data="$(cat user-data.yaml)"
$ lxc exec test-mounts -- cloud-init status --wait
  1. The fstab entry is octal-escaped (space written as \040):
$ lxc exec test-mounts -- tail -n1 /etc/fstab
server.example:/export  /mnt/Cdrom\040Drive  nfs  defaults,noauto,comment=cloudconfig  0  0
  1. util-linux can now parse the entry and resolve the mount point (this is the
    step that printed parse error ... -- ignored before the fix):
$ lxc exec test-mounts -- findmnt --fstab "/mnt/Cdrom Drive"
# prints the resolved TARGET/SOURCE row, no parse error
  1. The directory was created with a real space (not the literal \040):
$ lxc exec test-mounts -- ls -d "/mnt/Cdrom Drive"
  1. Idempotency: re-running the module must not duplicate the entry:
$ lxc exec test-mounts -- cloud-init single --name mounts --frequency always
$ lxc exec test-mounts -- grep -c 'Cdrom\\040Drive' /etc/fstab   # -> 1

Before the fix, step 2 shows a literal space in /etc/fstab and steps 3/mount -a
fail with parse error at line N -- ignored.

Unit tests:

  $ tox -e py3 -- tests/unittests/config/test_cc_mounts.py tests/unittests/test_util.py

New cases: TestFstabHandling.test_fstab_mountpoint_with_spaces (space in both
the device and the mount point) and test_fstab_mountpoint_with_spaces_idempotent
in test_cc_mounts.py, plus TestFstabEscaping (escape/unescape round-trip) in
test_util.py.

Integration test:

tests/integration_tests/bugs/test_gh6911.py boots the user-data above and
asserts clean cloud-init logs, the escaped value in /etc/fstab, and that
/mnt/Cdrom Drive exists.

Merge type

  • Squash merge using "Proposed Commit Message"
  • Rebase and merge unique commits. Requires commit messages per-commit each referencing the pull request number (#<PR_NUM>)

ekalinin added a commit to ekalinin/lima that referenced this pull request Jun 22, 2026
A vz + virtiofs mount whose host path contains a space (e.g.
"/Volumes/External HD") was never mounted in the guest, and no error was
reported.

cloud-init's cc_mounts writes each mount into /etc/fstab with a bare
"\t".join(fields) and does not octal-escape the mount point
(canonical/cloud-init#3603). A space/tab in the path therefore produces an
unparseable fstab line that mount(8) silently skips because of the nofail
option, so the mount simply never appears.

cc_mounts already creates the mount-point directory from the unescaped value,
so only the fstab line needs repair. In boot.Linux/05-lima-mounts.sh, octal-
escape the mount-point field of cloudconfig virtiofs entries (parsing with
-F'\t' isolates the field reliably and makes the pass idempotent), then
mount -t virtiofs -a. The existing SELinux remount now decodes the escaped
mount point before calling mount(8), mirroring the unescape in
boot.Linux/04-persistent-data-volume.sh.

This complements, but does not depend on, the upstream cloud-init fix
canonical/cloud-init#6911; distros will ship that only years from now.

Fixes: lima-vm#5136
Link: abiosoft/colima#1471
Signed-off-by: Eugene Kalinin <e.v.kalinin@gmail.com>

@blackboxsw blackboxsw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for taking on this work and contributing to cloud-init.

I have a couple of nits inline on this before we land.

Also, given that this aspect of mount testing hasn't historically been supported, it would be good to have a simple integration test that covers this case. Your tests case documented in the PR looks straightforward and could be provided as a one-off tests/integration-tests/bugs/test_gh691.py that provides your example user-data, asserts no errors in cloud-init logs, asserts that the escaped values made it into /etc/fstab and that /mnt/Cdrom Drive exists.

Thank you again for your work here.

Comment thread cloudinit/config/cc_mounts.py Outdated
Comment thread cloudinit/config/cc_mounts.py Outdated
Comment thread doc/module-docs/cc_mounts/data.yaml
Comment thread tests/unittests/config/test_cc_mounts.py Outdated
@blackboxsw blackboxsw self-assigned this Jul 2, 2026
@github-actions github-actions Bot added the documentation This Pull Request changes documentation label Jul 3, 2026
@ekalinin

ekalinin commented Jul 3, 2026

Copy link
Copy Markdown
Author

Thank you for taking on this work and contributing to cloud-init.

I have a couple of nits inline on this before we land.

Also, given that this aspect of mount testing hasn't historically been supported, it would be good to have a simple integration test that covers this case. Your tests case documented in the PR looks straightforward and could be provided as a one-off tests/integration-tests/bugs/test_gh691.py that provides your example user-data, asserts no errors in cloud-init logs, asserts that the escaped values made it into /etc/fstab and that /mnt/Cdrom Drive exists.

Thank you again for your work here.

Thank you for the review and the helpful pointers!

I've pushed changes addressing the inline nits, and added the integration test as suggested. A couple of notes:

  • I named it tests/integration_tests/bugs/test_gh6911.py - I assumed the gh691 in your comment was the PR number (#6911) with the last digit dropped, and this matches the test_gh<N>.py convention in that directory. Happy to rename if you had something else in mind.
  • It uses the example user-data from the PR (a network fs_spec with noauto, so the entry lands in /etc/fstab without depending on a real device/NFS server) and asserts: clean cloud-init logs (verify_clean_log + the config-mounts success line), the octal-escaped value (/mnt/Cdrom\040Drive) in /etc/fstab, and that /mnt/Cdrom Drive exists.

Let me know if you'd like any changes. Thanks again for the guidance!

cc_mounts wrote /etc/fstab entries by joining fields with tabs without
any escaping, so a space (or tab/newline/backslash) in the device
(fs_spec) or mount point (fs_file) produced an unparseable line and
`mount -a` failed with "parse error at line N -- ignored". The
documented `\040` workaround also failed, because the literal characters
ended up in the created directory name instead of a space.

Octal-escape the four characters that are significant in a
whitespace-separated fstab field (`\134`, `\040`, `\011`, `\012`) when
writing the fs_spec and fs_file fields (the only fields where escaping is
meaningful), matching util-linux's mangle/unmangle behaviour. The mount
point directory is still created from the unescaped value, and
parse_fstab() unescapes the device token so re-runs stay idempotent.

The escape/unescape helpers live in cloudinit.util and are reused by
util.mounts(), which previously only undid `\040` for spaces and now
decodes tabs, newlines and backslashes as well.

Add unit tests plus a one-off integration test (test_gh6911.py) and
document the behaviour in the cc_mounts examples.

LP: #1861903
Fixes canonicalGH-3603
@ekalinin ekalinin force-pushed the fix/mounts-escape-fstab-paths branch from 62a062b to 8e4984f Compare July 3, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation This Pull Request changes documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mountpoint containing a space not working

2 participants