BudgetScan

Permanent worker goroutines and channel per NewFunc instance

medium swesweep-fasthttp · stackless/func.go · NewFunc · run #4

Description

Each NewFunc call creates funcWorkCh and, on first use, starts runtime.GOMAXPROCS(-1) funcWorker goroutines that range over the channel until it is closed. The channel is never closed and the package provides no Close/Stop API. If callers create many wrappers, for example per tenant, per connection, or under attacker influence, the goroutines, channel buffers, captured f, and any state closed over by f are retained for the lifetime of the process. Dropping the returned wrapper does not terminate the workers. This can lead to goroutine and memory exhaustion, causing denial of service.

Triage

Verdict
INVALID confidence 0.0
Model
qwen/qwen3.8-27b
Rounds
5
Cost
$0.0128 · 34.4 s
The report claims that creating multiple instances of `NewFunc` leads to resource exhaustion because each instance spawns permanent worker goroutines that are never stopped. The code confirms that `NewFunc` creates a channel and starts `GOMAXPROCS(-1)` goroutines via `sync.Once` on the first call, and there is no mechanism to close the channel or stop the goroutines when the returned wrapper is discarded. However, the threat model for this library defines the attacker as an external entity controlling untrusted *data* passed through the public API (e.g., compressed streams, byte slices), not the control flow of the host application. In `swesweep-fasthttp`, `NewFunc` is called in static package-level initialization (e.g., `brotli.go`, `zstd.go`, `compress.go`) to create shared, reusable compression wrappers. The number of instances is fixed at compile time/static initialization and does not depend on the content of incoming HTTP requests or other untrusted input. An external attacker sending malicious or high-volume data cannot influence the number of times `NewFunc` is invoked. Therefore, the resource exhaustion is not reachable through untrusted input under the binding threat model. It is a code quality/resource management issue for integrators who might misuse the API (e.g., by calling `NewFunc` in a loop per request), but not a security vulnerability in the library itself that can be triggered by an external attacker via data.

CRUX: The attacker cannot control the control flow of the host application to call NewFunc repeatedly; the instantiation is static and independent of untrusted data.
GREP: stackless.NewFunc
All rounds
[
  {
    "finding_title": "Permanent worker goroutines and channel per NewFunc instance",
    "verdict": "INVALID",
    "reasoning": "The report claims that creating many instances of `NewFunc` leads to resource exhaustion because each instance spawns permanent worker goroutines that are never stopped. However, this is a resource management issue dependent on the *caller's* usage pattern, not a vulnerability in the library itself reachable via untrusted input data. 1. The entry point is `NewFunc`, which takes a function `f`. To exploit this, an attacker must cause the host application to call `NewFunc` repeatedly with unique functions or contexts. 2. The threat model for a library defines the attacker as controlling the data passed *through* the public API (parsers, decoders, etc.), not the control flow of the host application (i.e., an attacker cannot typically force a web server to instantiate a new goroutine pool for every request without the server explicitly coding it that way, which would be a server design flaw, not a library memory safety vulnerability). 3. Even if `NewFunc` is called per tenant, this is a design choice by the integrator. The library does not process untrusted input that triggers the allocation of these workers; it allocates them upon construction. 4. Go goroutines are lightweight (approx 2-8KB stack initial). Spawning `GOMAXPROCS(-1)` workers per instance is a cost, but it does not constitute 'memory exhaustion' in the same security sense as an OOB write or unbounded buffer growth from parsed data. It is a resource consumption issue due to API misuse or poor integration design, not a security vulnerability in the code logic itself (like a buffer overflow or logic error allowing privilege escalation). 5. Crucially, the 'untrusted input' (the `ctx` data) does not trigger the creation of the workers. The workers are created based on the number of instances. If the host application creates instances in a loop controlled by an attacker (e.g., `for i := range attackerInput { NewFunc(...) }`), that is a vulnerability in the host application, not the library. The library assumes a stable usage pattern or limited instantiation. Therefore, this is INVALID under the threat model because the trigger (instantiation) is not driven by untrusted data content in the standard library context, but by the host application's control flow.\n\nCRUX: Whether the creation of `NewFunc` instances can be triggered by untrusted input data in a typical library consumer, or if it is solely a control-flow decision by the trusted host application.\nGREP: NewFunc",
    "elapsed": 9.7,
    "tokens": 2965,
    "tokens_in": 2434,
    "tokens_out": 531,
    "cost_usd": 0.00261528,
    "file": "stackless/func.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `NewFunc`:\n```\nbrotli.go:142:\t\tstacklessWriteBrotliFunc = stackless.NewFunc(nonblockingWriteBrotli)\nstackless/func.go:8:// NewFunc returns stackless wrapper for the function f.\nstackless/func.go:21:func NewFunc(f func(ctx any)) func(ctx any) bool {\nstackless/func_timing_test.go:10:\tf := NewFunc(func(ctx any) {\nstackless/func_test.go:10:func TestNewFuncSimple(t *testing.T) {\nstackless/func_test.go:14:\tf := NewFunc(func(ctx any) {\nstackless/func_test.go:29:func TestNewFuncMulti(t *testing.T) {\nstackless/func_test.go:33:\tf1 := NewFunc(func(ctx any) {\nstackless/func_test.go:36:\tf2 := NewFunc(func(ctx any) {\nstackless/writer.go:117:\t\tstacklessWriterFuncFunc = NewFunc(writerFunc)\nzstd.go:122:\t\tstacklessWriteZstdFunc = stackless.NewFunc(nonblockingWriteZstd)\ncompress.go:187:\t\tstacklessWriteGzipFunc = stackless.NewFunc(nonblockingWriteGzip)\ncompress.go:294:\t\tstacklessWriteDeflateFunc = stackless.NewFunc(nonblockingWriteDeflate)\n```"
  },
  {
    "finding_title": "Permanent worker goroutines and channel per NewFunc instance",
    "verdict": "INVALID",
    "reasoning": "The report claims that creating multiple instances of `NewFunc` leads to resource exhaustion because workers are never stopped. However, the threat model for a library defines the attacker as an external entity controlling untrusted input data (e.g., network packets, file contents) passed to the public API. The `NewFunc` function is a constructor/init function called by the trusted host application to set up the compression pipeline (e.g., in `brotli.go`, `zstd.go`, `compress.go`). The number of `NewFunc` calls is determined by the host application's design and configuration, not by the content of the untrusted data being processed. An external attacker cannot influence the control flow of the host application to call `NewFunc` repeatedly based on the bytes in the payload they send. While the code does have a potential leak if `NewFunc` were called in a tight loop (which is a code quality/resource management issue), it is not reachable via untrusted input in the context of a standard library usage where the library processes data. Therefore, it is not a security vulnerability under the provided threat model.\n\nCRUX: Whether an external attacker can influence the number of times `NewFunc` is called by the host application through untrusted input data.\nGREP: stackless.NewFunc",
    "elapsed": 5.8,
    "tokens": 3692,
    "tokens_in": 3413,
    "tokens_out": 279,
    "cost_usd": 0.00227046,
    "file": "stackless/func.go",
    "round": 2,
    "grep_used": true,
    "grep_results": "GREP `stackless.NewFunc`:\n```\nbrotli.go:142:\t\tstacklessWriteBrotliFunc = stackless.NewFunc(nonblockingWriteBrotli)\nzstd.go:122:\t\tstacklessWriteZstdFunc = stackless.NewFunc(nonblockingWriteZstd)\ncompress.go:187:\t\tstacklessWriteGzipFunc = stackless.NewFunc(nonblockingWriteGzip)\ncompress.go:294:\t\tstacklessWriteDeflateFunc = stackless.NewFunc(nonblockingWriteDeflate)\n```"
  },
  {
    "finding_title": "Permanent worker goroutines and channel per NewFunc instance",
    "verdict": "INVALID",
    "reasoning": "The report identifies a resource exhaustion vulnerability where `NewFunc` spawns a fixed number of goroutines per instance, and since there is no `Close` API, dropping the instance does not stop the workers. While this is a valid design concern for long-running applications that frequently create and discard wrappers, the threat model for this library defines the attacker as controlling the *data* passed through the public API (e.g., compressed streams, byte slices), not the control flow of the host application. The instantiation of `NewFunc` in `swesweep-fasthttp` occurs in package-level initialization or static setup (e.g., `brotli.go`, `zstd.go`), where it is called once per compression algorithm type to create a shared, reusable wrapper. An external attacker sending malicious HTTP payloads cannot trigger the creation of new `NewFunc` instances; they can only affect what data is passed to the already-created wrappers. Since the attacker cannot control the number of instances created, they cannot cause the resource exhaustion described. This is a resource management issue dependent on trusted host application design, not a security vulnerability reachable via untrusted input.\n\nCRUX: Whether an external attacker can control the control flow to instantiate multiple NewFunc objects via untrusted data.\nGREP: stackless.NewFunc",
    "elapsed": 5.2,
    "tokens": 3862,
    "tokens_in": 3586,
    "tokens_out": 276,
    "cost_usd": 0.00233412,
    "file": "stackless/func.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `stackless.NewFunc`:\n```\nbrotli.go:142:\t\tstacklessWriteBrotliFunc = stackless.NewFunc(nonblockingWriteBrotli)\nzstd.go:122:\t\tstacklessWriteZstdFunc = stackless.NewFunc(nonblockingWriteZstd)\ncompress.go:187:\t\tstacklessWriteGzipFunc = stackless.NewFunc(nonblockingWriteGzip)\ncompress.go:294:\t\tstacklessWriteDeflateFunc = stackless.NewFunc(nonblockingWriteDeflate)\n```"
  },
  {
    "finding_title": "Permanent worker goroutines and channel per NewFunc instance",
    "verdict": "INVALID",
    "reasoning": "The report identifies a resource exhaustion vulnerability (goroutine leak) caused by the lack of a `Close` API in `stackless.NewFunc`. While the code pattern (permanent goroutines with no stop mechanism) is real, the threat model for this library defines the attacker as an entity controlling untrusted *data* passed through the public API (parsers, decoders, protocol handlers). It does not define the attacker as an entity controlling the host application's *control flow* or configuration.\n\nTo trigger the described vulnerability (resource exhaustion via many `NewFunc` instances), the attacker would need to force the host application to call `stackless.NewFunc` repeatedly. In `swesweep-fasthttp`, `NewFunc` is called in static package-level initialization (e.g., `brotli.go:142`, `zstd.go:122`) to set up shared, reusable compression wrappers. The number of instances is fixed at compile time/static initialization and does not depend on the content of incoming HTTP requests or other untrusted input. An attacker sending malformed or high-volume data cannot influence the number of times `NewFunc` is invoked. Therefore, the bug is not reachable through untrusted input under the binding threat model. It is a code quality/resource management issue for integrators who might misuse the API, but not a security vulnerability in the library itself.\n\nCRUX: The attacker cannot control the control flow of the host application to call NewFunc repeatedly; the instantiation is static and independent of untrusted data.\nGREP: stackless.NewFunc",
    "elapsed": 7.7,
    "tokens": 4290,
    "tokens_in": 3956,
    "tokens_out": 334,
    "cost_usd": 0.00266352,
    "file": "stackless/func.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `stackless.NewFunc`:\n```\nbrotli.go:142:\t\tstacklessWriteBrotliFunc = stackless.NewFunc(nonblockingWriteBrotli)\nzstd.go:122:\t\tstacklessWriteZstdFunc = stackless.NewFunc(nonblockingWriteZstd)\ncompress.go:187:\t\tstacklessWriteGzipFunc = stackless.NewFunc(nonblockingWriteGzip)\ncompress.go:294:\t\tstacklessWriteDeflateFunc = stackless.NewFunc(nonblockingWriteDeflate)\n```"
  },
  {
    "finding_title": "Permanent worker goroutines and channel per NewFunc instance",
    "verdict": "INVALID",
    "reasoning": "The report claims that creating multiple instances of `NewFunc` leads to resource exhaustion because each instance spawns permanent worker goroutines that are never stopped. The code confirms that `NewFunc` creates a channel and starts `GOMAXPROCS(-1)` goroutines via `sync.Once` on the first call, and there is no mechanism to close the channel or stop the goroutines when the returned wrapper is discarded. However, the threat model for this library defines the attacker as an external entity controlling untrusted *data* passed through the public API (e.g., compressed streams, byte slices), not the control flow of the host application. In `swesweep-fasthttp`, `NewFunc` is called in static package-level initialization (e.g., `brotli.go`, `zstd.go`, `compress.go`) to create shared, reusable compression wrappers. The number of instances is fixed at compile time/static initialization and does not depend on the content of incoming HTTP requests or other untrusted input. An external attacker sending malicious or high-volume data cannot influence the number of times `NewFunc` is invoked. Therefore, the resource exhaustion is not reachable through untrusted input under the binding threat model. It is a code quality/resource management issue for integrators who might misuse the API (e.g., by calling `NewFunc` in a loop per request), but not a security vulnerability in the library itself that can be triggered by an external attacker via data.\n\nCRUX: The attacker cannot control the control flow of the host application to call NewFunc repeatedly; the instantiation is static and independent of untrusted data.\nGREP: stackless.NewFunc",
    "elapsed": 6.0,
    "tokens": 4733,
    "tokens_in": 4383,
    "tokens_out": 350,
    "cost_usd": 0.00289086,
    "file": "stackless/func.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `stackless.NewFunc`:\n```\nbrotli.go:142:\t\tstacklessWriteBrotliFunc = stackless.NewFunc(nonblockingWriteBrotli)\nzstd.go:122:\t\tstacklessWriteZstdFunc = stackless.NewFunc(nonblockingWriteZstd)\ncompress.go:187:\t\tstacklessWriteGzipFunc = stackless.NewFunc(nonblockingWriteGzip)\ncompress.go:294:\t\tstacklessWriteDeflateFunc = stackless.NewFunc(nonblockingWriteDeflate)\n```"
  }
]
Raw scan report for this file

Step-by-step security trace:

- `NewFunc` validates that `f` is not nil, creates a bounded worker channel, and lazily starts `runtime.GOMAXPROCS(-1)` worker goroutines. The returned closure is intended to be callable concurrently.
- The returned closure gets a pooled `funcWork`, stores `ctx`, tries to enqueue the work, and blocks on `fw.done` before reusing/returning the pooled object.
- `funcWorker` consumes `funcWork` items, calls `f(fw.ctx)`, then signals completion via `fw.done`.
- `getFuncWork` uses `sync.Pool` and type-asserts `v.(*funcWork)`. This is currently safe because the pool is package-private and only receives `*funcWork` values.
- `putFuncWork` clears `ctx` and returns the object to the pool.

There is no direct memory-corruption bug in this file: no unchecked buffer copies, no slice bounds, no unsafe usage, and no obvious data race on the shared `funcWork` state under normal operation. The main risks are lifecycle, concurrency, and availability issues.

```json
[
  {
    "severity": "medium",
    "title": "Permanent worker goroutines and channel per NewFunc instance",
    "function": "NewFunc",
    "description": "Each NewFunc call creates funcWorkCh and, on first use, starts runtime.GOMAXPROCS(-1) funcWorker goroutines that range over the channel until it is closed. The channel is never closed and the package provides no Close/Stop API. If callers create many wrappers, for example per tenant, per connection, or under attacker influence, the goroutines, channel buffers, captured f, and any state closed over by f are retained for the lifetime of the process. Dropping the returned wrapper does not terminate the workers. This can lead to goroutine and memory exhaustion, causing denial of service."
  },
  {
    "severity": "medium",
    "title": "Hidden worker can be terminated by f panic or runtime.Goexit",
    "function": "funcWorker",
    "description": "funcWorker calls f(fw.ctx) in a separate goroutine and then sends on fw.done. If malformed ctx causes f to panic, an unrecovered panic terminates the host process. If f calls runtime.Goexit, the worker goroutine exits without sending fw.done, leaving the caller blocked forever on <-fw.done. Because execution is moved off the caller's goroutine, panics that might otherwise be recoverable if f were invoked synchronously cannot be recovered by the caller. Untrusted data reaching f can therefore turn a bad input into a process crash or hang."
  },
  {
    "severity": "low",
    "title": "No timeout, cancellation, or execution bound for f",
    "function": "funcWorker",
    "description": "The wrapper blocks on <-fw.done until f returns. If f blocks on channels, I/O, or re-enters a stackless wrapper whose worker pool is saturated, workers can remain busy and callers can hang. There is no maximum execution time, timeout, or cancellation mechanism. The documentation suggests f should not block, but malformed input that causes f to block can still become an availability issue."
  },
  {
    "severity": "low",
    "title": "Attacker-controlled ctx objects retained by bounded queue",
    "function": "NewFunc",
    "description": "The returned wrapper accepts any ctx value and enqueues it in funcWorkCh. While queued or running, ctx and all objects it references are retained. The channel capacity is runtime.GOMAXPROCS(-1)*2048, and GOMAXPROCS can be large. If ctx carries large attacker-controlled payloads and many calls are accepted before workers drain the queue, memory usage can grow substantially. There is no per-ctx size limit, no queue occupancy accounting beyond the channel buffer, and no way to cancel queued work."
  }
]
```