Skip to content

wal: close files on Open and Create error paths - #21766

Open
SebTardif wants to merge 1 commit into
etcd-io:mainfrom
SebTardif:fix-wal-resource-leaks
Open

SebTardif wants to merge 1 commit into
etcd-io:mainfrom
SebTardif:fix-wal-resource-leaks

Conversation

@SebTardif

@SebTardif SebTardif commented May 20, 2026

Copy link
Copy Markdown
Contributor

Fix resource leaks in WAL Open and Create error handling.

fileutil.purgeFile already closes the flock when os.Remove fails on current main, so that hunk is no longer in this PR.

1. wal.Open: FD + goroutine leak when OpenDir fails

Call chain: OpenopenAtIndex (succeeds, allocating file locks in w.locks, a filePipeline goroutine in w.fp, and a pre-allocated temp file) → fileutil.OpenDir(w.dir) (fails)

Trigger: Any filesystem error on the WAL directory between openAtIndex completing and OpenDir (e.g., directory removed by concurrent process, permission change, or FD exhaustion).

Leak: w.locks (locked WAL segment FDs), w.fp (goroutine + pre-allocated temp file) are never released. The return nil, err discards w without calling w.Close().

Fix: Call w.Close() before returning the error.

2. wal.Create: Locked FD leak on early error paths

Call chain: CreatecreateNewWALFile (succeeds, returns locked file f) → f.Seek / Preallocate / newFileEncoder / saveCrc / encode / SaveSnapshot (any fails)

Trigger: Disk full during Preallocate, I/O error during seek, or encoding failure.

Leak: The locked file f is never closed. While defer os.RemoveAll(tmpdirpath) removes the file from disk, the FD and advisory lock remain held.

Fix: Use a named return and a deferred closure that calls f.Close() when returning an error. Duplicate close after renameWAL takes ownership is harmless per os.File.Close.

@k8s-ci-robot

Copy link
Copy Markdown

Hi @SebTardif. Thanks for your PR.

I'm waiting for a etcd-io member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@serathius

Copy link
Copy Markdown
Member

@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.55556% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.00%. Comparing base (53363b5) to head (702fd61).

Files with missing lines Patch % Lines
server/storage/wal/wal.go 55.55% 4 Missing ⚠️
Additional details and impacted files
Files with missing lines Coverage Δ
server/storage/wal/wal.go 70.93% <55.55%> (+0.02%) ⬆️

... and 23 files with indirect coverage changes

@@            Coverage Diff             @@
##             main   #21766      +/-   ##
==========================================
- Coverage   73.03%   73.00%   -0.04%     
==========================================
  Files         448      448              
  Lines       31579    31583       +4     
==========================================
- Hits        23065    23057       -8     
- Misses       8511     8523      +12     
  Partials        3        3              

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 53363b5...702fd61. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@SebTardif

Copy link
Copy Markdown
Contributor Author

/retest pull-etcd-e2e-amd64

@SebTardif

Copy link
Copy Markdown
Contributor Author

/retest-required

@silentred

Copy link
Copy Markdown
Member

#21779 added a test for filelock leak in purge.go

@serathius

Copy link
Copy Markdown
Member

ping @ahrtr for review

Comment thread server/storage/wal/wal.go Outdated
@@ -174,6 +180,7 @@ func Create(lg *zap.Logger, dirpath string, metadata []byte) (*WAL, error) {
return nil, err
}

closeF = false // renameWAL takes ownership of f via w

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should we move this line after the defer statement (line 196)? otherwise, the file won't be closed via w.cleanupWAL

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Moved closeF=false to after the cleanupWAL defer registration in c4038b12c. Now if renameWAL fails, our defer still closes f directly. Once the cleanupWAL defer is in place, it takes over and we disable our defer.

Comment thread server/storage/wal/wal.go
@@ -97,7 +97,7 @@ type WAL struct {
// Create creates a WAL ready for appending records. The given metadata is
// recorded at the head of each WAL file, and can be retrieved with ReadAll
// after the file is Open.
func Create(lg *zap.Logger, dirpath string, metadata []byte) (*WAL, error) {
func Create(lg *zap.Logger, dirpath string, metadata []byte) (_ *WAL, err error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why not just using retErr error in return? it's easy to know it's return error and it won't be conflicted with errors defined in the function.

and I don't think we need this closeF := true. The f.Close() is allowed to close twice.

ref: https://pkg.go.dev/os#File.Close

@SebTardif

Copy link
Copy Markdown
Contributor Author

/retest-required

Comment thread server/storage/wal/wal.go Outdated
@@ -135,6 +135,12 @@ func Create(lg *zap.Logger, dirpath string, metadata []byte) (*WAL, error) {
)
return nil, err
}
closeF := true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@SebTardif
SebTardif force-pushed the fix-wal-resource-leaks branch from c4038b1 to b18b3bc Compare May 23, 2026 13:03
@SebTardif

Copy link
Copy Markdown
Contributor Author

Addressed review feedback: removed closeF entirely per @ahrtr and @fuweid's suggestion. The defer now simply checks if err != nil { f.Close() }. On the perr error paths, the named return err gets set via return nil, perr, so the defer still fires correctly. If cleanupWAL also closes f, the duplicate close is harmless per os.File.Close docs. Force-pushed as a single squashed commit.

@k8s-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: ahrtr, SebTardif, serathius

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@silentred

Copy link
Copy Markdown
Member

/cc @fuweid

@silentred

Copy link
Copy Markdown
Member

@SebTardif Could you please do a rebase? Thanks! I have added this PR to the agenda for the next triage meeting. https://docs.google.com/document/d/16XEGyPBisZvmmoIHSZzv__LoyOeluC5a4x353CX0SIM/edit?tab=t.xjc2zly8zbof

On Open, close the WAL if OpenDir fails so segment locks and the
filePipeline goroutine are not leaked.

On Create, use a named return so a deferred Close covers Seek,
Preallocate, encode, and snapshot errors on the locked file.

purgeFile already closes the flock on os.Remove failure on main.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@SebTardif SebTardif changed the title wal,fileutil: fix resource leaks on error paths wal: close files on Open and Create error paths Sep 21, 2026
@SebTardif
SebTardif force-pushed the fix-wal-resource-leaks branch from b18b3bc to 702fd61 Compare September 21, 2026 14:20
@SebTardif

Copy link
Copy Markdown
Contributor Author

@silentred

Could you please do a rebase?

Yes. Rebased onto current main. purgeFile already closes the flock when os.Remove fails, so that hunk dropped out. What remains is Open/Create cleanup in wal.go.

@kubernetes-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: ahrtr, SebTardif, serathius

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

6 participants