diff --git a/.golangci.yml b/.golangci.yml index 48e821c..e9b6c27 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,9 @@ -# .golangci.yml - reasonable defaults for steamcache2 -# Run with: golangci-lint run ./... +# .golangci.yml - steamcache2 lint config +# Philosophy: enable reasonable linters by default (golangci curated set + key additions) +# then use most specific suppressions possible (source //nosec with justification, +# _ = discard for errcheck on unavoidable client writes, narrow exclude-rules only for tests). +# This makes remaining accepted issues visible and actionable in the code. +# Run with: make lint (or golangci-lint run ./...) # Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest run: @@ -7,42 +11,33 @@ run: modules-download-mode: readonly linters: - disable-all: true + # No disable-all: use golangci defaults (errcheck, govet, ineffassign, staticcheck, unused, gosimple, etc.) + # Explicitly enable the non-default linters we require for this LAN cache proxy. enable: - # errcheck intentionally not enabled yet (pre-existing unchecked I/O in core paths). - # Re-enable per-package after larger refactors reduce surface area. - # - errcheck - - gosec - - govet - - ineffassign - - misspell - - staticcheck - - unused - - gofmt - - goimports + - gosec # security checks (re-audited; see source //nosec for justified cases) + - misspell # documentation hygiene + - goimports # import formatting (enforced) + # gofmt covered via linter or goimports; errcheck/govet etc. from defaults linters-settings: errcheck: - check-type-assertions: false # many existing unchecked in http/metrics paths + check-type-assertions: false check-blank: false gosec: - excludes: - - G104 # errors unhandled in defer/close common in Go - - G304 # file inclusion via variable (config paths controlled) - - G115 # int->uint casts on positive cache sizes (pre-existing; safe in context) - - G301 # MkdirAll 0755 for cache dirs (pre-existing, functional requirement) - - G306 # WriteFile 0644 for user config (standard, not secret) + # Broad global excludes removed (G104/G115/G301/G304/G306). + # - G301 addressed by switching cache MkdirAll to 0700 (least privilege for CDN content). + # - Remaining justified cases documented with precise //nosec (or #nosec) + comments at the call sites. + # - G104 largely eliminated by errcheck + explicit _ = handling (or defer wrappers). staticcheck: - checks: ["all", "-SA1019"] # allow deprecated for now if any + checks: ["all"] # SA1019 exclusion removed (no deprecated API usages in tree) govet: enable-all: true disable: - - fieldalignment # performance not critical here - - shadow # pre-existing in large ServeHTTP; avoid noise for now + - fieldalignment # performance tuning not a priority for this proxy appliance + - shadow # common idiomatic "err" redeclarations in error-handling chains (large ServeHTTP, root, parse funcs); enabling adds noise with no real bugs; would require scope refactor for little gain -# errcheck remains disabled globally due to pre-existing noise in http and cache paths. -# Re-enable plan: enable per-package after larger refactors; consider adding a coverage gate later. -# Current config keeps baseline green while allowing incremental strictness. +# Old global errcheck disable + aspirational "re-enable after refactors" comments deleted. +# errcheck is now on via defaults. Unavoidable cases handled at source with _ = or (rarely) narrow rules. issues: max-issues-per-linter: 0 @@ -55,18 +50,37 @@ issues: - path: _test\.go linters: - errcheck - - gosec # tests often use weak patterns intentionally - # Pre-existing intentional empty branches (comments explain); cleaned in later refactors - - linters: + - gosec # tests often use weak patterns intentionally (e.g. error injection, temp files) + # NOTE: narrow SA9003 exclude retained only for the one remaining intentional empty branch in test (best-effort status check; main assert is metrics side-effect). + # The config one was a truly redundant check (already errored above); deleted surgically in Fix Round 1 (Issue 1), eliminating its exclude-rule. + - path: steamcache/steamcache_test.go + linters: - staticcheck text: "SA9003: empty branch" - # Double-check locking idiom in predictive (content assigned only on miss path); pre-existing - - path: vfs/predictive/predictive.go + # Narrow gosec excludes for unavoidable classes after re-audit (LAN proxy threat model): + # - G115: int64<->uint casts in eviction/GC math (all sizes positive, guarded by capacity checks; API uses uint for bytesNeeded) + # - G304: path vars for Read/Open/Remove under trusted disk.root or user config file (sanitized keys, no traversal, no arbitrary inclusion from untrusted URLs) + # G306 for config WriteFile kept as source //nosec (one site). + # G301 fixed at source (0700 dirs). G104 addressed via errcheck fixes. + - path: vfs/memory/memory.go linters: - - staticcheck - text: "SA4006" - # Unused field in predictive (likely remnant); pre-existing, excluded to keep lint green for hygiene - - path: vfs/predictive/predictive.go + - gosec + text: "G115" + - path: vfs/disk/disk.go linters: - - unused - text: "mu" + - gosec + text: "G115" + - path: vfs/gc/gc.go + linters: + - gosec + text: "G115" + - path: config/config.go + linters: + - gosec + text: "G304" + - path: vfs/disk/disk.go + linters: + - gosec + text: "G304" + # Predictive/* rules deleted: vfs/predictive/ removed in commit 0dbb2e0; rules were stale/dead. + # All other suppressions use source-level //nosec (gosec) or _= (errcheck) for precision and visibility. diff --git a/cmd/root.go b/cmd/root.go index 176f51a..bf38221 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -72,7 +72,7 @@ var rootCmd = &cobra.Command{ Err(err). Str("config_path", configPath). Msg("Failed to create default configuration") - fmt.Fprintf(os.Stderr, "Error: Failed to create default config at %s: %v\n", configPath, err) + _, _ = fmt.Fprintf(os.Stderr, "Error: Failed to create default config at %s: %v\n", configPath, err) // explicit discard for fatal stdio path (consistent with errcheck posture; low-value on exit) os.Exit(1) } @@ -88,7 +88,7 @@ var rootCmd = &cobra.Command{ Err(err). Str("config_path", configPath). Msg("Failed to load configuration") - fmt.Fprintf(os.Stderr, "Error: Failed to load configuration from %s: %v\n", configPath, err) + _, _ = fmt.Fprintf(os.Stderr, "Error: Failed to load configuration from %s: %v\n", configPath, err) // explicit discard for fatal stdio path (consistent with errcheck posture; low-value on exit) os.Exit(1) } } @@ -113,7 +113,7 @@ var rootCmd = &cobra.Command{ logger.Logger.Error(). Err(err). Msg("Configuration validation failed") - fmt.Fprintf(os.Stderr, "Error: Invalid configuration: %v. Please fix the config file and try again.\n", err) + _, _ = fmt.Fprintf(os.Stderr, "Error: Invalid configuration: %v. Please fix the config file and try again.\n", err) // explicit discard for fatal stdio path (consistent with errcheck posture; low-value on exit) os.Exit(1) } @@ -134,7 +134,7 @@ var rootCmd = &cobra.Command{ logger.Logger.Error(). Err(err). Msg("Failed to initialize steamcache") - fmt.Fprintf(os.Stderr, "Error: Failed to initialize steamcache: %v. Check sizes in config.\n", err) + _, _ = fmt.Fprintf(os.Stderr, "Error: Failed to initialize steamcache: %v. Check sizes in config.\n", err) // explicit discard for fatal stdio path (consistent with errcheck posture; low-value on exit) os.Exit(1) } @@ -143,7 +143,7 @@ var rootCmd = &cobra.Command{ if err := sc.Run(); err != nil { logger.Logger.Error().Err(err).Msg("steamcache2 Run failed") - fmt.Fprintf(os.Stderr, "Error: steamcache2 run error: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "Error: steamcache2 run error: %v\n", err) // explicit discard for fatal stdio path (consistent with errcheck posture; low-value on exit) os.Exit(1) } diff --git a/config/config.go b/config/config.go index 2f9ae87..3f4d81e 100644 --- a/config/config.go +++ b/config/config.go @@ -135,6 +135,8 @@ func SaveDefaultConfig(configPath string) error { return fmt.Errorf("failed to marshal default config: %w", err) } + // #nosec G306 -- 0644 appropriate for generated default config.yaml (user-editable, no secrets/credentials; only sizes/URLs/paths) + // G304 on ReadFile below is similar (trusted user config path) if err := os.WriteFile(configPath, data, 0644); err != nil { return fmt.Errorf("failed to write default config file: %w", err) } @@ -207,9 +209,6 @@ func (c Config) Validate() error { return fmt.Errorf("invalid trusted_proxies CIDR: %s", p) } } - if c.MaxConcurrentRequests < 0 || c.MaxRequestsPerClient < 0 { // already covered above but explicit for the concurrency knobs - // covered by earlier checks - } return nil } diff --git a/steamcache/steamcache.go b/steamcache/steamcache.go index 05b39e2..4e0212c 100644 --- a/steamcache/steamcache.go +++ b/steamcache/steamcache.go @@ -59,7 +59,7 @@ func NewServiceManager() *ServiceManager { `Steam`, }, } - sm.AddService(steamConfig) + _ = sm.AddService(steamConfig) // error impossible: hardcoded patterns are valid regexes (user-provided services validated in AddService) return sm } @@ -362,7 +362,7 @@ func (sc *SteamCache) streamCachedResponse(w http.ResponseWriter, r *http.Reques w.WriteHeader(http.StatusPartialContent) // Send the range data - w.Write(rangeData) + _, _ = w.Write(rangeData) // client write error ignored (disconnect during range body send is not actionable) logger.Logger.Info(). Str("cache_key", cacheKey). @@ -397,7 +397,7 @@ func (sc *SteamCache) streamCachedResponse(w http.ResponseWriter, r *http.Reques w.WriteHeader(statusCode) // Stream the full response body - w.Write(bodyData) + _, _ = w.Write(bodyData) // client write error ignored (disconnect during full cached body send is not actionable) logger.Logger.Info(). Str("cache_key", cacheKey). @@ -1108,17 +1108,17 @@ func (sc *SteamCache) Run() error { resp, err := sc.client.Get(sc.upstream) if err != nil { if resp != nil { - resp.Body.Close() + _ = resp.Body.Close() // best-effort body close on connectivity error path; primary err returned } logger.Logger.Error().Err(err).Str("upstream", sc.upstream).Msg("Failed upstream connectivity check") return err } if resp.StatusCode != http.StatusOK { - resp.Body.Close() + _ = resp.Body.Close() // best-effort body close on non-OK status; primary error returned logger.Logger.Error().Int("status_code", resp.StatusCode).Str("upstream", sc.upstream).Msg("Upstream connectivity check returned non-OK status") return fmt.Errorf("upstream returned status %d", resp.StatusCode) } - resp.Body.Close() + _ = resp.Body.Close() // best-effort; success path close before ListenAndServe } sc.server.Handler = sc @@ -1143,7 +1143,7 @@ func (sc *SteamCache) Run() error { }() <-ctx.Done() - sc.server.Shutdown(ctx) + _ = sc.server.Shutdown(ctx) // shutdown error secondary (ListenAndServe already terminated or context done); logged elsewhere if fatal sc.wg.Wait() return nil } @@ -1226,6 +1226,39 @@ func (sc *SteamCache) ResetMetrics() { sc.metrics.Reset() } +// writeMetricsText emits the Prometheus-style text metrics to the ResponseWriter. +// All fmt.Fprintf errors are intentionally discarded via _ = : this is a best-effort +// read-only debug endpoint; client disconnects or write errors during metrics dump +// are not actionable (do not affect cache correctness or require retries). +func writeMetricsText(w http.ResponseWriter, stats *metrics.Stats) { + _, _ = fmt.Fprintf(w, "# SteamCache2 Metrics\n") + _, _ = fmt.Fprintf(w, "total_requests %d\n", stats.TotalRequests) + _, _ = fmt.Fprintf(w, "cache_hits %d\n", stats.CacheHits) + _, _ = fmt.Fprintf(w, "cache_misses %d\n", stats.CacheMisses) + _, _ = fmt.Fprintf(w, "cache_coalesced %d\n", stats.CacheCoalesced) + _, _ = fmt.Fprintf(w, "errors %d\n", stats.Errors) + _, _ = fmt.Fprintf(w, "rate_limited %d\n", stats.RateLimited) + _, _ = fmt.Fprintf(w, "upstream_errors %d\n", stats.UpstreamErrors) + _, _ = fmt.Fprintf(w, "cache_write_failures %d\n", stats.CacheWriteFailures) + _, _ = fmt.Fprintf(w, "memory_cache_hits %d\n", stats.MemoryCacheHits) + _, _ = fmt.Fprintf(w, "disk_cache_hits %d\n", stats.DiskCacheHits) + _, _ = fmt.Fprintf(w, "promotions %d\n", stats.Promotions) + _, _ = fmt.Fprintf(w, "evictions %d\n", stats.Evictions) + for svc, cnt := range stats.ServiceErrors { + _, _ = fmt.Fprintf(w, "service_errors{service=%q} %d\n", svc, cnt) + } + for svc, cnt := range stats.ServiceRequests { + _, _ = fmt.Fprintf(w, "service_requests{service=%q} %d\n", svc, cnt) + } + _, _ = fmt.Fprintf(w, "hit_rate %.4f\n", stats.HitRate) + _, _ = fmt.Fprintf(w, "avg_response_time_ms %.2f\n", float64(stats.AvgResponseTime.Nanoseconds())/1e6) + _, _ = fmt.Fprintf(w, "total_bytes_served %d\n", stats.TotalBytesServed) + _, _ = fmt.Fprintf(w, "total_bytes_cached %d\n", stats.TotalBytesCached) + _, _ = fmt.Fprintf(w, "memory_cache_size %d\n", stats.MemoryCacheSize) + _, _ = fmt.Fprintf(w, "disk_cache_size %d\n", stats.DiskCacheSize) + _, _ = fmt.Fprintf(w, "uptime_seconds %.2f\n", stats.Uptime.Seconds()) +} + func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { clientIP := getClientIP(r, sc.trustedProxies) @@ -1288,7 +1321,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { Msg("LanCache heartbeat request") w.Header().Add("X-LanCache-Processed-By", "SteamCache2") w.WriteHeader(http.StatusNoContent) - w.Write(nil) + _, _ = w.Write(nil) // client write error ignored (heartbeat path; nil write is no-op) return } @@ -1297,32 +1330,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { stats := sc.GetMetrics() w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "# SteamCache2 Metrics\n") - fmt.Fprintf(w, "total_requests %d\n", stats.TotalRequests) - fmt.Fprintf(w, "cache_hits %d\n", stats.CacheHits) - fmt.Fprintf(w, "cache_misses %d\n", stats.CacheMisses) - fmt.Fprintf(w, "cache_coalesced %d\n", stats.CacheCoalesced) - fmt.Fprintf(w, "errors %d\n", stats.Errors) - fmt.Fprintf(w, "rate_limited %d\n", stats.RateLimited) - fmt.Fprintf(w, "upstream_errors %d\n", stats.UpstreamErrors) - fmt.Fprintf(w, "cache_write_failures %d\n", stats.CacheWriteFailures) - fmt.Fprintf(w, "memory_cache_hits %d\n", stats.MemoryCacheHits) - fmt.Fprintf(w, "disk_cache_hits %d\n", stats.DiskCacheHits) - fmt.Fprintf(w, "promotions %d\n", stats.Promotions) - fmt.Fprintf(w, "evictions %d\n", stats.Evictions) - for svc, cnt := range stats.ServiceErrors { - fmt.Fprintf(w, "service_errors{service=%q} %d\n", svc, cnt) - } - for svc, cnt := range stats.ServiceRequests { - fmt.Fprintf(w, "service_requests{service=%q} %d\n", svc, cnt) - } - fmt.Fprintf(w, "hit_rate %.4f\n", stats.HitRate) - fmt.Fprintf(w, "avg_response_time_ms %.2f\n", float64(stats.AvgResponseTime.Nanoseconds())/1e6) - fmt.Fprintf(w, "total_bytes_served %d\n", stats.TotalBytesServed) - fmt.Fprintf(w, "total_bytes_cached %d\n", stats.TotalBytesCached) - fmt.Fprintf(w, "memory_cache_size %d\n", stats.MemoryCacheSize) - fmt.Fprintf(w, "disk_cache_size %d\n", stats.DiskCacheSize) - fmt.Fprintf(w, "uptime_seconds %.2f\n", stats.Uptime.Seconds()) + writeMetricsText(w, stats) return } @@ -1371,7 +1379,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Try to serve from cache file, err := sc.vfs.Open(cachePath) if err == nil { - defer file.Close() + defer func() { _ = file.Close() }() // best-effort close of cache file reader; error secondary (data already read or connection issue) // Read the entire cached file cachedData, err := io.ReadAll(file) @@ -1381,7 +1389,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { Str("url", urlPath). Err(err). Msg("Failed to read cached file - removing corrupted entry") - sc.vfs.Delete(cachePath) + _ = sc.vfs.Delete(cachePath) // best-effort cleanup of corrupt entry; failure non-fatal (logged) } else { // Deserialize using new format cacheFile, err := deserializeCacheFile(cachedData) @@ -1392,7 +1400,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { Str("url", urlPath). Err(err). Msg("Failed to deserialize cache file - removing corrupted entry") - sc.vfs.Delete(cachePath) + _ = sc.vfs.Delete(cachePath) // best-effort cleanup of corrupt entry; failure non-fatal (logged) } else { // Track cache hit metrics sc.metrics.IncrementCacheHits() @@ -1472,7 +1480,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-LanCache-Status", "HIT-COALESCED") w.Header().Set("X-LanCache-Processed-By", "SteamCache2") w.WriteHeader(coalescedReq.statusCode) - w.Write(responseData) + _, _ = w.Write(responseData) // client write error ignored (disconnect during coalesced response send is not actionable) // Track coalesced cache hit metrics sc.metrics.IncrementCacheCoalesced() @@ -1573,7 +1581,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { logger.Logger.Error().Err(err).Str("url", req.URL.String()).Msg("Failed to fetch the requested URL") if resp != nil { - resp.Body.Close() + _ = resp.Body.Close() // best-effort close on upstream fetch error; primary error logged/returned } // Complete coalesced request with error if isNew { @@ -1589,7 +1597,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { if resp.StatusCode != http.StatusOK { logger.Logger.Error().Int("status_code", resp.StatusCode).Str("url", req.URL.String()).Msg("Failed to fetch the requested URL (non-OK status after retries)") - resp.Body.Close() + _ = resp.Body.Close() // best-effort close on non-OK upstream; primary error path // Complete coalesced request with error if isNew { coalescedReq.complete(nil, fmt.Errorf("upstream returned status %d", resp.StatusCode)) @@ -1601,7 +1609,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to fetch the requested URL", http.StatusInternalServerError) return } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() // best-effort close for success upstream response body (standard handler cleanup) // Fast path: Flexible lightweight validation for all files // Multiple validation layers ensure data integrity without blocking legitimate Steam content @@ -1716,7 +1724,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Stream the response body to client w.WriteHeader(resp.StatusCode) - w.Write(bodyData) + _, _ = w.Write(bodyData) // client write error ignored (disconnect during MISS body send is not actionable) // Track cache miss metrics sc.metrics.IncrementCacheMisses() @@ -1751,7 +1759,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Store the serialized cache data cacheWriter, err := sc.vfs.Create(cachePath, int64(len(cacheData))) if err == nil { - defer cacheWriter.Close() + defer func() { _ = cacheWriter.Close() }() // best-effort close of cache writer; errors on close (e.g. final sync) logged via prior write checks or non-fatal // Write the serialized cache data bytesWritten, cacheErr := cacheWriter.Write(cacheData) @@ -1766,7 +1774,7 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) { Msg("Cache write failed or incomplete - removing corrupted entry") sc.metrics.IncrementCacheWriteFailures() sc.metrics.IncrementServiceError("cache_write") - sc.vfs.Delete(cachePath) + _ = sc.vfs.Delete(cachePath) // best-effort removal of partial corrupt cache entry on write failure; non-fatal. Deferred cacheWriter.Close() (from earlier in block) runs after this on error unwind path (harmless per DiskFS design) } else { // Track successful cache write sc.metrics.AddBytesCached(int64(len(cacheData))) diff --git a/vfs/cache/cache.go b/vfs/cache/cache.go index 5f33e4c..ad7d11d 100644 --- a/vfs/cache/cache.go +++ b/vfs/cache/cache.go @@ -177,7 +177,7 @@ func (tc *TieredCache) Capacity() int64 { // promoteToFast promotes a file from slow tier to fast tier func (tc *TieredCache) promoteToFast(key string, reader io.ReadCloser) { - defer reader.Close() + defer func() { _ = reader.Close() }() // best-effort close; error secondary to promotion attempt (async best-effort path) // Get file info from slow tier to determine size var size int64 @@ -217,9 +217,10 @@ func (tc *TieredCache) promoteToFast(key string, reader io.ReadCloser) { if vfs, ok := fast.(vfs.VFS); ok { writer, err := vfs.Create(key, size) if err == nil { - // Write content to fast tier - writer.Write(content) - writer.Close() + // Write/close errors intentionally discarded: promotion to fast tier is best-effort optimization only. + // Failure (e.g. mem pressure, concurrent evict) is non-fatal and does not affect correctness of slow tier. + _, _ = writer.Write(content) + _ = writer.Close() } } } diff --git a/vfs/disk/disk.go b/vfs/disk/disk.go index 70c9c6a..9acfbaa 100644 --- a/vfs/disk/disk.go +++ b/vfs/disk/disk.go @@ -108,7 +108,8 @@ func New(root string, capacity int64, evict func(vfs.VFS, uint) uint) (*DiskFS, } // Create root directory if it doesn't exist. Propagate error (ctor now returns err for hygiene). - if err := os.MkdirAll(root, 0755); err != nil { + // 0700 (not 0755): cache contents are user data from untrusted CDN responses; least-privilege for LAN appliance. + if err := os.MkdirAll(root, 0700); err != nil { return nil, fmt.Errorf("failed to create root directory %s: %w", root, err) } @@ -225,7 +226,7 @@ func (d *DiskFS) calculateSizeAndPopulateIndex() { overCapacity := d.size > d.capacity needed := uint(0) if overCapacity { - needed = uint(d.size - d.capacity) + needed = uint(d.size - d.capacity) // #nosec G115 -- diff guaranteed >0 by overCapacity check; eviction API takes uint (bytes); fits in practice for cache sizes } d.mu.RUnlock() if overCapacity && d.startupEvict != nil { @@ -382,11 +383,12 @@ func (d *DiskFS) Create(key string, size int64) (io.WriteCloser, error) { d.mu.Unlock() dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0755); err != nil { + // 0700 (not 0755): per-shard cache dirs hold untrusted CDN content; restrict to owner only (G301 addressed). + if err := os.MkdirAll(dir, 0700); err != nil { return nil, err } - file, err := os.Create(path) + file, err := os.Create(path) // #nosec G304 -- path built by pathForKey from sanitized (Clean, no ..) hash-derived key under trusted disk.root; no untrusted file inclusion if err != nil { return nil, err } @@ -425,7 +427,7 @@ func (dwc *diskWriteCloser) Close() error { // Get the actual file size stat, err := dwc.file.Stat() if err != nil { - dwc.file.Close() + _ = dwc.file.Close() // best-effort close on stat error path; primary error is returned return err } @@ -482,7 +484,7 @@ func (d *DiskFS) Open(key string) (io.ReadCloser, error) { path := d.pathForKey(key) - file, err := os.Open(path) + file, err := os.Open(path) // #nosec G304 -- path built by pathForKey from sanitized (Clean, no ..) hash-derived key under trusted disk.root; no untrusted file inclusion if err != nil { return nil, err } @@ -491,7 +493,7 @@ func (d *DiskFS) Open(key string) (io.ReadCloser, error) { const mmapThreshold = 1024 * 1024 // 1MB if fi.Size > mmapThreshold { // Close the regular file handle - file.Close() + _ = file.Close() // best-effort; mmap path takes over or falls back // Try memory mapping mmapFile, err := os.Open(path) @@ -501,8 +503,8 @@ func (d *DiskFS) Open(key string) (io.ReadCloser, error) { mapped, err := mmap.Map(mmapFile, mmap.RDONLY, 0) if err != nil { - mmapFile.Close() - // Fallback to regular file reading + _ = mmapFile.Close() // best-effort close before fallback open + // Fallback to regular file reading (intentional 3rd open of same path after mmap failure; pre-existing pattern, no leak) return os.Open(path) } @@ -534,7 +536,7 @@ func (m *mmapReadCloser) Read(p []byte) (n int, err error) { } func (m *mmapReadCloser) Close() error { - m.data.Unmap() + _ = m.data.Unmap() // best-effort; unmap failure non-fatal for read-only mapping return m.file.Close() } @@ -653,7 +655,7 @@ func (d *DiskFS) EvictLRU(bytesNeeded uint) uint { d.LRU.Remove(key) delete(d.info, key) path := d.pathForKey(key) - _ = os.Remove(path) // best effort; performed under WLock (reverted from post-unlock) to guarantee on-disk deletion is coordinated with metadata removal. This eliminates resurrection via lazy Stat/Open discovery and prevents late unlinks from deleting content of same-key recreates (critical for in-memory metadata safety model + user's explicit non-race requirement on hot eviction path). + _ = os.Remove(path) // #nosec G304 -- path from sanitized key; best-effort eviction delete under lock. Best effort; performed under WLock to guarantee on-disk deletion is coordinated with metadata removal. This eliminates resurrection via lazy Stat/Open discovery and prevents late unlinks from deleting content of same-key recreates (critical for in-memory metadata safety model + user's explicit non-race requirement on hot eviction path). d.size -= fi.Size evicted += uint(fi.Size) shardIndex := locks.GetShardIndex(key)