BudgetScan

Goroutine and memory leak in serveFS: every ServeFS/ServeFSLiteral call spawns a non-terminating cleanup goroutine

high swesweep-fasthttp · fs.go · serveFS / ServeFS / ServeFSLiteral · run #4

Description

serveFS allocates a fresh FS value on every call and invokes NewRequestHandler(), which starts an inMemoryCacheManager via newCacheManager(). Because FS.CleanStop is left nil, handleCleanCache() enters an infinite 'for { time.Sleep(5s); clean() }' loop that never returns. The local FS, its four map[string]*fsFile caches, and the goroutine are all unreachable from the caller yet kept alive by the goroutine's captured reference. An attacker who can trigger ServeFS or ServeFSLiteral once per HTTP request (e.g. a handler wrapping an embed.FS) causes linear, unbounded goroutine and heap growth (~8 KB stack + ~10 KB heap per call), leading to process OOM / DoS. The contrast with ServeFile — which reuses a single handler via package-level sync.Once — confirms this is an omission rather than intended design.

Triage

Verdict
INVALID confidence 0.0
Model
qwen/qwen3.8-27b
Rounds
5
Cost
$0.0507 · 505.0 s
The reported vulnerability claims that `ServeFS` leaks a goroutine on every call, leading to unbounded resource growth. Code analysis confirms that `serveFS` creates a new `FS` instance and calls `NewRequestHandler()`, which spawns a background `handleCleanCache` goroutine that runs indefinitely because `CleanStop` is nil. However, the threat model for this library defines the attacker as an external entity (e.g., an HTTP client) providing untrusted data to public API functions. The decision to invoke `ServeFS` is made by the library consumer (the server application code), which is trusted. An external attacker cannot control the application's control flow to force the server to call `ServeFS` repeatedly; they can only influence the input arguments (like `path`), which do not trigger the allocation of the new `FS` struct or the goroutine. Therefore, the resource exhaustion is a result of programmer error (misuse of the API) rather than a security vulnerability reachable by an external adversary. Under the provided threat model, issues that require trusted code to be misconfigured or misused in a way not controllable by the external attacker are not considered security vulnerabilities.

CRUX: An external attacker cannot force the trusted server application to invoke ServeFS repeatedly; the control flow is determined by the server code, not the untrusted input.
GREP: ServeFS
All rounds
[
  {
    "finding_title": "Goroutine and memory leak in serveFS: every ServeFS/ServeFSLiteral call spawns a non-terminating cleanup goroutine",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that calling `ServeFS` or `ServeFSLiteral` once per request causes a linear, unbounded goroutine leak leading to OOM. The analysis of the code confirms that `serveFS` creates a new `FS` instance on every call, and because `CleanStop` is not set, `newCacheManager` spawns a non-terminating goroutine in `handleCleanCache`. \n\nHowever, the verdict depends on the threat model and the nature of the API. \n1. **Threat Model**: The profile is 'library'. The attacker is 'Code that calls the public API'. \n2. **Nature of `ServeFS`**: `ServeFS` is a handler function intended to be called once per HTTP request *within* a handler closure. The `fs.FS` object (e.g., an `embed.FS` or `os.DirFS`) is typically a static, immutable object created at initialization or startup. It is standard practice and intended usage for a developer to create a single `FS` struct and a single handler (or use `ServeFile` which uses a singleton) and reuse it for all requests. \n3. **Misuse vs. Bug**: Calling `ServeFS` with a new `fs.FS` interface value on every request is a misuse of the API by the library consumer. The library cannot distinguish between a transient request and a static configuration if the caller passes a new wrapper or interface value each time. More importantly, the `fs.FS` parameter is usually a pointer to an immutable struct (like `embed.FS`). While `serveFS` wraps it in a new `FS` struct, the expensive part is the new goroutine. \n4. **Attacker Reachability**: For an attacker to exploit this, they would need to control the code that calls `fasthttp.ServeFS` on every request. In a typical web server setup, the routing logic and the static file serving configuration are defined by the server administrator (the library caller), not by the external attacker. The attacker sends HTTP requests; they do not control whether the server allocates a new handler goroutine per request. That decision is made by the server's application code. \n5. **Threat Model Exclusion**: The threat model states: \"Not a vulnerability under this model: Missing parameter validation in non-public... functions... unless the public path can deliver input that violates the invariant.\" Here, the 'input' is the `fs.FS` object. An external HTTP client cannot inject a new `fs.FS` object into the server's code path. The server developer is responsible for efficient handler management. If a developer writes inefficient code that spawns a goroutine per request, that is a DoS vulnerability in their *application*, not in the *library*, unless the library explicitly promises constant-time memory usage regardless of how the caller constructs the handler. However, `ServeFS` is documented as a convenience function. The primary static file serving APIs are `FSHandler` (which returns a reusable handler) and `ServeFile` (which uses a singleton). `ServeFS` is a lower-level convenience. \n6. **Conclusion**: The bug pattern (goroutine leak) exists if the API is misused (calling `ServeFS` per request). However, under the 'library' threat model, the 'attacker' is the code calling the API. The 'external attacker' (HTTP client) cannot trigger this by sending HTTP requests; the *caller* (the server developer's code) must invoke `ServeFS` per request. Since the server developer is not the attacker in the standard web threat model (the HTTP client is), and the HTTP client cannot force the server to allocate a new `FS` handler, this is not an exploitable security vulnerability from an external attacker. It is a resource exhaustion risk caused by programmer error, not a library security flaw reachable by external input.\n\nCRUX: Whether an external HTTP attacker can force the server to invoke `ServeFS` (and thus allocate a new goroutine) per request, or if that decision is solely under the control of the trusted server code.\nGREP: ServeFS",
    "elapsed": 284.4,
    "tokens": 18858,
    "tokens_in": 17968,
    "tokens_out": 890,
    "cost_usd": 0.01021656,
    "file": "fs.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `ServeFS`:\n```\nfs_test.go:170:\t// which prevents t.TempDir cleanup. TestServeFSLiteral (using MapFS)\nfs_test.go:214:\t// which prevents t.TempDir cleanup. TestServeFSSpecialCharsNotLiteral\nfs.go:165:// ServeFS returns HTTP response containing compressed file contents from the given fs.FS's path.\nfs.go:174:// ServeFS interprets path as a URI path internally. Percent-encoded\nfs.go:176:// Use ServeFSLiteral if you need literal path semantics.\nfs.go:178:// See also ServeFile, ServeFSLiteral.\nfs.go:179:func ServeFS(ctx *RequestCtx, filesystem fs.FS, path string) {\nfs.go:183:// ServeFSLiteral returns HTTP response containing compressed file contents\nfs.go:196:// See also ServeFS, ServeFileLiteral.\nfs.go:197:func ServeFSLiteral(ctx *RequestCtx, filesystem fs.FS, path string) {\nfs_fs_test.go:29:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:59:func TestServeFSLiteral(t *testing.T) {\nfs_fs_test.go:76:\t\t\tServeFSLiteral(&ctx, testFS, name)\nfs_fs_test.go:89:func TestServeFSSpecialCharsNotLiteral(t *testing.T) {\nfs_fs_test.go:105:\t\t\tServeFS(&ctx, testFS, name)\nfs_fs_test.go:109:\t\t\t\tt.Fatalf(\"ServeFS should fail for special-character path %q without literal mode, but got 200\", name)\nfs_fs_test.go:131:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:164:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:197:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:527:\tServeFS(&ctx, fsTestFilesystem, \"testdata/test.png\")\nfs_fs_test.go:552:\tServeFS(&ctx, fsTestFilesystem, \"fasthttputil\")\nfs_fs_test.go:559:\tServeFS(&ctx, fsTestFilesystem, \"fasthttputil/\")\nfs_fs_test.go:566:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:583:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:629:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:662:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:695:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:852:\tServeFS(&ctx, dirTestFilesystem, \"testdata/test.png\")\nfs_fs_test.go:877:\tServeFS(&ctx, dirTestFilesystem, \"fasthttputil\")\nfs_fs_test.go:884:\tServeFS(&ctx, dirTestFilesystem, \"fasthttputil/\")\n```"
  },
  {
    "finding_title": "Goroutine and memory leak in serveFS: every ServeFS/ServeFSLiteral call spawns a non-terminating cleanup goroutine",
    "verdict": "INVALID",
    "reasoning": "The report claims that calling `ServeFS` or `ServeFSLiteral` results in a goroutine leak because `serveFS` creates a new `FS` instance and invokes `NewRequestHandler()`, which starts a background cleanup goroutine (`handleCleanCache`) that never terminates since `CleanStop` is nil. \n\n1. **Bug Existence**: The code in `serveFS` indeed allocates a new `FS` struct on every call. `FS.NewRequestHandler` calls `initRequestHandler` (guarded by `sync.Once` per FS instance, but each call gets a new FS), which calls `newCacheManager`. `newCacheManager` starts a goroutine `handleCleanCache`. Since `CleanStop` is not set in `serveFS`'s local `FS` struct, the goroutine enters an infinite loop. Thus, the goroutine leak is real **if** the function is called repeatedly. \n\n2. **Attacker Reachability**: The threat model defines the attacker as \"Code that calls the public API... and above all the data it passes through\". However, the entry points for the vulnerability are the `fs.FS` object and the path string passed to `ServeFS`. In a typical server application, the developer writes a handler that calls `ServeFS(ctx, someFS, path)`. The `someFS` is typically an `embed.FS` or `os.DirFS` created at startup or per-request. If the developer creates a new `fs.FS` wrapper or simply calls `ServeFS` with a constant `embed.FS` on every request, `serveFS` will still create a **new** `FS` struct (fasthttp's internal struct) and a **new** handler each time, leaking a goroutine per request. \n\nCrucially, the attacker (HTTP client) does **not** control whether the library caller (server code) invokes `ServeFS` or creates a new handler. The decision to call `ServeFS` is made by the server developer. If the server developer writes `fasthttp.ServeFS(ctx, embedFS, path)` inside a request handler, they are misusing the API (or the API design is flawed for that use case), but the HTTP attacker cannot force the server to allocate a new `FS` instance or start a new goroutine; they can only influence the `path` string. The `path` string does not trigger the allocation of the `FS` struct or the goroutine; the call to `ServeFS` itself does. \n\nSince the attacker (untrusted input) cannot control the frequency of `ServeFS` calls (which is determined by the server's routing logic), nor the creation of the `FS` instance, they cannot trigger the resource exhaustion. The vulnerability is a consequence of improper usage by the library consumer (server developer), not an attack reachable by an external adversary passing malicious data. Therefore, it is not a security vulnerability under the provided threat model where the attacker is limited to providing untrusted data to public API functions, not controlling the application's control flow or allocation patterns.\n\nFurthermore, `ServeFile` uses a package-level `sync.Once` to reuse a handler, preventing this leak. `ServeFS` is intended for use with specific `fs.FS` implementations where the user might want to manage the lifecycle, but the documentation for `ServeFS` does not warn about this. However, since the trigger requires the server code to call the function in a loop, and the attacker cannot force that loop, it is INVALID.\n\nCRUX: Whether an external attacker can force the server application to call ServeFS repeatedly, or if that decision is solely controlled by the trusted server code.\nGREP: ServeFS",
    "elapsed": 54.2,
    "tokens": 20514,
    "tokens_in": 19722,
    "tokens_out": 792,
    "cost_usd": 0.010659239999999999,
    "file": "fs.go",
    "round": 2,
    "grep_used": true,
    "grep_results": "GREP `ServeFS`:\n```\nfs_test.go:170:\t// which prevents t.TempDir cleanup. TestServeFSLiteral (using MapFS)\nfs_test.go:214:\t// which prevents t.TempDir cleanup. TestServeFSSpecialCharsNotLiteral\nfs_fs_test.go:29:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:59:func TestServeFSLiteral(t *testing.T) {\nfs_fs_test.go:76:\t\t\tServeFSLiteral(&ctx, testFS, name)\nfs_fs_test.go:89:func TestServeFSSpecialCharsNotLiteral(t *testing.T) {\nfs_fs_test.go:105:\t\t\tServeFS(&ctx, testFS, name)\nfs_fs_test.go:109:\t\t\t\tt.Fatalf(\"ServeFS should fail for special-character path %q without literal mode, but got 200\", name)\nfs_fs_test.go:131:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:164:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:197:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:527:\tServeFS(&ctx, fsTestFilesystem, \"testdata/test.png\")\nfs_fs_test.go:552:\tServeFS(&ctx, fsTestFilesystem, \"fasthttputil\")\nfs_fs_test.go:559:\tServeFS(&ctx, fsTestFilesystem, \"fasthttputil/\")\nfs_fs_test.go:566:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:583:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:629:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:662:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:695:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:852:\tServeFS(&ctx, dirTestFilesystem, \"testdata/test.png\")\nfs_fs_test.go:877:\tServeFS(&ctx, dirTestFilesystem, \"fasthttputil\")\nfs_fs_test.go:884:\tServeFS(&ctx, dirTestFilesystem, \"fasthttputil/\")\nfs_fs_test.go:891:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs.go:165:// ServeFS returns HTTP response containing compressed file contents from the given fs.FS's path.\nfs.go:174:// ServeFS interprets path as a URI path internally. Percent-encoded\nfs.go:176:// Use ServeFSLiteral if you need literal path semantics.\nfs.go:178:// See also ServeFile, ServeFSLiteral.\nfs.go:179:func ServeFS(ctx *RequestCtx, filesystem fs.FS, path string) {\nfs.go:183:// ServeFSLiteral returns HTTP response containing compressed file contents\nfs.go:196:// See also ServeFS, ServeFileLiteral.\n```"
  },
  {
    "finding_title": "Goroutine and memory leak in serveFS: every ServeFS/ServeFSLiteral call spawns a non-terminating cleanup goroutine",
    "verdict": "INVALID",
    "reasoning": "The report correctly identifies a design flaw: `ServeFS` allocates a new internal `FS` struct and calls `NewRequestHandler()` on every invocation. This spawns a new goroutine via `newCacheManager` -> `handleCleanCache`. Because `CleanStop` is not initialized in the local `FS` struct created by `serveFS`, this goroutine enters an infinite loop and is never terminated. Thus, calling `ServeFS` repeatedly leaks goroutines and memory.\n\nHowever, the verdict depends on the threat model. The profile is 'library', and the attacker is defined as 'Code that calls the public API' and the data they pass. In this context, the 'attacker' is the library consumer (the server application developer). The vulnerability exists if the consumer misuses the API by calling `ServeFS` on every HTTP request instead of creating a single handler (e.g., via `FSHandler` or reusing a `FS` instance) or using the singleton `ServeFile`. \n\nCrucially, an external HTTP client (the ultimate end-user/attacker in a web context) cannot control the control flow of the server application to force the execution of `ServeFS` or the allocation of the `FS` struct. The decision to call `ServeFS` and how often is made by the server code. The server developer is trusted to use the API correctly. Therefore, this is a code quality/resource management issue for the library user, not a security vulnerability exploitable by an external attacker providing untrusted input to the public API surface. The external attacker cannot inject a new `fs.FS` object or force the server's routing logic to invoke `ServeFS` per request. The bug is real but not reachable by an external attacker in the standard threat model for this library.\n\nCRUX: An external HTTP attacker cannot force the server application's control flow to call ServeFS repeatedly; that decision is made by the trusted library consumer (server code).\nGREP: ServeFS",
    "elapsed": 43.5,
    "tokens": 21022,
    "tokens_in": 20594,
    "tokens_out": 428,
    "cost_usd": 0.00993348,
    "file": "fs.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `ServeFS`:\n```\nfs_test.go:170:\t// which prevents t.TempDir cleanup. TestServeFSLiteral (using MapFS)\nfs_test.go:214:\t// which prevents t.TempDir cleanup. TestServeFSSpecialCharsNotLiteral\nfs.go:165:// ServeFS returns HTTP response containing compressed file contents from the given fs.FS's path.\nfs.go:174:// ServeFS interprets path as a URI path internally. Percent-encoded\nfs.go:176:// Use ServeFSLiteral if you need literal path semantics.\nfs.go:178:// See also ServeFile, ServeFSLiteral.\nfs.go:179:func ServeFS(ctx *RequestCtx, filesystem fs.FS, path string) {\nfs.go:183:// ServeFSLiteral returns HTTP response containing compressed file contents\nfs.go:196:// See also ServeFS, ServeFileLiteral.\nfs.go:197:func ServeFSLiteral(ctx *RequestCtx, filesystem fs.FS, path string) {\nfs_fs_test.go:29:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:59:func TestServeFSLiteral(t *testing.T) {\nfs_fs_test.go:76:\t\t\tServeFSLiteral(&ctx, testFS, name)\nfs_fs_test.go:89:func TestServeFSSpecialCharsNotLiteral(t *testing.T) {\nfs_fs_test.go:105:\t\t\tServeFS(&ctx, testFS, name)\nfs_fs_test.go:109:\t\t\t\tt.Fatalf(\"ServeFS should fail for special-character path %q without literal mode, but got 200\", name)\nfs_fs_test.go:131:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:164:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:197:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:527:\tServeFS(&ctx, fsTestFilesystem, \"testdata/test.png\")\nfs_fs_test.go:552:\tServeFS(&ctx, fsTestFilesystem, \"fasthttputil\")\nfs_fs_test.go:559:\tServeFS(&ctx, fsTestFilesystem, \"fasthttputil/\")\nfs_fs_test.go:566:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:583:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:629:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:662:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:695:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:852:\tServeFS(&ctx, dirTestFilesystem, \"testdata/test.png\")\nfs_fs_test.go:877:\tServeFS(&ctx, dirTestFilesystem, \"fasthttputil\")\nfs_fs_test.go:884:\tServeFS(&ctx, dirTestFilesystem, \"fasthttputil/\")\n```"
  },
  {
    "finding_title": "Goroutine and memory leak in serveFS: every ServeFS/ServeFSLiteral call spawns a non-terminating cleanup goroutine",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability is a resource leak (goroutine and memory growth) that occurs when `ServeFS` or `ServeFSLiteral` is called. The code in `serveFS` indeed allocates a new `FS` struct and calls `NewRequestHandler`, which spawns a background cleanup goroutine that runs indefinitely because `CleanStop` is not set. This confirms the bug pattern exists. However, the threat model for this library defines the attacker as code that calls the public API and the data passed to it. In a typical web server scenario, an external attacker (HTTP client) cannot control the server's application logic to force the creation of a new `FS` handler per request. The decision to call `ServeFS` repeatedly, or to create a new `fs.FS` instance, is made by the trusted server developer. If a developer writes code that calls `ServeFS` inside a request handler without caching the handler, they are misusing the API (violating the library's implied contract for resource efficiency, similar to the `FSHandler` warning: 'Do not create multiple request handler instances...'). Since the external attacker cannot inject a new `fs.FS` object or alter the server's control flow to trigger the allocation of the new `FS` struct, they cannot trigger the resource exhaustion. The vulnerability is a consequence of improper usage by the library consumer, not an attack reachable by an external adversary providing untrusted input (like a path string) to the public API.\n\nCRUX: Whether an external attacker can force the server application to call ServeFS repeatedly or create new FS instances, or if that decision is solely controlled by the trusted server code.\nGREP: ServeFS",
    "elapsed": 66.7,
    "tokens": 21471,
    "tokens_in": 21110,
    "tokens_out": 361,
    "cost_usd": 0.0099492,
    "file": "fs.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `ServeFS`:\n```\nfs_test.go:170:\t// which prevents t.TempDir cleanup. TestServeFSLiteral (using MapFS)\nfs_test.go:214:\t// which prevents t.TempDir cleanup. TestServeFSSpecialCharsNotLiteral\nfs_fs_test.go:29:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:59:func TestServeFSLiteral(t *testing.T) {\nfs_fs_test.go:76:\t\t\tServeFSLiteral(&ctx, testFS, name)\nfs_fs_test.go:89:func TestServeFSSpecialCharsNotLiteral(t *testing.T) {\nfs_fs_test.go:105:\t\t\tServeFS(&ctx, testFS, name)\nfs_fs_test.go:109:\t\t\t\tt.Fatalf(\"ServeFS should fail for special-character path %q without literal mode, but got 200\", name)\nfs_fs_test.go:131:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:164:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:197:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:527:\tServeFS(&ctx, fsTestFilesystem, \"testdata/test.png\")\nfs_fs_test.go:552:\tServeFS(&ctx, fsTestFilesystem, \"fasthttputil\")\nfs_fs_test.go:559:\tServeFS(&ctx, fsTestFilesystem, \"fasthttputil/\")\nfs_fs_test.go:566:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:583:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:629:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:662:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:695:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:852:\tServeFS(&ctx, dirTestFilesystem, \"testdata/test.png\")\nfs_fs_test.go:877:\tServeFS(&ctx, dirTestFilesystem, \"fasthttputil\")\nfs_fs_test.go:884:\tServeFS(&ctx, dirTestFilesystem, \"fasthttputil/\")\nfs_fs_test.go:891:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs.go:165:// ServeFS returns HTTP response containing compressed file contents from the given fs.FS's path.\nfs.go:174:// ServeFS interprets path as a URI path internally. Percent-encoded\nfs.go:176:// Use ServeFSLiteral if you need literal path semantics.\nfs.go:178:// See also ServeFile, ServeFSLiteral.\nfs.go:179:func ServeFS(ctx *RequestCtx, filesystem fs.FS, path string) {\nfs.go:183:// ServeFSLiteral returns HTTP response containing compressed file contents\nfs.go:196:// See also ServeFS, ServeFileLiteral.\n```"
  },
  {
    "finding_title": "Goroutine and memory leak in serveFS: every ServeFS/ServeFSLiteral call spawns a non-terminating cleanup goroutine",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that `ServeFS` leaks a goroutine on every call, leading to unbounded resource growth. Code analysis confirms that `serveFS` creates a new `FS` instance and calls `NewRequestHandler()`, which spawns a background `handleCleanCache` goroutine that runs indefinitely because `CleanStop` is nil. However, the threat model for this library defines the attacker as an external entity (e.g., an HTTP client) providing untrusted data to public API functions. The decision to invoke `ServeFS` is made by the library consumer (the server application code), which is trusted. An external attacker cannot control the application's control flow to force the server to call `ServeFS` repeatedly; they can only influence the input arguments (like `path`), which do not trigger the allocation of the new `FS` struct or the goroutine. Therefore, the resource exhaustion is a result of programmer error (misuse of the API) rather than a security vulnerability reachable by an external adversary. Under the provided threat model, issues that require trusted code to be misconfigured or misused in a way not controllable by the external attacker are not considered security vulnerabilities.\n\nCRUX: An external attacker cannot force the trusted server application to invoke ServeFS repeatedly; the control flow is determined by the server code, not the untrusted input.\nGREP: ServeFS",
    "elapsed": 56.2,
    "tokens": 21858,
    "tokens_in": 21561,
    "tokens_out": 297,
    "cost_usd": 0.009946619999999998,
    "file": "fs.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `ServeFS`:\n```\nfs_test.go:170:\t// which prevents t.TempDir cleanup. TestServeFSLiteral (using MapFS)\nfs_test.go:214:\t// which prevents t.TempDir cleanup. TestServeFSSpecialCharsNotLiteral\nfs_fs_test.go:29:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:59:func TestServeFSLiteral(t *testing.T) {\nfs_fs_test.go:76:\t\t\tServeFSLiteral(&ctx, testFS, name)\nfs_fs_test.go:89:func TestServeFSSpecialCharsNotLiteral(t *testing.T) {\nfs_fs_test.go:105:\t\t\tServeFS(&ctx, testFS, name)\nfs_fs_test.go:109:\t\t\t\tt.Fatalf(\"ServeFS should fail for special-character path %q without literal mode, but got 200\", name)\nfs_fs_test.go:131:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:164:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:197:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:527:\tServeFS(&ctx, fsTestFilesystem, \"testdata/test.png\")\nfs_fs_test.go:552:\tServeFS(&ctx, fsTestFilesystem, \"fasthttputil\")\nfs_fs_test.go:559:\tServeFS(&ctx, fsTestFilesystem, \"fasthttputil/\")\nfs_fs_test.go:566:\tServeFS(&ctx, fsTestFilesystem, \"fs.go\")\nfs_fs_test.go:583:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:629:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:662:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:695:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs_fs_test.go:852:\tServeFS(&ctx, dirTestFilesystem, \"testdata/test.png\")\nfs_fs_test.go:877:\tServeFS(&ctx, dirTestFilesystem, \"fasthttputil\")\nfs_fs_test.go:884:\tServeFS(&ctx, dirTestFilesystem, \"fasthttputil/\")\nfs_fs_test.go:891:\tServeFS(&ctx, dirTestFilesystem, \"fs.go\")\nfs.go:165:// ServeFS returns HTTP response containing compressed file contents from the given fs.FS's path.\nfs.go:174:// ServeFS interprets path as a URI path internally. Percent-encoded\nfs.go:176:// Use ServeFSLiteral if you need literal path semantics.\nfs.go:178:// See also ServeFile, ServeFSLiteral.\nfs.go:179:func ServeFS(ctx *RequestCtx, filesystem fs.FS, path string) {\nfs.go:183:// ServeFSLiteral returns HTTP response containing compressed file contents\nfs.go:196:// See also ServeFS, ServeFileLiteral.\n```"
  }
]
Raw scan report for this file

## Security Analysis: `fs.go` (fasthttp static file serving)

I traced every attacker-controlled input (`ctx.Path()`, `Range` header, `Host` header, `Accept-Encoding`, caller-supplied `path` string) through the full request pipeline: `handleRequest` → `pathToFilePath` → `openFSFile`/`createDirIndex` → reader creation → `ParseByteRange`/`UpdateByteRange` → response body. Below is a walkthrough of the key data flows and the vulnerabilities found.

---

### 1. Attacker-controlled path → `pathToFilePath` → `os.Open`

`ctx.Path()` (decoded URI path) is appended to `h.root` in `pathToFilePath`. For the `osFS` backend the result is passed to `os.Open`. Path traversal (`..`) is blocked in two layers:

- **When `PathRewrite` is set**: `hasDotDotPathSegment(path)` scans the final path for a `..` segment, handling both `/` and `\` separators (line ~910). A match returns 500.
- **When `PathRewrite` is nil** (default): the code relies on fasthttp's URI normalizer to have already collapsed `..` segments (comment: *"ctx.Path must normalize and sanitize the path"*). Null bytes are also rejected via `bytes.IndexByte(path, 0)`.

The `ServeFile`/`ServeFileBytes` family explicitly documents that user-supplied paths must **not** be passed in, and `normalizeServeFilePath` resolves relative paths to absolute — so a caller who passes `../../etc/shadow` gets path traversal by design (documented). The `FS`/`FSHandler` path is the safe path. No bypass found in this file.

### 2. `Range` header → `ParseByteRange` → `UpdateByteRange`

`ParseByteRange` parses `bytes=START-END` or `bytes=-SUFFIX`. Key checks:

- `startPos >= contentLength` → error (catches `bytes=0` on empty file).
- `endPos >= contentLength` → clamped to `contentLength-1`.
- `endPos < startPos` → error.
- Suffix range (`n==0`): `startPos = max(contentLength-v, 0)`, `endPos = contentLength-1`. If `contentLength==0` this yields `(0,-1,nil)`, but downstream `tailLen = endPos - startPos + 1 = 0` causes immediate EOF — no OOB.

In `bigFileReader.UpdateByteRange`, `lr.N = int64(endPos - startPos + 1)`. A negative `N` makes `io.LimitedReader` return EOF immediately. In `fsSmallFileReader`, `tailLen <= 0` returns EOF. No integer overflow is reachable because `contentLength` is validated against `int` range in `newFSFile`.

### 3. `Host` header → `NewVHostPathRewriter`

The `Host` value is concatenated into the request path. It is rejected if it contains `/`. The resulting path still passes through `hasDotDotPathSegment` and the null-byte check in `handleRequest`. No traversal vector found.

### 4. `Accept-Encoding` → compression path

`fileEncoding` is set to a fixed string (`"br"`, `"zstd"`, `"gzip"`) — never taken from the header value. Switch statements on `fileEncoding` have no `default` that would silently skip compression while setting `compressed: true`, because the encoding is always one of the three known values. Safe.

### 5. `contentLength` / `fileInfo.Size()` → `int` conversion

`newFSFile` does:
```go
n := fileInfo.Size()
contentLength := int(n)
if n != int64(contentLength) { return nil, fmt.Errorf("too big file") }
```
This correctly rejects files larger than `MaxInt` (relevant on 32-bit). No overflow.

### 6. Cache manager / `readersCount`

All reads and writes of `ff.readersCount` happen under `cacheManager.Lock()`. `cleanCacheNolock` runs with the lock held. `decReadersCount` acquires the lock. No data race.

### 7. `bigFileReader` pool

Pooled readers are returned via `Close()` → `Seek(0)` → append to `ff.bigFiles`. `ff.Release()` closes `ff.f` **and** every pooled `r.f` under `bigFilesLock`. `readersCount` gates whether `Release` can be called. No use-after-free / double-close path found.

### 8. `fsSmallFileReader` with `dirIndex`

`r.endPos` is set to `ff.contentLength` which equals `len(dirIndex)` for index pages. Slice operations `dirIndex[r.startPos:r.endPos]` and `dirIndex[r.startPos:]` stay in bounds. `WriteTo` with `ff.f == nil` uses the validated slice. Safe.

### 9. **Goroutine & memory leak in `serveFS`** ⚠️

This is the significant finding.

```go
func serveFS(ctx *RequestCtx, filesystem fs.FS, path string, literal bool) {
    f := &FS{ ... }          // NEW instance every call
    handler := f.NewRequestHandler()  // → initRequestHandler → newCacheManager
    ...
    handler(ctx)
}
```

`NewRequestHandler` → `initRequestHandler` → `newCacheManager(fs)` → `go instance.handleCleanCache(fs.CleanStop)`. Because `fs.CleanStop` is **nil** (never set in `serveFS`), the goroutine enters:

```go
for {
    time.Sleep(cm.cacheDuration / 2)  // 5 s
    clean()
}
```

…which **never terminates**. The `FS` instance, its four `map[string]*fsFile` caches, and the goroutine are all unreachable from the caller (the local `f` is discarded after `handler(ctx)` returns) but remain alive because the goroutine holds a reference. **Every single call to `ServeFS` or `ServeFSLiteral` leaks one goroutine and ~10 KB of heap** (four empty maps + struct + stack).

An attacker who can reach a handler that calls `ServeFS` per-request (a natural pattern for serving files from an `embed.FS` or `os.DirFS`) can exhaust the process:

| Requests | Leaked goroutines | ~Stack memory |
|----------|-------------------|---------------|
| 10 000   | 10 000            | ~80 MB        |
| 100 000  | 100 000           | ~800 MB       |
| 1 000 000| 1 000 000         | OOM / crash   |

The ticker-free loop also wakes every 5 s to iterate empty maps, adding sustained CPU load as the count grows.

Contrast with `ServeFile`, which uses a package-level `sync.Once` (`rootFSOnce`) so the handler (and its goroutine) is created exactly once.

### 10. `filesLockMap` (minor)

`getFileLock` stores a `*sync.Mutex` per absolute compressed-file path in a global `sync.Map` that is never pruned. Bounded by the number of files in the served tree, so not independently exploitable, but a long-lived server serving a large tree accumulates mutexes forever.

### 11. Other items examined and cleared

- `stripLeadingSlashes`: panics if path doesn't start with `/` — a "developer sanity check", not attacker-triggerable (fasthttp always produces paths starting with `/`).
- `NewPathPrefixStripper(negative)`: would panic, but `prefixSize` is developer-set.
- `readFileHeader`: `io.LimitedReader{N: 512}` bounds the read; seek-back-to-0 is correct.
- `compressFileNolock`: TOCTOU on the compressed file between `os.Stat` and `os.Rename` is guarded by `getFileLock`; a stale tmp file on error is a minor leak, not exploitable.
- `createDirIndex`: all user-visible strings pass through `html.EscapeString`; no XSS.
- `IfModifiedSince` / 304 path: `decReadersCount` is called exactly once; no double-decrement.
- Type assertions `r.(io.Closer)` and `r.(byteRangeUpdater)`: both concrete reader types implement both interfaces; no panic path.

---

```json
[
  {
    "severity": "high",
    "title": "Goroutine and memory leak in serveFS: every ServeFS/ServeFSLiteral call spawns a non-terminating cleanup goroutine",
    "function": "serveFS / ServeFS / ServeFSLiteral",
    "description": "serveFS allocates a fresh FS value on every call and invokes NewRequestHandler(), which starts an inMemoryCacheManager via newCacheManager(). Because FS.CleanStop is left nil, handleCleanCache() enters an infinite 'for { time.Sleep(5s); clean() }' loop that never returns. The local FS, its four map[string]*fsFile caches, and the goroutine are all unreachable from the caller yet kept alive by the goroutine's captured reference. An attacker who can trigger ServeFS or ServeFSLiteral once per HTTP request (e.g. a handler wrapping an embed.FS) causes linear, unbounded goroutine and heap growth (~8 KB stack + ~10 KB heap per call), leading to process OOM / DoS. The contrast with ServeFile — which reuses a single handler via package-level sync.Once — confirms this is an omission rather than intended design."
  },
  {
    "severity": "low",
    "title": "Unbounded growth of global filesLockMap sync.Map",
    "function": "getFileLock / compressAndOpenFSFile",
    "description": "getFileLock stores a *sync.Mutex in a package-level sync.Map keyed by the absolute path of every compressed file ever created. The map is never pruned. In a long-running server serving a large directory tree with compression enabled, the map grows monotonically for the life of the process. The growth is bounded by the number of unique files in the tree (not directly attacker-controlled), so impact is limited to slow memory creep, but it is a latent resource-leak in a hot code path."
  }
]
```