Skip to content

create: do not archive an atime we caused ourselves, fixes #6194 - #10111

Merged
ThomasWaldmann merged 2 commits into
borgbackup:masterfrom
ThomasWaldmann:atime-6194
Aug 19, 2026
Merged

create: do not archive an atime we caused ourselves, fixes #6194#10111
ThomasWaldmann merged 2 commits into
borgbackup:masterfrom
ThomasWaldmann:atime-6194

Conversation

@ThomasWaldmann

@ThomasWaldmann ThomasWaldmann commented Aug 14, 2026

Copy link
Copy Markdown
Member

borg has to open() a fs item before it can fstat() it. On platforms without O_NOATIME
support (or when we are not allowed to use it), that open() may already have updated the
atime - and then we archive an atime that we caused ourselves, instead of the one the item
had before borg touched it.

stat_update_check() now returns a os.stat_result look-alike (StatOrigAtime) that has
everything from the fd-based stat, but the atime from the stat we did before opening the
item - as suggested in the issue. This is only done if the atime actually changed between
the two stat calls, so when O_NOATIME worked, the plain stat result is returned as before
(no proxy object, no overhead).

Doing this in stat_update_check() covers all its call sites at once: regular files, fifos,
devices and both directory opens.

Note: this only changes what we archive - without O_NOATIME we still can not avoid
updating the atime in the source filesystem.

Tests:

  • test_atime_open_updates_atime simulates a platform where the open() already updates the
    atime (by reading a byte in a monkeypatched os_open) and checks that the archived atime is
    still the one from before. It fails without the fix.
  • unit tests for stat_update_check(): atime fixup, unchanged atime (returns the stat result
    as is) and the two race condition checks, which had no test coverage.

2nd commit: test_atime: do not create a hardlink

create_test_files creates input/hardlink as a hard link to input/file1, so both share one
inode and anything touching one of them also changes the atime of the other. That is not what
test_atime is about and it made the test fail on cygwin (part of #7218):

  • At extraction time, borg creates the hard link via os.link(). On cygwin, os.link() updates
    the atime of the file it links to, at least if that file has the x bit set - and
    create_test_files does chmod(input/file1, 0o4755). Measured: mode 0o644 keeps the atime,
    0o755 and 0o4755 lose it. So the atime borg had correctly restored for input/file1 was
    overwritten by "now" again, and the test saw the wrong value. Tracing the extraction confirms
    that set_times() is called exactly once for input/file1, with the correct atime, and that
    the very next os.link() clobbers it.
  • Independently of that: while backing up, borg has to open() input/file1, which updates the
    atime of the shared inode. When borg then gets to input/hardlink, that already changed atime
    is what the stat before the open sees, so stat_update_check() can not recognize it as one we
    caused ourselves and archives it. The test never looks at input/hardlink, so this did not
    make it fail, but hardlinks and atime testing simply do not mix well.

So the test now just does not create the hardlink.

Note for reviewers: on cygwin it is this 2nd commit that makes test_atime pass - the 1st commit
does not change anything there, because cygwin does not show the atime update in the fstat()
right after the open(), so the fixup never triggers. The 1st commit is for the platforms where
it does.

Testing:

  • cygwin (win11, py3.12): test_atime failed reliably before, passes now (3 of 3 runs, both
    archiver and remote_archiver). Full extract_cmd_test.py: 59 passed, 56 skipped.
  • FreeBSD 15.1 (py3.14.7), rebased onto current master: full test suite 2643 passed, 975 skipped.

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.00%. Comparing base (6cef09f) to head (00fd667).
⚠️ Report is 17 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #10111      +/-   ##
==========================================
+ Coverage   86.94%   87.00%   +0.05%     
==========================================
  Files         101      101              
  Lines       17983    17993      +10     
  Branches     2737     2738       +1     
==========================================
+ Hits        15636    15654      +18     
+ Misses       1639     1635       -4     
+ Partials      708      704       -4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@ThomasWaldmann
ThomasWaldmann force-pushed the atime-6194 branch 2 times, most recently from 7e62246 to 9c3a5ef Compare August 14, 2026 20:35
ThomasWaldmann and others added 2 commits August 19, 2026 17:50
…#6194

borg has to open() a fs item before it can fstat() it. On platforms
without O_NOATIME support (or when we are not allowed to use it), that
open() may already have updated the atime - and then we would archive
that atime instead of the one the item had before borg touched it.

stat_update_check() now returns a stat result look-alike that has the
atime from the stat we did before opening the item, if the atime changed
between the two stat calls. If it did not change (e.g. O_NOATIME worked),
the fd-based stat result is returned as before.
create_test_files creates input/hardlink as a hard link to input/file1, so both
share one inode and anything touching one of them also changes the atime of the
other. That is not what test_atime is about, but it made the test fail on cygwin:

At extraction time, borg creates the hard link via os.link(). On cygwin, os.link()
updates the atime of the file it links to (at least if that file has the x bit set,
as input/file1 has here). So it overwrote the atime borg had correctly restored for
input/file1 just before and the test then saw "now" instead of the expected atime.

Also, while backing up, borg has to open() input/file1 and that updates the atime of
the shared inode. When borg then gets to input/hardlink, that already changed atime
is what the stat before the open sees, so stat_update_check() can not recognize it
as one we caused ourselves and archives it. The test does not look at input/hardlink,
so this did not make it fail, but it is another reason why hardlinks and atime
testing do not mix well.

On cygwin, test_atime failed reliably before this and passes now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ThomasWaldmann
ThomasWaldmann merged commit adbbd2f into borgbackup:master Aug 19, 2026
32 of 48 checks passed
@ThomasWaldmann
ThomasWaldmann deleted the atime-6194 branch August 19, 2026 20:56
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.

1 participant