Skip to content

file locking: keep the log on a failed roll, fix the mutex name - #319

Merged
FreeAndNil merged 15 commits into
masterfrom
Feature/319-file-locking
Sep 22, 2026
Merged

FreeAndNil merged 15 commits into
masterfrom
Feature/319-file-locking

Conversation

@FreeAndNil

@FreeAndNil FreeAndNil commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Four findings from an audit, all in file locking. None is a vulnerability; audit da18b6fd-*
cites them for provenance.

  • f036: RollingFileAppender reported a failed rename, which a backup or antivirus agent
    holding the file without FILE_SHARE_DELETE is enough to cause, then reopened the log without
    appending and destroyed everything it held: 21 consecutive events lost in the Windows run. It
    appends now, and only that rename is retried, once per tenth of MaxFileSize of growth, so the
    file can exceed MaxFileSize while the rename keeps failing.
  • f032: FileAppender.LockingStream's recursion counter went below zero, because the footer,
    close and open paths released a lock they had failed to take, after which every later acquisition
    on that stream failed and the footer and buffered tail were dropped in silence.
  • f031: FileAppender.InterProcessLock named its mutex from the configured path before
    ConvertToFullPath ran, so a relative and an absolute spelling of one file took two mutexes and
    excluded nothing. That resolution is all it covers: symbolic links, 8.3 names and letter case
    still name differently.
  • f010: RollingFileAppender's rolling lock and FileAppender.InterProcessLock both name their
    mutex after the log path, and Unix rejects one over 255 UTF-8 bytes, which 104 characters can
    already exceed, throwing out of ActivateOptions before anything is written. Such names are
    hashed now; Windows has no limit and keeps its names.

The Local\ prefix, per-user scoping and ACL the scan proposed are deliberately absent: measured on
Windows, cross-session exclusion never existed, and a bare Global\ prefix throws for whichever
process starts second.

Two more came out of review, both the same failure one step from the fix: a pending rename was
never retried in Date mode, because the check sat inside the size-rolling guard, and the rollover
at startup still truncated the file it could not move when AppendToFile was false.

Also here: a pre-Vista branch in EventLogAppender that could not be false, fixtures that keep no
state between tests, and three older ones that stopped leaving temp files behind.

- A failed rename was reported, then the file reopened without appending, which
  destroyed it. A backup agent holding a read handle is enough. It appends now,
  and only that rename is retried, once per MaxFileSize of growth, so the
  backups are never rotated twice and a retry that succeeds keeps the generation
  it recovers.
- The footer, close and open paths released the file lock even when acquiring it
  had failed. Only what was taken is released now.

audit da18b6f-f036, da18b6f-f032
@FreeAndNil FreeAndNil changed the title keep the log when a roll cannot rename it #319 keep the log when a roll cannot rename it Sep 11, 2026
- InterProcessLock named its mutex before the path was resolved, so a relative
  and an absolute spelling of one file took two mutexes and excluded nothing.
- A name over 255 characters throws on Unix, out of ActivateOptions, so a deep
  log path took the appender down. Those are hashed now. Windows has no limit,
  measured, so its names are left alone and keep excluding older versions.
- Both mutexes take their name from one helper, which carries why there is no
  ACL, no Global\ prefix and no user component.

audit da18b6f-f031, da18b6f-f010
Windows 7 SP1 is the floor for the net462 build this file compiles into, so the
version test could not fail and the 32766 constant behind it was dead. The
surviving constant keeps its measured value and loses the superseded lore.
@FreeAndNil FreeAndNil changed the title keep the log when a roll cannot rename it keep the log when a roll cannot rename it, and name the file lock after the resolved path Sep 14, 2026
@FreeAndNil FreeAndNil changed the title keep the log when a roll cannot rename it, and name the file lock after the resolved path file locking: keep the log on a failed roll, fix the mutex name Sep 14, 2026
@FreeAndNil
FreeAndNil marked this pull request as ready for review September 14, 2026 20:41
@FreeAndNil FreeAndNil added this to the 3.5.0 milestone Sep 14, 2026
Comment thread src/log4net.Tests/Appender/FileAppenderMutexNameTest.cs Outdated
Comment thread src/log4net/Appender/FileAppender.cs
Comment thread src/log4net/Appender/FileAppender.cs Outdated
Comment thread src/log4net/Appender/FileAppender.cs Outdated
Comment thread src/log4net/Appender/FileAppender.cs Outdated
Comment thread src/log4net/Appender/RollingFileAppender.cs Outdated
- Release the file lock with _stream?.ReleaseLock() wherever the acquire was
  already null-conditional.
- One CountingWriter property instead of five QuietWriter casts, through
  EnsureIs<> so a wrong writer names itself. The two sites after a reopen keep
  their null check: a refused lock leaves no writer there.
- The new fixtures keep no state between tests: AutoTempFolder from
  PeanutButter.Utils and a per-test error handler. Its namespace collides with
  our test helper Utils, which is internal TestUtils now.
- SmtpPickupDirAppenderTest kept its pickup directory in a fixture field, shared
  by every test, and created it inside the build output.
- FileAppenderTest leaked two files per run: Path.GetTempFileName creates them
  and nothing deleted them.
- PatternStringTest tracked its config file by hand to delete it in a finally.

All three use AutoTempFolder now, which disposes what it made.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Date-only and startup rollover failures can still bypass recovery or truncate retained log data.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Improves file-locking reliability and rollover recovery following four audit findings.

Changes:

  • Preserves logs after failed rollovers and corrects lock-release handling.
  • Derives mutex names from resolved paths and hashes oversized Unix names.
  • Adds regression tests, temporary-directory cleanup, changelogs, and contributor guidance.
File summaries
File Description
CLAUDE.md Extends coding and testing guidance.
src/Directory.Build.props Defines the PeanutButter.Utils version.
src/log4net/Appender/EventLogAppender.cs Removes obsolete event-log size branching.
src/log4net/Appender/FileAppender.cs Fixes lock accounting and mutex naming.
src/log4net/Appender/RollingFileAppender.cs Adds failed-roll preservation and retries.
src/log4net/Util/SystemInfo.cs Adds Windows runtime detection.
src/log4net.Tests/Appender/FileAppenderMutexNameTest.cs Tests mutex path and length handling.
src/log4net.Tests/Appender/FileAppenderTest.cs Uses isolated temporary files.
src/log4net.Tests/Appender/LockingStreamTest.cs Tests lock recursion and failed acquisition.
src/log4net.Tests/Appender/RollingFileAppenderRollFailureTest.cs Tests rollover failure recovery.
src/log4net.Tests/Appender/SmtpPickupDirAppenderTest.cs Isolates pickup directories per test.
src/log4net.Tests/Context/LogicalThreadContextTest.cs Updates renamed test utilities.
src/log4net.Tests/Context/ThreadContextTest.cs Updates renamed test utilities.
src/log4net.Tests/Layout/PatternLayoutTest.cs Updates renamed test utilities.
src/log4net.Tests/TestUtils.cs Renames and restricts the test utility class.
src/log4net.Tests/Util/PatternStringTest.cs Uses an automatically cleaned temporary folder.
src/log4net.Tests/log4net.Tests.csproj Adds PeanutButter.Utils.
src/changelog/3.5.0/319-lock-level-underflow.xml Documents lock-counter recovery.
src/changelog/3.5.0/319-mutex-name-length.xml Documents long mutex-name handling.
src/changelog/3.5.0/319-mutex-resolved-path.xml Documents resolved-path mutex naming.
src/changelog/3.5.0/319-rollover-keeps-events.xml Documents failed-roll preservation.
Review details
  • Files reviewed: 21/21 changed files
  • Comments generated: 10
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/log4net/Appender/RollingFileAppender.cs
Comment thread src/log4net/Appender/FileAppender.cs Outdated
Comment thread src/log4net/Appender/RollingFileAppender.cs Outdated
Comment thread src/changelog/3.5.0/319-lock-level-underflow.xml Outdated
Comment thread src/changelog/3.5.0/319-mutex-name-length.xml Outdated
Comment thread src/changelog/3.5.0/319-mutex-resolved-path.xml Outdated
Comment thread src/changelog/3.5.0/319-rollover-keeps-events.xml Outdated
Comment thread src/log4net.Tests/Appender/FileAppenderMutexNameTest.cs
Comment thread src/log4net.Tests/Appender/LockingStreamTest.cs
Comment thread src/log4net/Appender/RollingFileAppender.cs Outdated
Comment thread src/Directory.Build.props Outdated
104 characters can be 304 bytes, and Unix rejects by bytes.
- Date mode never retried: the check sat inside the size-rolling guard.
- The startup roll fell through to AppendToFile and truncated the file.
CLAUDE.md: a logged raw string keeps its newlines, and only the first
line carries the prefix.
- the message printed 0 bytes of growth where the threshold was really 1
- the retry-count assertion allowed 2 to 19, so a wrong threshold passed it.
  Two attempts on Unix, three on Windows, and the range says which is which
- record what the mutex suffix collision costs
- drop the empty else, rewrap a long changelog line

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Failed startup rollover state can still be corrupted, AppendToFile can be mutated, and one changelog entry misstates the data-loss impact.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
Resolved since last review (10)
Previously missed (1)

In code that hasn't changed since last review

Low severity Correctly describe data loss from stream counter underflow

src/​changelog/​3.5.0/​319-lock-level-underflow.xml:12

This contradicts both the failure mode and the PR description: after the footer or close path underflowed the existing stream's counter, later acquisitions on that stream failed, so the footer and buffered tail could be dropped. The appender only reopened automatically when no writer had been installed during the open path. Please describe the actual data-loss impact rather than stating that nothing was lost.

Comment thread src/log4net/Appender/RollingFileAppender.cs
Comment thread src/log4net/Appender/RollingFileAppender.cs
- ExistingInit rolled again for AppendToFile=false, moving the file out from
  under the pending rename, so the first retry archived the new file under the
  previous date.
- The forced append reached AppendToFile through base.OpenFile and replaced the
  configured value. Every successful roll did this, not just a failed one, so
  RollingCombinedWithPreserveExtension now asserts it too.
- The changelog entries now describe what a user sees.
- TheDeadlineCoversTheWholeSendAndNotOneOperation timed the whole send against a
  5 s bound, and failed on a slow Windows runner at 6289 ms
- the fake transport now records which call found the token cancelled

@gdziadkiewicz gdziadkiewicz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the improvements, Jan! LGTM

@FreeAndNil
FreeAndNil merged commit c9edbc9 into master Sep 22, 2026
3 checks passed
@FreeAndNil
FreeAndNil deleted the Feature/319-file-locking branch September 22, 2026 10:24
FreeAndNil added a commit that referenced this pull request Sep 28, 2026
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.

4 participants