Bind SCP file timestamps to open descriptor#1037
Conversation
There was a problem hiding this comment.
Pull request overview
Hardens the SCP receive path against a symlink TOCTOU by applying peer-supplied timestamps to the still-open destination file descriptor (when supported), instead of applying them later via a path-based utimes().
Changes:
- Update SCP receive timestamp application to prefer fd-based timestamp setting (
futimes/_futime) and flush before applying times to prevent close-time writes from clobberingmtime. - Add
WFUTIMESport macro gated byHAVE_FUTIMES, and detectfutimesinconfigure.ac. - Add an end-to-end unit test asserting received files end up with the peer-supplied
mtime/atime.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/wolfscp.c |
Applies timestamps via the open file descriptor (when available) with pre-flush ordering to prevent TOCTOU and mtime clobbering. |
wolfssh/port.h |
Introduces WFUTIMES macro gated by HAVE_FUTIMES for portability. |
configure.ac |
Adds futimes feature detection to enable fd-based timestamp updates. |
tests/unit.c |
Adds an end-to-end regression test for SCP receive timestamp behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
f939cc7 to
75b4dc1
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1037
Scan targets checked: wolfssh-bugs, wolfssh-src
No new issues found in the changed files. ✅
75b4dc1 to
497436a
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1037
Scan targets checked: wolfssh-bugs, wolfssh-src
No new issues found in the changed files. ✅
497436a to
ebdd79b
Compare
ebdd79b to
54dcd50
Compare
|
Hi @ejohnstown , |
Bind SCP file timestamps to the open descriptor (fix symlink TOCTOU)
Summary
In the SCP receive path, peer-supplied modification/access times were applied with a path-based
utimes()after the destination file was closed. On POSIX,utimes()follows symlinks, so a local process running as the authenticated user could replace the just-written file with a symlink in the window between the close and the timestamp update, redirecting the peer-controlledmTime/aTimeonto an arbitrary target inode.This change binds the timestamp update to the open file descriptor where possible, and uses a symlink-safe path call where it isn't, so the update can no longer be redirected to an arbitrary target.
Approach — a priority ladder
The timestamp update now prefers the strongest mechanism the platform supports:
futimens()on the open fd, before close (POSIX,HAVE_FUTIMENS) — bound to the inode opened inNEW_FILE; no path lookup, so a path swap is irrelevant. Fully closes the race._futime()on the open CRT fd, before close (Windows) — same idea; no longer re-opens by path.utimensat(AT_FDCWD, path, ts, AT_SYMLINK_NOFOLLOW)by path, after close (POSIX withoutfutimens,HAVE_UTIMENSAT) — no descriptor to bind to, butAT_SYMLINK_NOFOLLOWrefuses to follow a swapped-in symlink, defeating the exploited redirect-to-arbitrary-target.utimes()by path, after close — last resort only when neither of the above is detected.futimenskeeps strict priority:WOLFSSH_SCP_FD_UTIMESis keyed on the fd-based macro only, soutimensatis reached solely on builds withoutfutimens.Changes
src/wolfscp.cSetTimestampInfo()now takes the openWFILE*and implements the ladder above (fd → path-no-follow → path).fclose()cannot write and overwrite the modification time.WOLFSSH_SCP_FILE_DONEhandler applies the timestamp beforeWFCLOSEon descriptor-capable platforms. If timestamps are present but no file was opened, it aborts rather than falling back to an unsafe path update.fp, matching the POSIX contract.WS_FATAL_ERRORon failure, consistent with the documentedWS_SUCCESS/negative contract.wolfssh/port.hWFUTIMES(fd, t)(→futimens, gated onHAVE_FUTIMENS) — the fd-bound primary.WUTIMES_NOFOLLOW(f, t)(→utimensat(..., AT_SYMLINK_NOFOLLOW), gated onHAVE_UTIMENSAT) — the symlink-safe path fallback.struct timevalpair to thestruct timespecpair the new calls expect, and include<time.h>sostruct timespecis guaranteed visible (it is not required to come transitively from<sys/stat.h>/<sys/time.h>).utimensatexists; priority is enforced at the call site.configure.acAC_CHECK_FUNCSentry with header-awareAC_LINK_IFELSEprobes that defineHAVE_FUTIMENS/HAVE_UTIMENSAT. Both functions are POSIX.1-2008 and their declarations are feature-test-macro gated, so a link-only test can report the libc symbol present while the prototype stays hidden at compile time — silently disabling the hardening. Probing through the real header (and prototype) makes a positive result build-safe.tests/unit.ctest_ScpRecvCallback_Timestamp— drives the default receive callback through a full single-file receive and asserts the written file'smtime/atimeequal the peer-supplied values. On descriptor builds it also asserts aFILE_DONEcarrying timestamps with no open file aborts (no path fallback) and pins the flush-before-set ordering.test_ScpTimestamp_NoFollow(HAVE_UTIMENSAT && WOLFSSH_HAVE_SYMLINK) — sets times through a symlink and asserts the time lands on the symlink itself while the target it points to is left untouched.Platform behavior
futimensfutimens()on the open fd, before close_futime()on the open CRT fd, before closefutimens, withutimensatutimensat(AT_SYMLINK_NOFOLLOW)by path, after closeutimes()by path, after close (residual race)WUTIMES(unchanged)Known limitation
futimensfully closes the race. Theutimensatfallback defeats the symlink-to-arbitrary-target redirect (the exploited case) but a narrow window remains where the path could be swapped for a regular file before the call. The plain-utimeslast resort retains the original behavior and is only reachable on builds that detect neitherfutimensnorutimensat; autotools builds get one of the two.Testing
tests/unit.test—ScpRecvCallback_TimestampandScpTimestamp_NoFollowpass; full suite green under ASan + UBSan (no leak detection on macOS).ScpRecvCallback_Timestampfail (fcloseclobbers mtime); flippingAT_SYMLINK_NOFOLLOW→0makesScpTimestamp_NoFollowfail (follows the symlink); restoring the fix makes both pass.configurereportschecking for futimens... yes/checking for utimensat... yesvia the header-aware probes;HAVE_FUTIMENS/HAVE_UTIMENSATdefined inconfig.h.--enable-scp --enable-sftp, no sanitizers) — clean, no new warnings.