Skip to content

Fix crashing and leak#79

Closed
gabsow wants to merge 5 commits intogal-13438-internal-commands-protocolfrom
fixCrashingAndLeak
Closed

Fix crashing and leak#79
gabsow wants to merge 5 commits intogal-13438-internal-commands-protocolfrom
fixCrashingAndLeak

Conversation

@gabsow
Copy link
Copy Markdown
Contributor

@gabsow gabsow commented Feb 12, 2026

Note

Medium Risk
Touches Execution lifetime/refcounting across the event loop and worker threads; mistakes here could cause use-after-free or leaks, but the change is localized to teardown/scheduling paths.

Overview
Hardens Execution lifetime management by adding explicit refcount ownership rules around MR_DeleteExecution and all paths that schedule deletion (e.g., MR_DropExecution, MR_NotifyDone, local completion in MR_RunExecution, and MR_ExecutionCtxSetDone).

Prevents worker-thread crashes and premature frees by holding a ref while executing task callbacks in MR_ExecutionMain, adding null-guard checks for empty task queues, and splitting cleanup into MR_FreeExecutionInternal for safe final disposal when the refcount reaches zero.

Written by Cursor Bugbot for commit 3e30af5. This will update automatically on new commits. Configure here.

gabsow and others added 2 commits February 12, 2026 15:05
- Check for empty task list (head == NULL) and return to avoid null deref
- Check for null task and return
- Hold execution ref before unlock so it cannot be freed by another thread
  (e.g. MR_DisposeExecution) while we read task->callback and run callback
- Release ref on both exit paths (finalizing callback and normal path)

Co-authored-by: Cursor <cursoragent@cursor.com>
When the callback is MR_DisposeExecution or MR_ExecutionTimedOutInternal,
release our ref after the callback and free the execution if refCount
reaches 0 (we are the last holder). Extract actual free logic into
MR_FreeExecutionInternal so both MR_FreeExecution and MR_ExecutionMain
can invoke it when refCount drops to zero.

Co-authored-by: Cursor <cursoragent@cursor.com>
… loop vs worker)

When the event loop runs MR_DeleteExecution(e), it removes e from the dict
then calls MR_ExecutionAddTask(e, MR_DisposeExecution, NULL). A worker
thread in MR_ExecutionMain can free e (refCount 0) in between, so the
event loop then touches freed e in MR_ExecutionAddTask (crash at 0x28).

Hold a ref in MR_DeleteExecution across the dict delete and add-task so
e cannot be freed until we are done; release the ref after adding the task.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Comment thread src/mr.c
mr_dictDelete(mrCtx.executionsDict, e->id);
/* Send dispose execution task, this will be last task this execution will ever receive. */
MR_ExecutionAddTask(e, MR_DisposeExecution, NULL);
__atomic_sub_fetch(&e->refCount, 1, __ATOMIC_RELAXED);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing free when refCount reaches zero causes memory leak

High Severity

In MR_DeleteExecution, the __atomic_sub_fetch at line 990 doesn't check if refCount reached zero. If a thread pool worker picks up and fully completes the dispose task between MR_ExecutionAddTask returning and this sub_fetch executing, refCount drops to zero here with no call to MR_FreeExecutionInternal, causing a memory leak of the entire Execution and all its owned resources. The return value of the sub_fetch needs to be checked, similar to how MR_ExecutionMain handles it at line 1330.

Fix in Cursor Fix in Web

gabsow and others added 2 commits February 12, 2026 16:13
MR_DeleteExecution can be scheduled via MR_EventLoopAddTask. By the time
the event loop runs it, a worker in MR_ExecutionMain may have freed e
(refCount 0), so MR_DeleteExecution(e) then touches freed e in
MR_ExecutionAddTask (crash at 0x28 on macOS 26).

Take a ref at the two schedule sites (MR_RunExecution for Local execution,
MR_ExecutionCtxSetDone) so e stays alive until the scheduled task runs.
MR_DeleteExecution releases that ref with sub_fetch at the end.

Co-authored-by: Cursor <cursoragent@cursor.com>
…xecution only releases

Remove the add-ref at start of MR_DeleteExecution; it didn't help when the
task was scheduled (e could already be freed when the task ran). Rely on
callers to hold a ref: direct callers (MR_DropExecution, MR_NotifyDone path)
now take a ref before MR_DeleteExecution; schedule sites already did. So
MR_DeleteExecution just does dictDelete, AddTask, sub_fetch.

Co-authored-by: Cursor <cursoragent@cursor.com>
@gabsow gabsow closed this Feb 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant