Skip to content

shared-module/os: fix size_t underflow in abspath(".." at root) - #11113

Merged
dhalbert merged 1 commit into
adafruit:mainfrom
94xhn:fix-abspath-root-dotdot-underflow
Jul 13, 2026
Merged

shared-module/os: fix size_t underflow in abspath(".." at root)#11113
dhalbert merged 1 commit into
adafruit:mainfrom
94xhn:fix-abspath-root-dotdot-underflow

Conversation

@94xhn

@94xhn 94xhn commented Jul 13, 2026

Copy link
Copy Markdown

Summary

common_hal_os_path_abspath() (shared-module/os/__init__.c) normalizes ..
components by decrementing slash_count and then indexing
slashes[slash_count - 1]. When the path resolves to .. applied directly at
the filesystem root (/), only the root boundary itself has been recorded, so
slash_count == 1 at that point. The unconditional slash_count-- makes it
0, and slashes[(size_t)0 - 1] reads slashes[SIZE_MAX] — an out-of-bounds
read of a 1-element stack VLA. The garbage value read back is then used as
output_len to do full_path[output_len] = '\0', an out-of-bounds/wild write.

This helper is shared by every path-taking os function, so it's reachable
directly from user code whenever cwd is "/" (the default at boot, before
any chdir), e.g.:

os.chdir("..")
os.listdir("..")
os.mkdir("../x")
os.remove("../x")
os.rename("../x", "../y")
os.utime("../x", ...)

Fix

Only decrement slash_count when more than the root boundary is recorded
(slash_count > 1). This matches POSIX "cd .. from / stays at /"
semantics, and leaves every non-root case byte-for-byte identical to the
current behavior (verified below).

Verification

Building/running the full CircuitPython firmware/unix-port test suite wasn't
practical in this environment (submodules aren't checked out and this is a
3-line change deep in path normalization, not something that needs a target
board to exercise). Instead I extracted the exact normalization algorithm
verbatim (only the allocator call and the cwd plumbing were adapted to run
as a standalone host program — every line of the loop under test is unchanged)
into a small C program and ran it under gcc + AddressSanitizer:

  • Before this fix: abspath(cwd="/", path="..") reliably triggers
    ERROR: AddressSanitizer: dynamic-stack-buffer-overflow ...
    #0 ... in common_hal_os_path_abspath ... (the `slashes[slash_count - 1]` line
       in the ".." branch)
    
    and the process aborts.
  • After this fix: the same call returns "/", abspath("/", "../foo")
    returns "/foo", and abspath(NULL, "..") (the cwd == NULL/fresh-boot
    default-init path) also returns "/" — zero ASan reports.
  • Every non-root case I tried (abspath("/a/b", ".."),
    abspath("/a/b/c", "../.."), abspath("/", "/foo/../bar"),
    abspath("/a", "./foo"), etc.) produces byte-identical output between
    the unpatched and patched algorithm, i.e. this change has no effect outside
    the root-boundary case.

I also ran the repository's own formatter (tools/codeformat.py -c /
uncrustify) over the changed file; it made no further changes beyond what's
in this diff.

Disclosure

Generative AI (Claude, Anthropic) was used to help investigate this issue
(reading the current source, deriving the failure trace, writing and running
the AddressSanitizer reproduction) and to implement this fix. All changes
were reviewed by me before submission.

common_hal_os_path_abspath() removes ".." components by decrementing
slash_count and then indexing slashes[slash_count - 1]. When the input
path resolves to ".." applied directly at the filesystem root ("/"),
only the root boundary itself has been recorded (slash_count == 1).
The unconditional slash_count-- makes it 0, and slashes[(size_t)0 - 1]
reads slashes[SIZE_MAX] -- an out-of-bounds read of a single-element
stack VLA whose garbage value is then used as output_len to write
full_path[output_len] = '\0', an out-of-bounds/wild write.

This is reachable directly from Python whenever cwd is "/" (the default
at boot, before any chdir), e.g.:
    os.chdir("..")
    os.listdir("..")
    os.mkdir("../x")
and likewise for os.remove/os.rmdir/os.rename/os.utime, since they all
normalize their path argument through this same helper first.

Fix: only decrement slash_count when there is more than the root
boundary recorded (slash_count > 1). This matches POSIX "cd .. from /
stays at /" semantics and leaves every non-root case byte-for-byte
identical to the previous behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@dhalbert dhalbert 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.

This is a good fix, thanks. It would be nice to add a test for this case to the test suite. If you have an idea for that, go ahead. There is no direct way to call abspath, unfortunately.

@dhalbert
dhalbert merged commit 242fe31 into adafruit:main Jul 13, 2026
674 checks passed
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.

2 participants