Skip to content

fix(filesystem): ensure readdir names are NUL-terminated - #197

Merged
Aswin-V merged 1 commit into
embeddedos-org:masterfrom
harshini-5040:fix/filesystem-readdir-nul
Nov 2, 2026
Merged

Aswin-V merged 1 commit into
embeddedos-org:masterfrom
harshini-5040:fix/filesystem-readdir-nul

Conversation

@harshini-5040

Copy link
Copy Markdown
Contributor

Summary

eos_fs_readdir() can return an entry whose name is not NUL-terminated.

It copies the inode name with strncpy(e->name, name, EOS_PATH_MAX - 1). eos_fs_validate_path() accepts paths of up to EOS_PATH_MAX - 1 (127) characters, and for a name of exactly that length strncpy writes no terminator. e->name[EOS_PATH_MAX - 1] keeps whatever the caller's buffer held, so a caller that reads the name as a string (printf, strcmp, strlen) reads past the end of the field.

Reproduced on master (89300b6): create a 127-character directory, pre-fill the eos_dirent_t with 'X', and read it back:

e.name[EOS_PATH_MAX-1]    = 0x58 (expected 0x00)
NUL within e.name         = NO
strlen(e.name)            = 128

Type of Change

  • feat — New feature
  • fix — Bug fix
  • docs — Documentation only
  • style — Formatting, no code change
  • refactor — Code restructuring without behavior change
  • test — Add or fix tests
  • build — Build system or dependency changes
  • ci — CI/CD pipeline changes
  • perf — Performance improvement

Changes

  • services/filesystem/src/filesystem.c: terminate the name explicitly after the copy (one line), the same way eos_fs_init() and eos_fs_rename() in this file already do.
  • tests/test_filesystem.c: add test_fs_readdir_terminates_longest_name, which creates a 127-character path, fills the dirent with non-zero bytes before each readdir, and checks that the returned name is terminated and equal to the path.

Names shorter than 127 characters are unaffected: strncpy already zero-fills the rest of the buffer, so that byte was already NUL. No header, struct, signature or return value changes.

Testing

WSL Ubuntu, gcc 13.3.0, -DCMAKE_BUILD_TYPE=Debug -DEOS_BUILD_TESTS=ON:

  • Unit tests pass: ctest, 50/50 passing, in both a plain Debug build and an ASan + UBSan build
  • Integration tests pass (not run separately beyond the ctest suite)
  • Manual testing performed (standalone reproduction against master, output above)
  • New tests added for new functionality

The new test fails on master and passes with the fix:

# before
test_filesystem.c:373: test_fs_readdir_terminates_longest_name: Assertion `entry.name[EOS_PATH_MAX - 1] == '\0'' failed.
# after
[PASS] fs readdir terminates longest name
100% tests passed, 0 tests failed out of 50

Under ASan + UBSan: no AddressSanitizer or LeakSanitizer reports, and no UBSan reports outside the vendored ed25519 sources. The ed25519 reports are the same on master.

Pre-Submission Checklist

  • Code compiles without warnings (-Wall -Wextra -Werror for C): no new warnings. The build shows 21 warnings, all of them already on master and none in the changed files.
  • All existing tests pass
  • New tests added for new functionality
  • Documentation updated if API changed (N/A, no API change)
  • Commit messages follow (): convention
  • Branch is rebased on latest master

Related Issues

None. No existing issue covers this bug.

Additional Notes

test_filesystem still prints ALL FS TESTS PASSED (14/14), but it already ran 16 tests before this change. I left that out of this PR to keep it focused.

eos_fs_readdir() copied the inode name with
strncpy(e->name, name, EOS_PATH_MAX - 1). eos_fs_validate_path() accepts
paths of up to EOS_PATH_MAX - 1 characters, and for a name of exactly that
length strncpy writes no terminator, so e->name[EOS_PATH_MAX - 1] kept
whatever the caller's buffer held. Callers reading the name as a string
then ran past the end of the field.

Terminate explicitly, as eos_fs_init() and eos_fs_rename() in the same
file already do. Shorter names are unaffected: strncpy already zero-fills
the rest of the buffer, so that byte was already NUL.

The regression test creates a 127-character path, pre-fills the dirent
with non-zero bytes, and checks the returned name is terminated and equal
to the path. It fails on master and passes with this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Aswin-V

Aswin-V commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Stewardship review (2026-11-01, acting-owner sweep): the fix itself is approved in principle, but this PR is held — it does not yet meet the merge bar (CI green on the head sha + linked closing issue).

What checks out: the premise reproduces from the code exactly as described (strncpy(..., EOS_PATH_MAX - 1) writes no terminator for a 127-char name), the one-line fix e->name[EOS_PATH_MAX - 1] = '\0'; in eos_fs_readdir() is correct, and the regression test (pre-filling the dirent with 'X') is well designed. Host Tests / Linux / Windows builds pass on the head.

What blocks merging:

  1. Stale base. The head is based on 89300b6 (2026-10-23). Its Build Gate, QEMU Boot Test and Cross-compile ARM Cortex-M4 failures are a pre-existing linker failure on that base (boards/startup_stm32f407.S: Default_Handler dangerous relocation) — unrelated to this change, and those same jobs pass on current master. Please rebase onto current master and push so CI re-runs on a current base.
  2. No closing issue. The Policy / Linked Issue check fails: the PR body must close a same-repository issue (Fixes #N). There is currently no dedicated issue for this readdir bug (searched 2026-11-01) — please file one describing the bug (your PR description's reproduction is a good basis) and add Fixes #N to the PR body.

With a rebased, green head and a linked issue, this merges as-is — no code changes requested.

Aswin-V added a commit that referenced this pull request Nov 2, 2026
…adme-matrix

fix(filesystem): land #197 — ensure readdir names are NUL-terminated (rebased)
@Aswin-V
Aswin-V merged commit a3760f0 into embeddedos-org:master Nov 2, 2026
27 of 31 checks passed
@Aswin-V

Aswin-V commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Superseded by #229, which has now merged — closing this PR.

Your commits landed unchanged: #229's merge commit has this PR's head (e3cfeb6) as its first parent, so your work is in master exactly as you authored it, and #229's CI on current master is fully green. The rebase could not be pushed into this fork branch — the GitHub API returns 404 on git-data writes to fork branches even with maintainer edits enabled (verified 2026-11-02) — which is why the landing went through a same-repo PR instead.

One process note: #229 was initially opened with a wrong description (a stale review note misdescribed this PR), caught and corrected on the PR itself the same day; the press report records it. The change that landed is exactly yours. Thank you for the fix and the well-built regression test.

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.

3 participants