Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions allowedpaths/context_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026-present Datadog, Inc.

package allowedpaths

import (
"context"
"io"
"sync"
)

// contextFile wraps an io.ReadWriteCloser and ensures Close is called when
// the associated context expires. This acts as a safety backstop to prevent
// file descriptor leaks if a caller omits an explicit Close call. Callers
// that already defer Close (the common case) are unaffected — the underlying
// file is closed at most once regardless of how many times Close is invoked.
type contextFile struct {
io.ReadWriteCloser
closeOnce sync.Once
stopOnce sync.Once
stopped chan struct{}
}

// WithContextClose wraps f so that f.Close() is guaranteed to be called when
// ctx is done, in addition to any explicit Close calls made by the caller.
// The underlying file is closed at most once.
//
// A background goroutine waits for either ctx cancellation or an explicit
// Close call. If the context expires first the file is closed immediately and
// the goroutine exits. If Close is called first the goroutine is signalled to
// stop and exits without touching the file again.
func WithContextClose(ctx context.Context, f io.ReadWriteCloser) io.ReadWriteCloser {
cf := &contextFile{
ReadWriteCloser: f,
stopped: make(chan struct{}),
}
go func() {
select {
case <-ctx.Done():
cf.closeOnce.Do(func() {
cf.ReadWriteCloser.Close() //nolint:errcheck
})
case <-cf.stopped:
// explicitly closed; goroutine exits cleanly
}
}()
return cf
}

// Close closes the underlying file and signals the background goroutine to
// exit. Safe to call multiple times; the file is closed at most once.
func (cf *contextFile) Close() error {
// Signal the goroutine to exit before closing, so it cannot race to
// close the file after we return.
cf.stopOnce.Do(func() {
close(cf.stopped)
})
var err error
cf.closeOnce.Do(func() {
err = cf.ReadWriteCloser.Close()
})
return err
}
Loading
Loading