Skip to content

Decode /proc/mounts octal escapes in parseMounts - #129

Merged
LarsLaskowski merged 2 commits into
mainfrom
fix-issue-128-parsemounts-octal-escapes
Aug 23, 2026
Merged

Decode /proc/mounts octal escapes in parseMounts#129
LarsLaskowski merged 2 commits into
mainfrom
fix-issue-128-parsemounts-octal-escapes

Conversation

@LarsLaskowski

@LarsLaskowski LarsLaskowski commented Aug 23, 2026

Copy link
Copy Markdown
Owner

📖 Description

parseMounts in internal/collector/disk.go split each /proc/mounts line
on whitespace and used the resulting fields as device/mountpoint/fstype
verbatim. The kernel encodes space, tab, newline, and backslash as octal
escapes (\040, \011, \012, \134) in /proc/mounts, since those
characters would otherwise be ambiguous in its whitespace-separated format
(see proc(5)). A mountpoint or device path containing one of these — e.g. a
USB drive labeled My Drive, mounted at /media/pi/My\040Drive — was never
decoded back to its real path, so it wouldn't match the actual filesystem
path elsewhere (an include/exclude list, the dashboard, the API).

This is a bug fix, not a breaking change: parsing of ordinary paths (the
overwhelming majority, with no escapes) is unaffected.

Adds unescapeMountField, applied to both the device and mountpoint
fields in parseMounts. It only recognizes the four escapes proc(5)
documents for this format; any other backslash sequence (or a lone trailing
backslash) is left untouched rather than misinterpreted.

🎫 Issues

Closes #128

👩‍💻 Reviewer Notes

The change is contained to parseMounts and the new unescapeMountField/
mountFieldEscapes in internal/collector/disk.go. Everything downstream
(Collect, dedupeMounts, collectDisk) is untouched — they already
operate on mountEntry.device/.mountpoint as opaque strings.

📑 Test Plan

Added to internal/collector/disk_test.go, alongside the existing
TestParseMounts:

  • TestParseMounts_DecodesOctalEscapes — a mountpoint fixture with all four
    escapes (\040, \011, \012, \134) decodes to the literal
    space/tab/newline/backslash characters.
  • TestParseMounts_DecodesOctalEscapesInDevice — the same escape (\040)
    in the device field (e.g. a /dev/disk/by-label/... path) is decoded too.
  • TestParseMounts_LeavesUnknownBackslashSequencesUntouched — a backslash
    sequence that isn't one of the four real escapes, and a lone trailing
    backslash, are passed through unchanged (regression guard against
    misinterpreting arbitrary \NNN-shaped text, and against an out-of-range
    slice on a short trailing backslash).

Confirmed TestParseMounts_DecodesOctalEscapes and
TestParseMounts_DecodesOctalEscapesInDevice fail against the pre-fix code
(raw \040 etc. observed in the parsed fields) and pass after the fix.

Ran go build ./..., go vet ./..., go test ./... -race -cover (all
packages pass, internal/collector at 91.1%), and golangci-lint run (0
issues).

This isn't verifiable against a real escaped mountpoint on physical Pi
hardware in this environment, but the fixtures reproduce the exact
/proc/mounts encoding documented in proc(5) and observed by the kernel
for such paths.

✅ Checklist

General

  • I have added/updated tests for my changes (go test ./... -race -cover passes locally).
  • go vet ./... and golangci-lint run are clean.
  • I have tested my changes.
  • I have read the CONTRIBUTING documentation and followed the project's code style guidelines.
  • I have updated ARCHITECTURE.md if this changes a documented design decision. (not applicable — internal parsing detail, no documented design decision changes)

REST API / configuration / packaging

  • I have updated docs/API.md to reflect a REST API change. (not applicable — no API shape change; mountpoint/device strings are now correctly decoded, not restructured)
  • No breaking change to /api/v1/... response shapes, or a new API version (/api/v2/...) was introduced instead.
  • I have updated README.md / packaging/pimonitor.example.yaml to reflect a new or changed configuration option. (not applicable — no new configuration)
  • I have updated packaging/install.sh or the systemd units if this changes installation/packaging, and kept the unprivileged/privileged service split intact (see SECURITY.md). (not applicable)

⏭ Next Steps

None — this closes out the follow-up noted in #109.

The kernel encodes space, tab, newline, and backslash as octal escapes
(\040, \011, \012, \134) in /proc/mounts, since those characters would
otherwise be ambiguous in its whitespace-separated format. parseMounts
used the raw whitespace-split fields verbatim, so a mountpoint or
device path containing one of these characters (e.g. a USB drive
labeled "My Drive", mounted at /media/pi/My\040Drive) never matched
its real path.

Add unescapeMountField to decode the four escapes proc(5) documents
for this format, applied to both the device and mountpoint fields.
Any other backslash sequence is left untouched.

@LarsLaskowski LarsLaskowski left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed against the project's Go/security/API conventions. go build ./..., go vet ./..., go test ./... -race -cover (internal/collector 91.1%) and golangci-lint run (0 issues) all pass on a4b2bc9.

The decoder logic itself is correct — I checked the bounds arithmetic (i+3 < len(s) is exactly the condition under which s[i+1:i+4] is in range, including an escape ending the string), the i += 3 / loop-i++ advance, and the left-to-right single pass that makes \134 self-consistent. No API shape change, no new dependency, no exec/privilege surface touched.

Two findings inline, both in the test/consistency layer rather than the decoder. The table-driven restructuring is required by docs/TESTS.md and should be addressed before merge.

Comment thread internal/collector/disk_test.go
Comment thread internal/collector/disk.go Outdated
fstype is mangled by the kernel with the same octal escapes as device
and mountpoint (e.g. a fuse subtype derived from user input can be
"fuse.My\040FS"), but only the other two fields were being decoded.
Apply unescapeMountField to fstype as well, since it's used for the
excludedFSType lookup and surfaced verbatim in /api/v1/metrics and the
dashboard.

Collapse the three separate escape-decoding test functions into one
table-driven TestParseMounts_DecodesOctalEscapes per docs/TESTS.md, and
add a case pinning that the single left-to-right decoding pass doesn't
re-scan bytes it already emitted: an escaped backslash ("\134")
immediately followed by digits that look like another escape ("040")
must decode to a literal "\040", not a space.
@sonarqubecloud

Copy link
Copy Markdown

@LarsLaskowski
LarsLaskowski merged commit f5ceab5 into main Aug 23, 2026
5 checks passed
@LarsLaskowski
LarsLaskowski deleted the fix-issue-128-parsemounts-octal-escapes branch August 23, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

parseMounts doesn't decode /proc/mounts octal escapes

2 participants