Skip to content

Harden checks for iChID - #3909

Open
pljones wants to merge 1 commit into
jamulussoftware:mainfrom
pljones:bugfix/use-chidmutex-for-all-ichid-use
Open

Harden checks for iChID#3909
pljones wants to merge 1 commit into
jamulussoftware:mainfrom
pljones:bugfix/use-chidmutex-for-all-ichid-use

Conversation

@pljones

@pljones pljones commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Short description of changes

Fixes a potential server crash. (Raised by @mcfnord)

CHANGELOG: Bugfix: Remove potential server crash when recording

Context: Fixes an issue?

N/A

Does this change need documentation? What needs to be documented and how?

No.

Status of this Pull Request

No functional change. I've not run the tests that confirmed the issue.

What is missing until this pull request can be merged?

Verification the fix on the branch works as intended would be good.

Checklist

  • I've verified that this Pull Request follows the general code principles
  • I tested my code and it does what I want See above
  • My code follows the style guide
  • I waited some time after this Pull Request was opened and all GitHub checks completed without errors.
  • I've filled all the content above

@pljones

pljones commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

As a taste of another LLM's review style, here's what Cursor Composer 2.5 Fast wrote in response to

Take a look at the diff to main and let me know if

  • the possibility of nullptr causing a crash is resolved by the guard in CJamSession::DisconnectClient
  • moving the Mutex in CJamRecorder::OnDisconnected has any negative side effects
  • if any other use of iChId needs guarding

Here is a review of commit 0fc8ee50 against the three questions.

1. Does the nullptr guard in DisconnectClient fix the crash?

Yes, for the scenarios that would actually dereference a null pointer.

Before the change, DisconnectClient unconditionally did:

vecptrJamClients[iChID]->Disconnect();

That crashes whenever the slot is already nullptr, which can happen in several legitimate orderings on the recorder thread:

Scenario How nullptr arises
Disconnect before first recorded frame Client connects and leaves before any AudioFrame creates a CJamClient
Frame() already disconnected Address/channel change path calls DisconnectClient, then sets nullptr
End() already cleaned up Last client triggers StoppedOnEnd()End() disconnects everyone; a queued ClientDisconnected arrives later
Duplicate disconnect First call deletes the client; second call sees nullptr

The guard prevents the crash in all of those cases.

2. Does moving the mutex in OnDisconnected have negative side effects?

No meaningful ones. The move looks intentional and aligns with OnFrame.

Old pattern:

QMutexLocker ...;   // held for entire function
if (!isRecording) warn;
if (currentSession == nullptr) return;
DisconnectClient(...);

New pattern:

if (!isRecording) warn;
if (currentSession == nullptr) return;
QMutexLocker ...;   // held only for mutation
DisconnectClient(...);

This mirrors OnFrame, which deliberately calls Start() (and thus OnEnd()) before taking ChIdMutex:

    if ( !isRecording )
    {
        Start();
    }
    // ...
    {
        QMutexLocker mutexLocker ( &ChIdMutex );
        currentSession->Frame ( iChID, name, address, numAudioChannels, data, iServerFrameSizeSamples );
    }

Why that is safe:

  • All CJamRecorder slots run on the dedicated recorder thread via queued connections, so they are serialized by the event loop.
  • The unlocked reads of isRecording and currentSession are not a real TOCTOU race across threads; another slot cannot interleave on that thread between the check and the lock.
  • Early return when currentSession == nullptr avoids taking the mutex unnecessarily (e.g. after OnEnd() has already torn down the session).
  • ReaperProjectFromCurrentSession / AudacityLofFromCurrentSession are only called under the mutex from OnEnd(), so the new comments documenting that are accurate.

Pre-existing quirk (unchanged by this diff): the !isRecording branch logs a warning but does not return, so if currentSession were ever non-null while not recording, it would still call DisconnectClient. In practice OnEnd() clears both under the same lock, so that path should not occur.

3. Do any other iChID uses need guarding?

Within the recorder path, the mutex coverage is now complete. Every entry point that touches per-channel session state goes through ChIdMutex:

Entry point Mutex Per-channel null safety
OnFrame Yes, before Frame() Frame() checks vecptrJamClients[iChID]
OnDisconnected Yes, before DisconnectClient() New guard in DisconnectClient()
OnEnd / Start Yes, around session lifecycle End() checks != nullptr before disconnect
Internal Frame()DisconnectClient() Caller (OnFrame) holds mutex Now guarded at callee

CJamSession::DisconnectClient, Frame, and End rely on the caller holding ChIdMutex; all current callers do.

Nothing else in this diff scope obviously needs the same treatment. Worth knowing but outside this change:

  • Bounds: iChID is not validated against vecptrJamClients.size() / MAX_NUM_CHANNELS. That is pre-existing; the server is expected to only emit valid channel IDs.
  • Server-side iChID use (server.cpp, etc.) is unrelated to this recorder race.
  • chIdDisconnected is a single int, not per-channel, which is a pre-existing design constraint for concurrent multi-client disconnects, not introduced here.

Bottom line: The branch looks sound. The nullptr guard fixes the crash; the mutex move is a safe refinement, not a regression; and no additional iChID guarding is required in the recorder beyond what you have, aside from the optional chIdDisconnected tweak mentioned above if you want to harden against duplicate disconnect + stale frame ordering.

@pljones
pljones requested review from ann0see and softins and a lite review from Copilot August 17, 2026 17:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the recorder’s handling of client disconnects to prevent a potential server crash during recording, by avoiding dereferencing a null client pointer and adjusting mutex usage around disconnect handling.

Changes:

  • Add a null-pointer guard in CJamSession::DisconnectClient() to prevent dereferencing an absent client instance.
  • Document mutex expectations for project/LOF generation helpers.
  • Move ChIdMutex locking in CJamRecorder::OnDisconnected() to avoid locking when returning early.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +224 to +227
if ( vecptrJamClients[iChID] == nullptr )
{
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 AI: Confirmed on this branch: 390 guarded early returns, 389 of them followed by a frame that allocated a fresh CJamClient; a second run, 220 and 219. Once the guard fires, the late frame is the normal continuation, not an edge case.

@ann0see

ann0see commented Aug 17, 2026

Copy link
Copy Markdown
Member

If @mcfnord had a local reproducer, he should rerun it to test this.

QThread::currentThread()->exit();
}

// ChIdMutex held

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe rather @requires or something else that basically says that the mutex must be held here. Isn't there a way to check if the mutex is held code wise and else just return with error?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are you noticing that the reader locks, but the writers do not? I plan a PR for it, after this one is merged.

@ann0see ann0see added the AI AI generated or potentially AI generated label Aug 17, 2026
@ann0see ann0see added this to Tracking Aug 17, 2026
@github-project-automation github-project-automation Bot moved this to Triage in Tracking Aug 17, 2026
@mcfnord

mcfnord commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

(I don't understand where it says "2 321 293"... missing commas? And what's unordinary about this recording? I can think of what it might mean.)

🤖 AI: The reproducer was re-run against this branch. One worktree, one object set, so the two binaries differ in jamrecorder.o alone: control is this PR's base c862872e, arm is c4f0b45c.

Control: dies 4 of 4 runs, 1–2 s in, exit 139, the fault address from the original report. Arm: 2 321 293 cycles over 300 s, plus four further 20 s runs, without it.

Ordinary recording is unaffected — two clients, 40 s, WAV + .rpp + .lof written by both binaries, byte-identical totals in two of three run pairs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI AI generated or potentially AI generated

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

4 participants