Protect against dangling pointer in syncRequest#2513
Conversation
syncRequest creates a context that lives on the function's stack. This "works" because the transfer is only expected to live during the call to syncRequest (this is the entire premise of syncRequest). But if self.tick returns an error, the function returns, potentially with transfer.req.ctx still pointing to the stack value. This change captures a tick error and, if the sync request is still in_progress, aborts the transfer. There's maybe a world where the error is recoverable and the request could continue, but that seems error prone. AND, the most likely non-transfer error that tick would return are pretty "serious", e.g. OOM or client disconnected.
|
Note Verified this one fairly closely since it's a UAF fix — it holds up. The guard
The fix is correct — here's the path I tracedThe risk is real: flowchart TD
T["tick(200) errors"] --> G{"sync_ctx.completion<br/>still in_progress?"}
G -->|"no — transfer already completed<br/>(possibly freed this tick)"| R["return err<br/>(never touches transfer)"]
G -->|"yes — transfer still pending"| A["transfer.abort(err)"]
A --> RF["requestFailed → error_callback(&sync_ctx)<br/>fires synchronously — stack still valid ✓"]
A --> D{"must_defer?<br/>_performing OR (performing & conn)"}
D -->|"no"| DE["deinit() — transfer freed inline"]
D -->|"yes — curl mid-flight"| DI["detachInPerform: noop ALL<br/>ctx callbacks, unlink owner,<br/>defer deinit to processOneMessage"]
DE --> S["return err — no live ref to &sync_ctx ✓"]
DI --> S
R --> S2["return err ✓"]
Two things I wanted to confirm and did:
One behavioral questionIn the NitThe |
| pub fn request(self: *Client, req: Request, owner: ?*Owner) !void { | ||
| _ = try self.requestT(req, owner); | ||
| } |
There was a problem hiding this comment.
The public request lost its ownership-contract doc when the body moved into requestT. Since request is the entry point external callers actually read, a one-line pointer keeps the no-double-free contract discoverable:
| pub fn request(self: *Client, req: Request, owner: ?*Owner) !void { | |
| _ = try self.requestT(req, owner); | |
| } | |
| // Thin wrapper over `requestT`. See `requestT` for the ownership contract: | |
| // the HttpClient takes ownership of `req` (notably `req.headers`), so callers | |
| // must NOT add their own `errdefer headers.deinit()` — that would double-free. | |
| pub fn request(self: *Client, req: Request, owner: ?*Owner) !void { | |
| _ = try self.requestT(req, owner); | |
| } |
syncRequest creates a context that lives on the function's stack. This "works" because the transfer is only expected to live during the call to syncRequest (this is the entire premise of syncRequest). But if self.tick returns an error, the function returns, potentially with transfer.req.ctx still pointing to the stack value.
This change captures a tick error and, if the sync request is still in_progress, aborts the transfer. There's maybe a world where the error is recoverable and the request could continue, but that seems error prone. AND, the most likely non-transfer error that tick would return are pretty "serious", e.g. OOM or client disconnected.