Skip to content

fix: keep mtime when utimens only updates atime (UTIME_OMIT) - #386

Open
Yike-Ye wants to merge 1 commit into
libfuse:masterfrom
Yike-Ye:fix/utimens-utime-omit
Open

fix: keep mtime when utimens only updates atime (UTIME_OMIT)#386
Yike-Ye wants to merge 1 commit into
libfuse:masterfrom
Yike-Ye:fix/utimens-utime-omit

Conversation

@Yike-Ye

@Yike-Ye Yike-Ye commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Problem

On an sshfs mount, updating only a file's access time also changes its modification time on the server. On macOS this is very visible: pressing Space in Finder to Quick Look a file immediately changes its "Date Modified" (macfuse/macfuse#1157). The same bug was reported for FreeBSD (and macOS) in #282.

Minimal reproducer:

$ touch -t 202001010000 f      # atime = mtime = 2020-01-01
$ touch -a f                   # update atime only
$ stat -f 'atime=%Sa  mtime=%Sm' f

On a local file system mtime stays at 2020-01-01; on sshfs it becomes the current time.

Root cause

When only one of the two times is being changed, libfuse's high-level fuse_lib_setattr() passes the other one as { .tv_sec = 0, .tv_nsec = UTIME_OMIT } (and "now" as { .tv_sec = 0, .tv_nsec = UTIME_NOW }).

f045211 ("Make utimens(NULL) work correctly", #130, fixing #112) made sshfs_utimens() treat tv_sec == 0 as "now". That fixed UTIME_NOW, but UTIME_OMIT also has tv_sec == 0, so an omitted time is replaced with the current time. Because SSH_FILEXFER_ATTR_ACMODTIME always sets both atime and mtime, the server's mtime is overwritten.

Tracing the arguments (temporary debug print in sshfs_utimens(), macOS 27 + macFUSE 5.3.3) for utimensat(AT_FDCWD, f, {{0, UTIME_NOW}, {0, UTIME_OMIT}}, 0):

in atime={1789113909,180150000} mtime={0,-2} (UTIME_OMIT=-2) -> SETSTAT atime=1789113909 mtime=1789113909

@bfleischer also noted in macfuse/macfuse#1157 that the LoopbackFS reference file system does not show this, which points at sshfs.

Fix

Decide by tv_nsec instead of guessing from tv_sec:

  • UTIME_OMIT: send the time's current value, read with sshfs_getattr() (only when a time is actually omitted)
  • UTIME_NOW: send the current time (keeps the utimensat(NULL) not handled correctly #112 fix)
  • both omitted: send nothing
  • otherwise: send the given tv_sec

SFTP v3 cannot set just one of the two times, so reading the current value first is unavoidable; a concurrent change to the omitted time between the read and the SETSTAT would be lost.

Testing

  • New tst_utimens_omit in test/test_sshfs.py: set both times 1000 s in the past, run touch -a, assert that mtime is unchanged. touch -a passes UTIME_OMIT for mtime via utimensat().
  • The pytest suite skips on macOS (it needs fusermount), so I called tst_utimens, tst_utimens_now, tst_utimens_omit, tst_create and tst_open_write directly against a localhost: mount using the same cache options as test_sshfs(): with this patch 5/5 pass; current master fails only tst_utimens_omit.
  • Against a remote Linux/OpenSSH server: an atime-only update via utimensat() and via setattrlist(ATTR_CMN_ACCTIME) now keeps mtime; touch, touch -m -t, touch -t and creating a new file behave as before (no 1970 timestamps).
  • Built with clang and the strict-warnings CI flags: no new warnings. I have not run the Linux CI jobs locally.

Fixes #282
Refs: #112, #130, macfuse/macfuse#1157

🤖 Generated with Claude Code

libfuse passes a time that should be left unchanged as
{ tv_sec = 0, tv_nsec = UTIME_OMIT }. Since f045211 ("Make utimens(NULL)
work correctly") sshfs_utimens() only looked at tv_sec and replaced 0 with
the current time, so an atime-only update overwrote the file's mtime on
the server. On macOS this happens whenever Finder previews a file with
Quick Look.

SSH_FILEXFER_ATTR_ACMODTIME always sets both times, so when one of them is
UTIME_OMIT fetch the current attributes and send its current value, map
UTIME_NOW to the current time, and skip the request if both are omitted.

Add a regression test that updates only the atime with touch -a.

Refs: libfuse#282

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Yike-Ye
Yike-Ye force-pushed the fix/utimens-utime-omit branch from 1b828b6 to 5195b4c Compare September 11, 2026 08:24
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.

mtime adjusted when atime should be adjusted on some bsd instances

1 participant