BudgetScan

Hidden worker can be terminated by f panic or runtime.Goexit

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

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.

Triage

Verdict
INVALID confidence 0.17
Model
qwen/qwen3.8-27b
Rounds
6
Cost
$0.0272 · 80.4 s
[ARBITER] The reported issue describes standard Go runtime behavior (unrecovered panics in goroutines terminate the process) applied to a library component (`stackless.NewFunc`) that executes user-provided callbacks. Under the specified threat model for libraries, the attacker controls the data passed to the public API, not the code of the callback function `f` itself. The `stackless` package is a utility wrapper; it does not receive untrusted external input directly but rather the context provided by the host application. If the host application's callback `f` (e.g., a parser) fails to handle malformed data safely (by panicking or calling `runtime.Goexit` without recovery), that is a defect in the host application's error handling or input validation, not a vulnerability in the `stackless` library. The library is not responsible for protecting against panics initiated by the trusted code (the host's callback) it is asked to execute. Furthermore, `fasthttp`'s public server logic typically includes recovery mechanisms for handler panics, and the `stackless` worker is an internal optimization detail not directly exposed to untrusted data processing in a way that makes the library itself vulnerable to attack vectors.
All rounds
[
  {
    "finding_title": "Hidden worker can be terminated by f panic or runtime.Goexit",
    "verdict": "VALID",
    "reasoning": "The vulnerability report claims that a panic in the user-supplied function `f` will terminate the host process because it occurs in a separate goroutine (`funcWorker`) without a `recover` call, and that `runtime.Goexit` will cause a deadlock. \n\n1. **Analysis of the Bug Pattern**: In Go, if a goroutine panics and the panic is not recovered, the entire process terminates. This is standard Go runtime behavior. The `funcWorker` function executes `f(fw.ctx)` inside a goroutine. If `f` panics, that panic propagates up the stack of the worker goroutine. Since there is no `recover()` call in `funcWorker` or anywhere in the `stackless` package that wraps the call to `f`, the panic is not contained. Consequently, the process crashes. \n2. **Attacker Reachability**: The threat model defines the attacker as code calling the public API (`NewFunc`). The input `ctx` is passed to `f`. If the host application passes attacker-controlled data in `ctx` to `f`, and `f` processes this data in a way that causes a panic (e.g., slicing out of bounds, type assertion failure, nil pointer dereference), the panic will occur in the worker goroutine. \n3. **Security Impact**: A crash (process termination) is a denial-of-service vulnerability. For a library like `swesweep-fasthttp`, which is likely used in a web server context, a crash caused by malformed input is a significant security issue. \n4. **Defense Verification**: The code does not contain a `recover` statement in `funcWorker`. The `NewFunc` function creates workers that directly call `f`. There is no wrapper around `f` that catches panics. Therefore, no defense exists to mitigate this specific panic scenario. \n5. **Deadlock Scenario**: The report also mentions `runtime.Goexit`. If `f` calls `runtime.Goexit`, the goroutine terminates without sending to `fw.done`, causing the caller to block forever. This is also a valid DoS vector (hang). \n\nThe bug is real: the library does not isolate panics from user-provided functions. An attacker can trigger a panic (or hang) by providing malicious input to the function `f` via the `ctx` argument. This leads to process crash or resource exhaustion (deadlock). Thus, it is a valid security vulnerability under the threat model.\n\nCRUX: The funcWorker goroutine executes the user-supplied function f without a recover call, so any panic in f (triggered by untrusted ctx) will crash the host process.\nGREP: recover",
    "elapsed": 12.4,
    "tokens": 2999,
    "tokens_in": 2426,
    "tokens_out": 573,
    "cost_usd": 0.00273792,
    "file": "stackless/func.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `recover`:\n```\nfasthttpadaptor/adaptor.go:66:\t\t\t\tif rec := recover(); rec != nil {\nfasthttpadaptor/adaptor_test.go:699:\t\trecover() //nolint:errcheck\nprefork/prefork_test.go:282:\t\t\trecord(\"recover\", oldPid, newPid)\nprefork/prefork_test.go:297:\tvar recoverCount int\nprefork/prefork_test.go:310:\t\tcase \"recover\":\nprefork/prefork_test.go:311:\t\t\trecoverCount++\nprefork/prefork_test.go:313:\t\t\t\tt.Errorf(\"recover event has invalid PIDs: %v\", e.pids)\nprefork/prefork_test.go:316:\t\t\t\tt.Error(\"recover old and new PID should differ\")\nprefork/prefork_test.go:327:\t// Initial spawns + at least one recovery spawn\nprefork/prefork_test.go:332:\tif recoverCount == 0 {\nprefork/prefork_test.go:355:\trecoveredSpawnByPID := make(map[int]bool)\nprefork/prefork_test.go:356:\trecoveredPIDs := make(map[int]bool)\nprefork/prefork_test.go:359:\t\t\trecoveredSpawnByPID[e.pids[0]] = true\nprefork/prefork_test.go:361:\t\tif e.name == \"recover\" {\nprefork/prefork_test.go:362:\t\t\tif !recoveredSpawnByPID[e.pids[1]] {\nprefork/prefork_test.go:365:\t\t\trecoveredPIDs[e.pids[1]] = true\nprefork/prefork_test.go:368:\tfor pid := range recoveredPIDs {\nprefork/prefork_test.go:369:\t\tif !recoveredSpawnByPID[pid] {\nprefork/prefork_test.go:383:\tvar recoverCount int\nprefork/prefork_test.go:406:\t\t\trecoverCount++\nprefork/prefork_test.go:414:\tif recoverCount != 0 {\nprefork/prefork_test.go:415:\t\tt.Fatalf(\"OnChildRecover called %d times, want 0\", recoverCount)\nprefork/prefork.go:373:\t\t\t\tp.logger().Printf(\"OnChildSpawn callback failed for recovered PID %d: %v\\n\", newPid, err)\nserver_test.go:4221:\t\t\t// This will panic, but I was not able to intercept with recover()\nserver.go:166:\t// Take into account that no `panic` recovery is done by `fasthttp` (thus any `panic` will take down the entire server).\nserver.go:167:\t// Instead the user should use `recover` to handle these situations.\nhttp.go:2194:\t\tif r := recover(); r != nil {\n```"
  },
  {
    "finding_title": "Hidden worker can be terminated by f panic or runtime.Goexit",
    "verdict": "INVALID",
    "reasoning": "The report claims that because `funcWorker` executes `f` without a `recover` call, a panic in `f` will crash the process, and because it is in a separate goroutine, it is un-recoverable by the caller. However, this claim confuses the location of the bug pattern with its exploitability. In Go, a panic in *any* goroutine (including the caller's original goroutine, had `f` been called synchronously) terminates the process if not recovered. Moving execution to a worker goroutine does not introduce a *new* class of failure (process crash) that wasn't already present if the function panicked synchronously. It merely moves the panic location. The `stackless` package is a performance optimization utility; it does not introduce new trust boundaries. The threat model for this library focuses on the library itself handling untrusted input (e.g., via public parsers) safely. Here, `f` is a function provided by the *host application*. If the host application provides a buggy `f` that panics on malformed input, that is a defect in the host application, not the library. The library correctly passes the `ctx` to `f`; the failure occurs inside `f`'s logic. The library is not responsible for recovering from panics in arbitrary user-supplied callbacks unless it is a sandboxing layer, which it is not documented to be (it is a stack-saving utility). Furthermore, the 'deadlock' via `runtime.Goexit` is a misuse of the API by `f`; `f` is not allowed to call `runtime.Goexit` in a way that breaks the promise to return (or panic). Since the 'untrusted input' must flow through `f` to cause the issue, and `f` is trusted host code, the library itself is not vulnerable to an external attacker who cannot control `f`'s implementation or the specific logic of how it handles `ctx` beyond what the host intended. The host is responsible for ensuring `f` doesn't panic. Thus, this is not a security vulnerability in the library itself.\n\nCRUX: The panic occurs in the user-supplied function `f`, which is controlled by the host application, not the library; the library merely executes it, so a bug in `f`'s error handling is a host application defect, not a library vulnerability.\nGREP: NewFunc",
    "elapsed": 8.6,
    "tokens": 4299,
    "tokens_in": 3788,
    "tokens_out": 511,
    "cost_usd": 0.0031239600000000003,
    "file": "stackless/func.go",
    "round": 2,
    "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": "Hidden worker can be terminated by f panic or runtime.Goexit",
    "verdict": "UNCERTAIN",
    "reasoning": "{\"reasoning\":\"The report claims that a panic in `f` (caused by untrusted `ctx`) will crash the host process because `funcWorker` does not recover it, and that `runtime.Goexit` will cause a deadlock. However, I must evaluate this against the threat model for a **library** (swesweep-fasthttp). The threat model states that the attacker controls data passed through public APIs (like parsers and decoders). \\n\\n1. **Analysis of Attacker Reachability**: The `stackless` package is an internal utility in fasthttp used to offload stack-heavy operations (like brotli, zstd, gzip compression) to worker goroutines. The function `f` passed to `NewFunc` is **internal library code** (e.g., `nonblockingWriteBrotli`, `writerFunc`), not user-supplied code. The untrusted input enters via the `ctx` parameter, which contains data to be compressed/encoded. \\n\\n2. **Existing Defenses (Recover)**: In Go web servers like fasthttp, the main request handling loop typically wraps the request processing in a `recover()` block to handle panics caused by malformed input or bugs in the compression logic. Even if a panic occurs in the `stackless` worker, it would terminate the process *unless* the library or the host application recovers it. However, looking at `fasthttpadaptor/adaptor.go`, we see explicit `recover()` calls. More importantly, in the standard fasthttp model, the request handler (which eventually calls the compression functions) is often wrapped or the panic is considered a crash to be handled by the operator. \\n\\n3. **Crux: Is the library responsible for isolating panics in its own internal compression logic?** The threat model for a library says: \\\"A crash... reachable through the public API with a crafted payload is VALID.\\\" If a malicious brotli/zstd payload can cause `nonblockingWriteBrotli` to panic (e.g., due to a bug in the brotli decompressor or encoder), and that panic is not recovered, the host process crashes. \\n\\nHowever, we must look closer at the specific claim: \\\"Hidden worker can be terminated by f panic... leaving the caller blocked forever... process crash.\\\" \\n\\nLet's look at the `funcWorker` implementation again:\\n```go\\nfunc funcWorker(funcWorkCh <-chan *funcWork, f func(ctx any)) {\\n    for fw := range funcWorkCh {\\n        f(fw.ctx)\\n        fw.done <- struct{}{}\\n    }\\n}\\n```\\nIf `f` panics, the worker goroutine dies. The `select` in the caller (`NewFunc` wrapper) blocks on `<-fw.done`. Since the worker is dead, `fw.done` is never signaled. The caller blocks forever. This is a **deadlock** (Hang), not just a crash. If the worker dies, other requests might still be processed if there are other workers, but the specific caller hangs. If all workers die (due to repeated panics), the entire server hangs. \\n\\nBut is the panic *reachable*? The `f` functions are `nonblockingWriteBrotli`, `nonblockingWriteZstd`, etc. These are internal fasthttp functions. If the brotli/zstd libraries used by fasthttp are robust, they shouldn't panic on valid input. But if they *do* panic on malformed input (which is a security requirement for parsers/decoders), and the `stackless` wrapper does not recover the panic, the consequence is a hang (deadlock on `<-fw.done`) rather than just a crash. \\n\\nWait, if `f` panics, the worker dies. The caller hangs. The host process is still alive, but that specific goroutine (request handler) is stuck. If many requests cause panics, all workers die, and all subsequent requests hang (the channel `funcWorkCh` is not full, but no workers are reading from it, so `select { case funcWorkCh <- fw: ... }` would block forever? No, `funcWorkCh` has a buffer. If workers are dead, the buffer fills up. Then `funcWorkCh <- fw` blocks? No, it uses `select` with `default`. If the buffer is full and no worker is there, it goes to `default` and returns `false`. So it won't deadlock immediately. It will degrade to synchronous execution or failure. \\n\\nLet's re-read the `NewFunc` wrapper:\\n```go\\n    select {\\n    case funcWorkCh <- fw:\\n    default:\\n        putFuncWork(fw)\\n        return false\\n    }\\n    <-fw.done\\n```\\nIf the worker panics, `fw.done` is never sent. The caller blocks on `<-fw.done`. \\nIf *another* request comes in, it will try to send to `funcWorkCh`. If the buffer is empty (because the worker died and no other worker is there, assuming `GOMAXPROCS` workers), it will succeed. The new request will be processed by a *different* worker? No, if the worker that panicked is dead, it is gone. The loop `for fw := range funcWorkCh` in `funcWorker` is in a single goroutine. If it panics, that goroutine dies. The `onceInit` starts `n` goroutines. If one dies, it stays dead. \\n\\nSo, if a panic occurs in one worker, that worker is gone. The pool has `n-1` workers. The request that triggered the panic hangs forever (blocked on `<-fw.done`). This is a **Denial of Service** (specific request hangs, resource leak). \\n\\nIs this a **security vulnerability** in the library? \\nThe threat model says: \\\"A crash... reachable through the public API with a crafted payload is VALID.\\\" A hang is also a DoS. \\nThe key is whether `f` (the internal compression function) *actually* panics on untrusted input. If `fasthttp`'s brotli/zstd implementations are safe (i.e., they return errors, not panics), then the bug is not reachable. \\n\\nHowever, the report is about the **stackless wrapper** itself. The vulnerability is that the wrapper **does not recover** panics. If *any* bug in the internal `f` leads to a panic (which is a security concern for parsers), the result is a hang/crash rather than a controlled error. \\n\\nBut Reviewer 2 argues that `f` is trusted internal code. Under the library threat model, \\\"Misuse that violates a documented precondition... is not a vulnerability.\\\" But here, the input is untrusted. The internal code (`f`) processes untrusted input. If `f` has a bug that panics, the library should ideally handle it gracefully (return an error) rather than hanging the caller. \\n\\nLet's check if there is a `recover` in the specific `stackless` context or if the host app recovers. The host app (fasthttp server) usually recovers from panics in request handlers. But the hang occurs *inside* the request handler, blocking it. The `recover` in the outer scope of the request handler wouldn't help if the handler is stuck on `<-fw.done`. It would just hold the connection open. \\n\\nTherefore, a panic in `f` leads to a hung request. If an attacker can trigger a panic in `f` via untrusted `ctx`, they can cause requests to hang. \\n\\nIs `f` likely to panic? `nonblockingWriteBrotli` calls into the brotli encoder/decoder. If the brotli library is written in C (swig/foreign), panics might not be the mode of failure (C errors are checked). If it's pure Go, it might panic on malformed state. \\n\\nHowever, there is a simpler argument: **The code does not defend against panics.** The threat model requires the library to handle untrusted input safely. A panic in a worker caused by untrusted input, leading to a hang, is a failure to handle input safely. \\n\\nBut wait, look at `brotli.go:142`. `stacklessWriteBrotliFunc = stackless.NewFunc(nonblockingWriteBrotli)`. If `nonblockingWriteBrotli` panics, the worker dies. \\n\\nLet's consider the **INVALID** case again. Is this a \\\"code quality issue\\\"? No, a hang is a security issue. \\nIs it \\\"not attacker-reachable\\\"? The attacker controls `ctx`. `ctx` is passed to `f`. If `f` panics, the attacker reached the bug. \\n\\nThe only defense is if `f` *never* panics. I cannot verify that `f` never panics. In fact, security best practices for parsers/decoders are to *not* panic. If they do, the library should contain the failure. \\n\\nHowever, there is a nuance. The `stackless` package is a **performance optimization**. It is not a security sandbox. The primary responsibility for handling malformed input is the parser/decoder (`f`). If `f` is broken, it's broken. The `stackless` wrapper just makes the failure mode worse (hang instead of immediate return of error/panic propagation). \\n\\nComparing to Reviewer 2: \\\"The library is not responsible for recovering from panics in arbitrary user-supplied callbacks...\\\" But `f` is not user-supplied, it is library-internal. However, it processes user data. \\n\\nLet's look at the **deadlock** aspect. If `f` panics, the worker dies. The caller hangs. This is a **resource exhaustion** vector. An attacker can send 1 malformed request to kill 1 worker and hang 1 request. They can repeat this to kill all workers and hang all requests. This is a valid DoS. \\n\\nTherefore, the lack of `recover` in `funcWorker` is a security vulnerability because it allows a panic in internal code (triggered by untrusted input) to escalate to a hang/resource leak. \\n\\nVerdict: **VALID**.\\n\\nCrux: The `funcWorker` does not recover panics in `f`, so a panic in internal compression code triggered by untrusted `ctx` will kill the worker and hang the caller, leading to resource exhaustion (DoS).\\n\\nGrep: `recover` in `stackless` package to confirm absence.\\n\\nWait, I should check if `recover` is present in `stackless/func.go`. The code snippet shows it is not. \\n\\nOne counter-argument: In Go, if a goroutine panics, the process crashes. It does not \"}",
    "elapsed": 33.6,
    "tokens": 6356,
    "tokens_in": 4033,
    "tokens_out": 2323,
    "cost_usd": 0.00866286,
    "file": "stackless/func.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `stackless`:\n```\nbrotli.go:11:\t\"github.com/valyala/fasthttp/stackless\"\nbrotli.go:44:func acquireStacklessBrotliWriter(w io.Writer, level int) stackless.Writer {\nbrotli.go:46:\tp := stacklessBrotliWriterPoolMap[nLevel]\nbrotli.go:49:\t\treturn stackless.NewWriter(w, func(w io.Writer) stackless.Writer {\nbrotli.go:53:\tsw := v.(stackless.Writer)\nbrotli.go:58:func releaseStacklessBrotliWriter(sw stackless.Writer, level int) {\nbrotli.go:61:\tp := stacklessBrotliWriterPoolMap[nLevel]\nbrotli.go:86:\tstacklessBrotliWriterPoolMap = newCompressWriterPoolMap()\nbrotli.go:119:\t\t// These writers don't block, so we can just use stacklessWriteBrotli\nbrotli.go:125:\t\tstacklessWriteBrotli(ctx)\nbrotli.go:136:\tstacklessWriteBrotliOnce sync.Once\nbrotli.go:137:\tstacklessWriteBrotliFunc func(ctx any) bool\nbrotli.go:140:func stacklessWriteBrotli(ctx any) {\nbrotli.go:141:\tstacklessWriteBrotliOnce.Do(func() {\nbrotli.go:142:\t\tstacklessWriteBrotliFunc = stackless.NewFunc(nonblockingWriteBrotli)\nbrotli.go:144:\tstacklessWriteBrotliFunc(ctx)\nstackless/writer_test.go:1:package stackless\nstackless/func.go:1:package stackless\nstackless/func.go:8:// NewFunc returns stackless wrapper for the function f.\nstackless/func.go:10:// Unlike f, the returned stackless wrapper doesn't use stack space\nstackless/func.go:19:// The stackless wrapper returns false if the call cannot be processed\nstackless/func_timing_test.go:1:package stackless\nstackless/func_test.go:1:package stackless\nstackless/writer.go:1:package stackless\nstackless/writer.go:12:// Writer is an interface stackless writer must conform to.\nstackless/writer.go:23:// stackless writer.\nstackless/writer.go:26:// NewWriter creates a stackless writer around a writer returned\nstackless/writer.go:31:// Writers that use a lot of stack space may be wrapped into stackless writer,\nstackless/writer.go:93:\tif !stacklessWriterFunc(w) {\nstackless/writer.go:111:\tstacklessWriterFuncOnce sync.Once\n```"
  },
  {
    "finding_title": "Hidden worker can be terminated by f panic or runtime.Goexit",
    "verdict": "INVALID",
    "reasoning": "The report claims that a panic in the function `f` executed by `funcWorker` will terminate the host process because there is no `recover` call, and that `runtime.Goexit` will cause a deadlock. While the code analysis is partially correct in that `funcWorker` does not call `recover`, the security impact assessment is flawed under the specified threat model.\n\n1. **Go Runtime Behavior**: In Go, if a panic occurs in any goroutine (worker or main) and is not recovered by a `recover` call in that specific goroutine's stack, the **entire process terminates**. It does *not* just kill the worker goroutine and leave others running. Therefore, the claim that \"the worker can be terminated... leaving the caller blocked forever\" (implying the process stays alive but hangs) is incorrect for a panic. A panic leads to an immediate process crash (SIGABRT/abort), which is a denial of service, but it is indistinguishable from a panic that would occur if `f` were called synchronously on the main goroutine.\n\n2. **Responsibility and Trust Boundary**: The `stackless` package is a performance utility. The function `f` is provided by the caller (the host application, e.g., `nonblockingWriteBrotli` in fasthttp). The threat model states that the attacker controls the *data* passed through the public API (`ctx`), not the *code* of the callback (`f`). \n   - If the internal library code (`nonblockingWriteBrotli`) has a bug that causes it to panic on malicious input, that is a vulnerability in that specific parser/decoder (e.g., the brotli implementation), not a vulnerability in the `stackless` wrapper. The `stackless` wrapper is merely the execution environment. \n   - If the host application provides a malicious or buggy `f`, that is a violation of the trust boundary (attacker controlling code execution logic), which is out of scope for a data-input vulnerability.\n\n3. **Deadlock/Goexit**: The `runtime.Goexit` scenario is a misuse of the API. `f` is expected to return. If `f` calls `Goexit`, it violates the contract of the function signature (implicit return). This is a code quality/programming error, not a security vulnerability exploitable by an external attacker sending data, unless the attacker can inject code into `f` (which is impossible in a standard memory-safe execution model where `f` is a compiled binary or static function pointer).\n\n4. **Conclusion**: The lack of `recover` in `funcWorker` does not introduce a *new* security vulnerability. It simply propagates the failure mode (crash) of the callback `f` to the process. If `f` is vulnerable to crashing on bad input, `f` should be fixed. Wrapping it in `stackless` does not make it *more* vulnerable to external data attacks than it was before; it just moves the execution context. Since the attacker cannot control `f`'s logic, only its input, and the crash is a result of `f`'s inability to handle that input, the root cause is in `f`, not the `stackless` wrapper. Thus, this is not a valid security vulnerability in the `stackless` package itself.\n\nThe finding is INVALID because the crash/hang is a consequence of the callback's behavior, not a flaw in the library's handling of untrusted input. The library correctly passes input; the callback fails. The library is not a sandbox.\n\nCRUX: In Go, an unrecovered panic in any goroutine terminates the entire process, so the 'worker dies, caller hangs' scenario described is factually incorrect for panics (it crashes instead). Furthermore, the vulnerability (if any) resides in the callback function `f` (the parser/decoder), not in the `stackless` utility which merely executes it.\nGREP: panic",
    "elapsed": 13.6,
    "tokens": 7612,
    "tokens_in": 6763,
    "tokens_out": 849,
    "cost_usd": 0.00538746,
    "file": "stackless/func.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `panic`:\n```\nfasthttpadaptor/adaptor.go:67:\t\t\t\t\tctx.Logger().Printf(\"panic in net/http handler: %v\", rec)\nfasthttpadaptor/adaptor.go:180:\t\t\tpanic(\"net/http handler panicked\")\nfasthttpadaptor/adaptor.go:250:\t\tpanic(fmt.Sprintf(\"invalid WriteHeader code %v\", code))\nfasthttpadaptor/adaptor_test.go:694:\t\tpanic(\"test panic\")\nfasthttpadaptor/adaptor_test.go:704:\tt.Error(\"expected panic, but it didn't happen\")\nexpvarhandler/expvar_test.go:18:\t// Publish panics if the same var is published more than once,\nREADME.md:519:// this for loop doesn't panic if src is nil\nprefork/README.md:25:    panic(err)\nfasthttputil/inmemory_listener_test.go:190:\t\t\tpanic(err)\nfasthttputil/inmemory_listener_test.go:205:\t\t\tpanic(err)\nfasthttputil/inmemory_listener_test.go:248:\t\t\tpanic(err)\nstackless/writer_test.go:33:\t\t\tpanic(fmt.Sprintf(\"BUG: unexpected error: %v\", err))\nstackless/writer_test.go:63:\t\t\tpanic(fmt.Sprintf(\"BUG: cannot create gzip reader: %v\", err))\nstackless/func.go:24:\t\tpanic(\"BUG: f cannot be nil\")\nstackless/writer.go:135:\t\tpanic(fmt.Sprintf(\"BUG: unexpected op: %d\", w.op))\nbytesconv.go:254:\t\tpanic(\"BUG: int must be positive\")\nbytesconv.go:382:\t\tpanic(\"BUG: int must be positive\")\nperipconn.go:33:\t\tpanic(\"BUG: perIPConnCounter.Register() wasn't called\")\nserver_test.go:948:\t\tpanic(fmt.Sprintf(\"BUG: unexpected error: %v\", err))\nserver_test.go:4221:\t\t\t// This will panic, but I was not able to intercept with recover()\nuri.go:846:\t\t\tpanic(fmt.Sprintf(\"BUG: path must contain at least one slash: %q %q\", u.Path(), newURI))\nfuzz_test.go:57:\t\tpanic(err)\nlbclient.go:89:\t\tpanic(\"BUG: LBClient.Clients cannot be empty\")\nclient.go:1549:\t\tpanic(\"BUG: req cannot be nil\")\nclient.go:1553:\t\tpanic(\"BUG: resp cannot be nil\")\nclient.go:2240:\t\tpanic(\"fasthttp: internal error: misuse of tryDeliver\")\nargs.go:74:// behavior and can cause panics.\nzstd.go:72:\t\t\tpanic(err)\ntimer.go:14:\t\tpanic(\"BUG: active timer trapped into initTimer()\")\nheader_test.go:224:\t// Input generated by fuzz testing that caused the parser to panic.\n```"
  },
  {
    "finding_title": "Hidden worker can be terminated by f panic or runtime.Goexit",
    "verdict": "INVALID",
    "reasoning": "The report identifies a flaw in `stackless/func.go` where `funcWorker` executes the callback `f` without a `recover` guard. In Go, an unrecovered panic in any goroutine terminates the entire process. While the code pattern is real (absence of `recover`), the threat model for a library defines the attacker as an entity controlling the *data* passed through the public API (untrusted input/payloads), not the *code* of the callback function itself. The function `f` is provided by the host application or the library's internal logic (e.g., `nonblockingWriteBrotli`). If `f` panics on malformed input, the root cause is a bug in the logic of `f` (e.g., the brotli decompressor) failing to handle untrusted data safely, not a vulnerability in the `stackless` execution wrapper. The `stackless` package is a performance utility (stack optimization), not a security sandbox. It does not introduce a new trust boundary; it merely changes the execution context of code that is already responsible for validating the input it receives. The 'crash' impact is a consequence of the callback's defective handling of data, which would result in a crash (or potentially a recoverable error if handled correctly within `f`) regardless of whether `stackless` is used. Therefore, the lack of `recover` in the worker is a code quality/design choice for a utility library, not a security vulnerability exploitable by an external attacker under the defined threat model.\n\nCRUX: The panic originates in the callback function `f` (the parser/decoder logic), which is responsible for handling untrusted data; the `stackless` wrapper is merely the execution environment and does not introduce a new attack vector or trust boundary violation.\nGREP: recover",
    "elapsed": 7.2,
    "tokens": 8114,
    "tokens_in": 7723,
    "tokens_out": 391,
    "cost_usd": 0.004416659999999999,
    "file": "stackless/func.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `recover`:\n```\nfasthttpadaptor/adaptor.go:66:\t\t\t\tif rec := recover(); rec != nil {\nfasthttpadaptor/adaptor_test.go:699:\t\trecover() //nolint:errcheck\nprefork/prefork_test.go:282:\t\t\trecord(\"recover\", oldPid, newPid)\nprefork/prefork_test.go:297:\tvar recoverCount int\nprefork/prefork_test.go:310:\t\tcase \"recover\":\nprefork/prefork_test.go:311:\t\t\trecoverCount++\nprefork/prefork_test.go:313:\t\t\t\tt.Errorf(\"recover event has invalid PIDs: %v\", e.pids)\nprefork/prefork_test.go:316:\t\t\t\tt.Error(\"recover old and new PID should differ\")\nprefork/prefork_test.go:327:\t// Initial spawns + at least one recovery spawn\nprefork/prefork_test.go:332:\tif recoverCount == 0 {\nprefork/prefork_test.go:355:\trecoveredSpawnByPID := make(map[int]bool)\nprefork/prefork_test.go:356:\trecoveredPIDs := make(map[int]bool)\nprefork/prefork_test.go:359:\t\t\trecoveredSpawnByPID[e.pids[0]] = true\nprefork/prefork_test.go:361:\t\tif e.name == \"recover\" {\nprefork/prefork_test.go:362:\t\t\tif !recoveredSpawnByPID[e.pids[1]] {\nprefork/prefork_test.go:365:\t\t\trecoveredPIDs[e.pids[1]] = true\nprefork/prefork_test.go:368:\tfor pid := range recoveredPIDs {\nprefork/prefork_test.go:369:\t\tif !recoveredSpawnByPID[pid] {\nprefork/prefork_test.go:383:\tvar recoverCount int\nprefork/prefork_test.go:406:\t\t\trecoverCount++\nprefork/prefork_test.go:414:\tif recoverCount != 0 {\nprefork/prefork_test.go:415:\t\tt.Fatalf(\"OnChildRecover called %d times, want 0\", recoverCount)\nprefork/prefork.go:373:\t\t\t\tp.logger().Printf(\"OnChildSpawn callback failed for recovered PID %d: %v\\n\", newPid, err)\nserver_test.go:4221:\t\t\t// This will panic, but I was not able to intercept with recover()\nserver.go:166:\t// Take into account that no `panic` recovery is done by `fasthttp` (thus any `panic` will take down the entire server).\nserver.go:167:\t// Instead the user should use `recover` to handle these situations.\nhttp.go:2194:\t\tif r := recover(); r != nil {\n```"
  },
  {
    "verdict": "INVALID",
    "reasoning": "[ARBITER] The reported issue describes standard Go runtime behavior (unrecovered panics in goroutines terminate the process) applied to a library component (`stackless.NewFunc`) that executes user-provided callbacks. Under the specified threat model for libraries, the attacker controls the data passed to the public API, not the code of the callback function `f` itself. The `stackless` package is a utility wrapper; it does not receive untrusted external input directly but rather the context provided by the host application. If the host application's callback `f` (e.g., a parser) fails to handle malformed data safely (by panicking or calling `runtime.Goexit` without recovery), that is a defect in the host application's error handling or input validation, not a vulnerability in the `stackless` library. The library is not responsible for protecting against panics initiated by the trusted code (the host's callback) it is asked to execute. Furthermore, `fasthttp`'s public server logic typically includes recovery mechanisms for handler panics, and the `stackless` worker is an internal optimization detail not directly exposed to untrusted data processing in a way that makes the library itself vulnerable to attack vectors.",
    "round": 6,
    "file": "stackless/func.go",
    "finding_title": "Hidden worker can be terminated by f panic or runtime.Goexit",
    "elapsed": 5.0,
    "tokens_in": 4910,
    "tokens_out": 260,
    "cost_usd": 0.0028421999999999996
  }
]
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."
  }
]
```