chore: implement process-safe temp file cleanup (#9241)

Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: knqyf263 <knqyf263@users.noreply.github.com>
This commit is contained in:
Teppei Fukuda
2025-07-28 17:49:03 +04:00
committed by GitHub
parent 6095984d53
commit 8f5b56005a
20 changed files with 327 additions and 23 deletions

View File

@@ -41,6 +41,9 @@ func run() error {
return nil
}
// Ensure cleanup on exit
defer commands.Cleanup()
// Set up signal handling for graceful shutdown
ctx := commands.NotifyContext(context.Background())

View File

@@ -35,3 +35,16 @@ func mapSet(m dsl.Matcher) {
m.Match(`map[$x]struct{}`).
Report("use github.com/aquasecurity/trivy/pkg/set.Set instead of map.")
}
// Enforce usage of x/os package for temporary file operations
func tempFileOps(m dsl.Matcher) {
m.Match(`os.CreateTemp($*args)`).
Where(!m.File().Name.Matches(`.*_test\.go$`)).
Suggest(`xos.CreateTemp($args)`).
Report("use github.com/aquasecurity/trivy/pkg/x/os.CreateTemp instead of os.CreateTemp for process-safe temp file cleanup")
m.Match(`os.MkdirTemp($*args)`).
Where(!m.File().Name.Matches(`.*_test\.go$`)).
Suggest(`xos.MkdirTemp($args)`).
Report("use github.com/aquasecurity/trivy/pkg/x/os.MkdirTemp instead of os.MkdirTemp for process-safe temp file cleanup")
}

View File

@@ -7,6 +7,7 @@ import (
"syscall"
"github.com/aquasecurity/trivy/pkg/log"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
// NotifyContext returns a context that is canceled when SIGINT or SIGTERM is received.
@@ -26,7 +27,10 @@ func NotifyContext(parent context.Context) context.Context {
log.Info("Received signal, attempting graceful shutdown...")
log.Info("Press Ctrl+C again to force exit")
// TODO: Add any necessary cleanup logic here
// Perform cleanup
if err := Cleanup(); err != nil {
log.Debug("Failed to clean up temporary files", log.Err(err))
}
// Clean up signal handling
// After calling stop(), a second signal will cause immediate termination
@@ -35,3 +39,8 @@ func NotifyContext(parent context.Context) context.Context {
return ctx
}
// Cleanup performs cleanup tasks before Trivy exits
func Cleanup() error {
return xos.Cleanup()
}

View File

@@ -20,6 +20,7 @@ import (
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
var (
@@ -198,7 +199,7 @@ func (p *Parser) parseInnerJar(zf *zip.File, rootPath string) ([]ftypes.Package,
return nil, nil, xerrors.Errorf("unable to open %s: %w", zf.Name, err)
}
f, err := os.CreateTemp("", "inner")
f, err := xos.CreateTemp("", "jar-inner-")
if err != nil {
return nil, nil, xerrors.Errorf("unable to create a temp file: %w", err)
}

View File

@@ -17,6 +17,7 @@ import (
"golang.org/x/xerrors"
xhttp "github.com/aquasecurity/trivy/pkg/x/http"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
var ErrSkipDownload = errors.New("skip download")
@@ -36,7 +37,7 @@ type Auth struct {
// DownloadToTempDir downloads the configured source to a temp dir.
func DownloadToTempDir(ctx context.Context, src string, opts Options) (string, error) {
tempDir, err := os.MkdirTemp("", "trivy-download")
tempDir, err := xos.MkdirTemp("", "download-")
if err != nil {
return "", xerrors.Errorf("failed to create a temp dir: %w", err)
}

View File

@@ -10,6 +10,7 @@ import (
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/mapfs"
xos "github.com/aquasecurity/trivy/pkg/x/os"
"github.com/aquasecurity/trivy/pkg/x/sync"
)
@@ -20,7 +21,7 @@ type CompositeFS struct {
}
func NewCompositeFS() (*CompositeFS, error) {
tmpDir, err := os.MkdirTemp("", "analyzer-fs-*")
tmpDir, err := xos.MkdirTemp("", "analyzer-composite-")
if err != nil {
return nil, xerrors.Errorf("unable to create temporary directory: %w", err)
}
@@ -35,7 +36,7 @@ func NewCompositeFS() (*CompositeFS, error) {
func (c *CompositeFS) CopyFileToTemp(opener Opener, _ os.FileInfo) (string, error) {
// Create a temporary file to which the file in the layer will be copied
// so that all the files will not be loaded into memory
f, err := os.CreateTemp(c.dir, "file-*")
f, err := xos.CreateTemp("", "analyzer-file-")
if err != nil {
return "", xerrors.Errorf("create temp error: %w", err)
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
func init() {
@@ -261,7 +262,7 @@ func packageProvidedByVendor(pkg *rpmdb.PackageInfo) bool {
}
func writeToTempFile(rc io.Reader) (string, error) {
tmpDir, err := os.MkdirTemp("", "rpm")
tmpDir, err := xos.MkdirTemp("", "rpmdb-")
if err != nil {
return "", xerrors.Errorf("failed to create a temp dir: %w", err)
}

View File

@@ -29,6 +29,7 @@ import (
"github.com/aquasecurity/trivy/pkg/semaphore"
trivyTypes "github.com/aquasecurity/trivy/pkg/types"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
const artifactVersion = 1
@@ -64,7 +65,7 @@ func NewArtifact(img types.Image, c cache.ArtifactCache, opt artifact.Option) (a
return nil, xerrors.Errorf("config analyzer group error: %w", err)
}
cacheDir, err := os.MkdirTemp("", "layers")
cacheDir, err := xos.MkdirTemp("", "image-layers-")
if err != nil {
return nil, xerrors.Errorf("failed to create a cache layers temp dir: %w", err)
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/aquasecurity/trivy/pkg/oci"
"github.com/aquasecurity/trivy/pkg/remote"
"github.com/aquasecurity/trivy/pkg/types"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
var errNoSBOMFound = xerrors.New("remote SBOM not found")
@@ -88,9 +89,9 @@ func (a Artifact) parseReferrer(ctx context.Context, repo string, desc v1.Descri
const fileName string = "referrer.sbom"
repoName := fmt.Sprintf("%s@%s", repo, desc.Digest)
tmpDir, err := os.MkdirTemp("", "trivy-sbom-*")
tmpDir, err := xos.MkdirTemp("", "sbom-referrer-")
if err != nil {
return artifact.Reference{}, xerrors.Errorf("mkdir temp error: %w", err)
return artifact.Reference{}, xerrors.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
@@ -133,7 +134,7 @@ func (a Artifact) inspectRekorSBOMAttestation(ctx context.Context) (artifact.Ref
return artifact.Reference{}, xerrors.Errorf("failed to retrieve SBOM attestation: %w", err)
}
f, err := os.CreateTemp("", "sbom-*")
f, err := xos.CreateTemp("", "sbom-attestation-")
if err != nil {
return artifact.Reference{}, xerrors.Errorf("failed to create a temporary file: %w", err)
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/aquasecurity/trivy/pkg/fanal/artifact/local"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/fanal/walker"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
var (
@@ -93,7 +94,7 @@ func tryRemoteRepo(target string, c cache.ArtifactCache, w Walker, artifactOpt a
}
func cloneRepo(u *url.URL, artifactOpt artifact.Option) (string, error) {
tmpDir, err := os.MkdirTemp("", "trivy-remote-repo")
tmpDir, err := xos.MkdirTemp("", "git-clone-")
if err != nil {
return "", xerrors.Errorf("failed to create a temp dir: %w", err)
}

View File

@@ -29,6 +29,7 @@ import (
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/fanal/types"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
const (
@@ -130,7 +131,7 @@ func ContainerdImage(ctx context.Context, imageName string, opts types.ImageOpti
img := imgs[0]
f, err := os.CreateTemp("", "fanal-containerd-*")
f, err := xos.CreateTemp("", "containerd-export-")
if err != nil {
return nil, cleanup, xerrors.Errorf("failed to create a temporary file: %w", err)
}

View File

@@ -2,8 +2,8 @@ package daemon
const (
testContextHost = "npipe:////./pipe/test_docker_engine"
// Test socket paths for Windows systems
testFlagHost = "npipe:////./pipe/flag_docker"
testEnvHost = "npipe:////./pipe/env_docker"
)
testFlagHost = "npipe:////./pipe/flag_docker"
testEnvHost = "npipe:////./pipe/env_docker"
)

View File

@@ -7,6 +7,8 @@ import (
"github.com/docker/docker/client"
"github.com/google/go-containerregistry/pkg/name"
"golang.org/x/xerrors"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
// DockerImage implements v1.Image by extending daemon.Image.
@@ -56,7 +58,7 @@ func DockerImage(ref name.Reference, host string) (Image, func(), error) {
return nil, cleanup, xerrors.Errorf("unable to get history (%s): %w", imageID, err)
}
f, err := os.CreateTemp("", "fanal-*")
f, err := xos.CreateTemp("", "docker-export-")
if err != nil {
return nil, cleanup, xerrors.Errorf("failed to create a temporary file: %w", err)
}

View File

@@ -14,6 +14,8 @@ import (
dimage "github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"golang.org/x/xerrors"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
var (
@@ -130,7 +132,7 @@ func PodmanImage(ref, host string) (Image, func(), error) {
return nil, cleanup, xerrors.Errorf("unable to inspect the image (%s): %w", ref, err)
}
f, err := os.CreateTemp("", "fanal-*")
f, err := xos.CreateTemp("", "podman-export-")
if err != nil {
return nil, cleanup, xerrors.Errorf("failed to create a temporary file: %w", err)
}

View File

@@ -9,6 +9,7 @@ import (
"golang.org/x/xerrors"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
// cachedFile represents a file cached in memory or storage according to the file size.
@@ -37,7 +38,7 @@ func (o *cachedFile) Open() (xio.ReadSeekCloserAt, error) {
o.once.Do(func() {
// When the file is large, it will be written down to a temp file.
if o.size >= defaultSizeThreshold {
f, err := os.CreateTemp("", "fanal-*")
f, err := xos.CreateTemp("", "cached-file-")
if err != nil {
o.err = xerrors.Errorf("failed to create the temp file: %w", err)
return

View File

@@ -12,6 +12,7 @@ import (
"github.com/aquasecurity/trivy-kubernetes/pkg/artifacts"
"github.com/aquasecurity/trivy/pkg/log"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
var r = regexp.MustCompile("[\\\\/:*?<>]")
@@ -22,7 +23,7 @@ func generateTempFileByArtifact(artifact *artifacts.Artifact, tempDir string) (s
// removes characters not permitted in file/directory names on Windows
filename = filenameWindowsFriendly(filename)
}
file, err := os.CreateTemp(tempDir, filename)
file, err := xos.CreateTemp(tempDir, filename)
if err != nil {
return "", xerrors.Errorf("failed to create temporary file: %w", err)
}
@@ -45,7 +46,7 @@ func generateTempFileByArtifact(artifact *artifacts.Artifact, tempDir string) (s
// generateTempDir creates a directory with yaml files generated from kubernetes artifacts
// returns a directory name, a map for mapping a temp target file to k8s artifact and error
func generateTempDir(arts []*artifacts.Artifact) (string, map[string]*artifacts.Artifact, error) {
tempDir, err := os.MkdirTemp("", "trivyk8s*")
tempDir, err := xos.MkdirTemp("", "k8s-scan-")
if err != nil {
return "", nil, xerrors.Errorf("failed to create temp directory: %w", err)
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/aquasecurity/trivy/pkg/remote"
"github.com/aquasecurity/trivy/pkg/version/doc"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xos "github.com/aquasecurity/trivy/pkg/x/os"
)
const (
@@ -174,7 +175,7 @@ func (a *Artifact) download(ctx context.Context, layer v1.Layer, fileName, dir s
defer bar.Finish()
// https://github.com/hashicorp/go-getter/issues/326
tempDir, err := os.MkdirTemp("", "trivy")
tempDir, err := xos.MkdirTemp("", "oci-download-")
if err != nil {
return xerrors.Errorf("failed to create a temp dir: %w", err)
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
"github.com/aquasecurity/trivy/pkg/version"
xos "github.com/aquasecurity/trivy/pkg/x/os"
rpcCache "github.com/aquasecurity/trivy/rpc/cache"
rpcScanner "github.com/aquasecurity/trivy/rpc/scanner"
)
@@ -189,7 +190,7 @@ func (w dbWorker) update(ctx context.Context, appVersion, dbDir string,
}
func (w dbWorker) hotUpdate(ctx context.Context, dbDir string, dbUpdateWg, requestWg *sync.WaitGroup, opt types.RegistryOptions) error {
tmpDir, err := os.MkdirTemp("", "db")
tmpDir, err := xos.MkdirTemp("", "db-worker-")
if err != nil {
return xerrors.Errorf("failed to create a temp dir: %w", err)
}

88
pkg/x/os/os.go Normal file
View File

@@ -0,0 +1,88 @@
package os
import (
"fmt"
"os"
"path/filepath"
"sync"
"sync/atomic"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/log"
)
var (
tempDirOnce = sync.OnceValues(initTempDir)
// initialized tracks whether the temp directory has been created.
// This is used by Cleanup() to avoid creating a directory just to delete it.
initialized atomic.Bool
)
// initTempDir initializes the process-specific temp directory
func initTempDir() (string, error) {
pid := os.Getpid()
tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("trivy-%d", pid))
if err := os.MkdirAll(tempDir, 0o755); err != nil {
return "", xerrors.Errorf("failed to create temp dir: %w", err)
}
log.Debug("Created process-specific temp directory", log.String("path", tempDir))
initialized.Store(true)
return tempDir, nil
}
// CreateTemp creates a temporary file, using the process-specific directory when dir is empty
func CreateTemp(dir, pattern string) (*os.File, error) {
// If dir is empty, use our process-specific temp directory
if dir == "" {
tempDir, err := tempDirOnce()
if err != nil {
return nil, err
}
dir = tempDir
}
return os.CreateTemp(dir, pattern) //nolint: gocritic
}
// MkdirTemp creates a temporary directory, using the process-specific directory as base when dir is empty
func MkdirTemp(dir, pattern string) (string, error) {
// If dir is empty, use our process-specific temp directory
if dir == "" {
tempDir, err := tempDirOnce()
if err != nil {
return "", err
}
dir = tempDir
}
return os.MkdirTemp(dir, pattern) //nolint: gocritic
}
// TempDir returns the process-specific temp directory path
func TempDir() string {
tempDir, err := tempDirOnce()
if err != nil {
log.Debug("Failed to get process-specific temp directory, falling back to system temp", log.Err(err))
return os.TempDir() // fallback
}
return tempDir
}
// Cleanup removes the entire process-specific temp directory
// Note: On Windows, directory deletion may fail if files are still open
func Cleanup() error {
// If temp dir was never initialized, nothing to clean up
if !initialized.Load() {
return nil
}
tempDir, err := tempDirOnce()
if err != nil || tempDir == "" {
return nil
}
log.Debug("Cleaning up temp directory", log.String("path", tempDir))
return os.RemoveAll(tempDir)
}

175
pkg/x/os/os_test.go Normal file
View File

@@ -0,0 +1,175 @@
package os
import (
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// resetForTest resets global variables for testing
func resetForTest() {
tempDirOnce = sync.OnceValues(initTempDir)
}
func TestTempDir(t *testing.T) {
resetForTest()
t.Cleanup(func() {
_ = Cleanup()
resetForTest()
})
got := TempDir()
// Should contain process ID
pid := os.Getpid()
want := filepath.Join(os.TempDir(), "trivy-"+strconv.Itoa(pid))
assert.Equal(t, want, got)
// Directory should exist
_, err := os.Stat(got)
require.NoError(t, err)
}
func TestCreateTemp(t *testing.T) {
resetForTest()
tests := []struct {
name string
pattern string
}{
{
name: "simple pattern",
pattern: "test-",
},
{
name: "empty pattern",
pattern: "",
},
{
name: "pattern with extension",
pattern: "test-*.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test with empty dir (should use process-specific dir)
file, err := CreateTemp("", tt.pattern) //nolint: usetesting
require.NoError(t, err)
t.Cleanup(func() {
_ = file.Close()
_ = Cleanup()
resetForTest()
})
// File should exist
_, err = os.Stat(file.Name())
require.NoError(t, err)
// File should be in our temp directory
pid := os.Getpid()
expectedDir := filepath.Join(os.TempDir(), "trivy-"+strconv.Itoa(pid))
assert.True(t, strings.HasPrefix(file.Name(), expectedDir))
// Test with specific dir
customDir := t.TempDir()
file2, err := CreateTemp(customDir, tt.pattern)
require.NoError(t, err)
defer file2.Close()
// File should exist and be in custom dir
_, err = os.Stat(file2.Name())
require.NoError(t, err)
assert.True(t, strings.HasPrefix(file2.Name(), customDir))
})
}
}
func TestMkdirTemp(t *testing.T) {
resetForTest()
tests := []struct {
name string
pattern string
}{
{
name: "simple pattern",
pattern: "test-",
},
{
name: "empty pattern",
pattern: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Cleanup(func() {
Cleanup()
resetForTest()
})
// Test with empty dir (should use process-specific dir)
dir, err := MkdirTemp("", tt.pattern) //nolint:usetesting
require.NoError(t, err)
// Directory should exist
_, err = os.Stat(dir)
require.NoError(t, err)
// Directory should be in our temp directory
wantParent := filepath.Join(os.TempDir(), "trivy-"+strconv.Itoa(os.Getpid()))
assert.True(t, strings.HasPrefix(dir, wantParent))
// Test with specific dir
customParent := t.TempDir()
dir2, err := MkdirTemp(customParent, tt.pattern) //nolint:usetesting
require.NoError(t, err)
// Directory should exist and be in custom parent
_, err = os.Stat(dir2)
require.NoError(t, err)
assert.True(t, strings.HasPrefix(dir2, customParent))
})
}
}
func TestCleanup(t *testing.T) {
resetForTest()
t.Cleanup(func() {
resetForTest()
})
// Create a temp file
file, err := CreateTemp("", "test-") //nolint: usetesting
require.NoError(t, err)
filename := file.Name()
require.NoError(t, file.Close())
// Directory should exist
dir := filepath.Join(os.TempDir(), "trivy-"+strconv.Itoa(os.Getpid()))
_, err = os.Stat(dir)
require.NoError(t, err)
// File should exist
_, err = os.Stat(filename)
require.NoError(t, err)
// Cleanup
err = Cleanup()
require.NoError(t, err)
// File should be gone
_, err = os.Stat(filename)
require.ErrorIs(t, err, os.ErrNotExist)
// Directory should be gone
_, err = os.Stat(dir)
assert.ErrorIs(t, err, os.ErrNotExist)
}