amule-remote-gui: null dangling client→file refs before deleting a CKnownFile - #749
Closed
got3nks wants to merge 1 commit into
Closed
Conversation
…nownFile (amule-project#748) CKnownFilesRem::DeleteItem was destroying a CKnownFile without sweeping the client list to null out CUpDownClient::m_uploadingfile / m_reqfile on clients that still pointed at it. Sequence: 1. EC_TAG_FILE_REMOVED arrives for file X → CKnownFilesRem::DeleteItem(X) → X is destroyed. 2. Any CUpDownClient whose m_uploadingfile == X now holds a dangling pointer. 3. Some later INC_UPDATE batch removes that client → CUpDownClientListRem::DeleteItem(client) at line 1504 null-checks m_uploadingfile (non-null, passes the guard), line 1505 calls m_uploadingfile->RemoveUploadingClient(client) → operates on the freed CKnownFile, std::set::erase walks freed m_ClientUploadList nodes via CClientRef::operator< → SEGV. Fix is a single-line callout from CKnownFilesRem::DeleteItem to a new CUpDownClientListRem::DropReferencesTo(file) helper, which has the friend access to CUpDownClient's private fields that CKnownFilesRem lacks. O(N_clients) per file removal, bounded because EC_TAG_FILE_REMOVED markers are rare (only fire when amuled actually drops a known file, not on every INC_UPDATE). The bug existed pre-amule-project#727 in principle but the legacy "anything missing from the response is deleted" sweep tended to clean the dangling-pointer clients out of the list before the next time their DeleteItem ran. With explicit EC_TAG_FILE_REMOVED markers (opt-in via EC_TAG_CAN_PARTIAL_UPDATE), file deletions are rare and clients can hold the stale pointer arbitrarily long — heavy sharesets like the one reporting hit the timing window readily. Refs amule-project#748.
got3nks
marked this pull request as draft
May 27, 2026 22:36
got3nks
added a commit
to got3nks/amule
that referenced
this pull request
May 27, 2026
aMule has eight latent use-after-free sites where raw CKnownFile* / CPartFile* pointers outlive the pointee — surfaced by the ASan crash in issue amule-project#755 (CGenericClientListCtrl::m_knownfiles) and the production SEGV chain in amule-project#748 (CUpDownClient::m_uploadingfile / m_reqfile, fixed in amule-project#749). amule-project#749 is also superseded by this PR. Audit + fix as one coherent change: The new MuleNotify::KnownFileBeingDestroyed(file) broadcast fires from every CKnownFile destruction site BEFORE delete. Each subscriber strips references by pointer-value comparison only — never dereferences the pointer (by the time a main-thread queued subscriber runs, the bytes have typically been recycled). Documented contract in GuiEvents.h. Fire sites: - CPartFile::Delete() (user cancel) - CKnownFileList::Clear() (shutdown) - CKnownFileList::PruneDuplicates() × 3 (TTL/cap eviction) - CKnownFilesRem::DeleteItem (amulegui EC_TAG_FILE_REMOVED) Subscribers (GUI): - CGenericClientListCtrl::RemoveKnownFile (amule-project#755 crash site — m_knownfiles vector + ClientCtrlItem_Struct::m_owner per-row) - CCommentDialog::DropReferencesTo (open-instance registry + self-dismiss on file destruction) - CCommentDialogLst::DropReferencesTo (same) - CFileDetailDialog::DropReferencesTo (same + stops the 5-second update timer + strips m_files vector entries) Subscribers (amulegui only): - CUpDownClientListRem::DropReferencesTo (amule-project#749 content, now triggered through the broadcast instead of a direct call) Subscribers (amule daemon only): - CAICHHashSet::DropReferencesTo (strip-by-ptr on the static m_liRequestedData recovery-request list; pre-existing IsPartFile() guard in ClientAICHRequestFailed can be spoofed by allocator reuse so the strip is needed too) - CPartFileWriteThread::DropReferencesTo (strip pending writes whose pFile matches, deleting the buffered data; protects the write loop from dereffing a freed CPartFile next tick) Validate-before-deref defences added at three event-handler sites where the broadcast pattern doesn't fit (events already in flight on the wx queue, the dangling pointer is in the event payload): - OnFinishedAICHHashing — check IsKnownFile(owner) || IsPartFile(owner) before swapping hashsets; AICH hashing of a huge file can run for minutes during which the CKnownFile can be evicted - OnFinishedAllocation — wxASSERT was no-op in Release; convert to real check on IsPartFile() so the cancel-during-preallocation window (~10 s on slow disks) stops UAF'ing - OnFinishedCompletion — same fix OnFinishedHashing already had the validate-before-deref pattern; left in place. New helper: - CKnownFileList::IsKnownFile(const CKnownFile*) — pointer-value scan over m_knownFileMap, safe to call with a possibly-freed pointer (no deref). Symbol audit recap (see PR description for full table): Site Status CGenericClientListCtrl::m_knownfiles FIXED (amule-project#755) ClientCtrlItem_Struct::m_owner FIXED (rows pruned) CCommentDialog::m_file FIXED CCommentDialogLst::m_file FIXED CFileDetailDialog::m_file + m_files FIXED CUpDownClient::m_uploadingfile / m_reqfile FIXED (refactored amule-project#749) CAICHHashSet::m_liRequestedData FIXED CHashingEvent::m_owner / m_result FIXED (validate) CAllocFinishedEvent::m_file FIXED (validate) CCompletionEvent::m_owner FIXED (validate) CPartFileWriteThread::m_flushList FIXED CClientRef holders (Kad sources, upload list, friend list, …) SAFE (refcounted) Owner containers (m_Files_map, m_filelist, …) SAFE (owner pairing) CAICHHashSet::m_pOwner, CFileStatistic::fileParent SAFE (1:1 composition) Tested: - amule (full client, Debug, Ubuntu 25.10 aarch64) builds clean - amulegui (remote GUI, Debug) builds clean - amuled (headless daemon, Debug) builds clean - Smoke-test: amuled startup + shutdown clean (no crashes; broadcast fires for every known file during CKnownFileList::Clear without subscriber misbehaving) Supersedes amule-project#749. Closes amule-project#755. Refs amule-project#748.
Merged
4 tasks
Contributor
Author
|
Superseded by #756, which addresses 7 additional UAF sites of the same shape (raw |
got3nks
added a commit
to got3nks/amule
that referenced
this pull request
May 27, 2026
aMule has eight latent use-after-free sites where raw CKnownFile* / CPartFile* pointers outlive the pointee — surfaced by the ASan crash in issue amule-project#755 (CGenericClientListCtrl::m_knownfiles) and the production SEGV chain in amule-project#748 (CUpDownClient::m_uploadingfile / m_reqfile, fixed in amule-project#749). amule-project#749 is also superseded by this PR. Audit + fix as one coherent change: The new MuleNotify::KnownFileBeingDestroyed(file) broadcast fires from every CKnownFile destruction site BEFORE delete. Each subscriber strips references by pointer-value comparison only — never dereferences the pointer (by the time a main-thread queued subscriber runs, the bytes have typically been recycled). Documented contract in GuiEvents.h. Fire sites: - CPartFile::Delete() (user cancel) - CKnownFileList::Clear() (shutdown) - CKnownFileList::PruneDuplicates() × 3 (TTL/cap eviction) - CKnownFilesRem::DeleteItem (amulegui EC_TAG_FILE_REMOVED) Subscribers (GUI): - CGenericClientListCtrl::RemoveKnownFile (amule-project#755 crash site — m_knownfiles vector + ClientCtrlItem_Struct::m_owner per-row) - CCommentDialog::DropReferencesTo (open-instance registry + self-dismiss on file destruction) - CCommentDialogLst::DropReferencesTo (same) - CFileDetailDialog::DropReferencesTo (same + stops the 5-second update timer + strips m_files vector entries) Subscribers (amulegui only): - CUpDownClientListRem::DropReferencesTo (amule-project#749 content, now triggered through the broadcast instead of a direct call) Subscribers (amule daemon only): - CAICHHashSet::DropReferencesTo (strip-by-ptr on the static m_liRequestedData recovery-request list; pre-existing IsPartFile() guard in ClientAICHRequestFailed can be spoofed by allocator reuse so the strip is needed too) - CPartFileWriteThread::DropReferencesTo (strip pending writes whose pFile matches, deleting the buffered data; protects the write loop from dereffing a freed CPartFile next tick) Validate-before-deref defences added at three event-handler sites where the broadcast pattern doesn't fit (events already in flight on the wx queue, the dangling pointer is in the event payload): - OnFinishedAICHHashing — check IsKnownFile(owner) || IsPartFile(owner) before swapping hashsets; AICH hashing of a huge file can run for minutes during which the CKnownFile can be evicted - OnFinishedAllocation — wxASSERT was no-op in Release; convert to real check on IsPartFile() so the cancel-during-preallocation window (~10 s on slow disks) stops UAF'ing - OnFinishedCompletion — same fix OnFinishedHashing already had the validate-before-deref pattern; left in place. New helper: - CKnownFileList::IsKnownFile(const CKnownFile*) — pointer-value scan over m_knownFileMap, safe to call with a possibly-freed pointer (no deref). Symbol audit recap (see PR description for full table): Site Status CGenericClientListCtrl::m_knownfiles FIXED (amule-project#755) ClientCtrlItem_Struct::m_owner FIXED (rows pruned) CCommentDialog::m_file FIXED CCommentDialogLst::m_file FIXED CFileDetailDialog::m_file + m_files FIXED CUpDownClient::m_uploadingfile / m_reqfile FIXED (refactored amule-project#749) CAICHHashSet::m_liRequestedData FIXED CHashingEvent::m_owner / m_result FIXED (validate) CAllocFinishedEvent::m_file FIXED (validate) CCompletionEvent::m_owner FIXED (validate) CPartFileWriteThread::m_flushList FIXED CClientRef holders (Kad sources, upload list, friend list, …) SAFE (refcounted) Owner containers (m_Files_map, m_filelist, …) SAFE (owner pairing) CAICHHashSet::m_pOwner, CFileStatistic::fileParent SAFE (1:1 composition) Tested: - amule (full client, Debug, Ubuntu 25.10 aarch64) builds clean - amulegui (remote GUI, Debug) builds clean - amuled (headless daemon, Debug) builds clean - Smoke-test: amuled startup + shutdown clean (no crashes; broadcast fires for every known file during CKnownFileList::Clear without subscriber misbehaving) Supersedes amule-project#749. Closes amule-project#755. Refs amule-project#748.
mrjimenez
pushed a commit
that referenced
this pull request
May 28, 2026
aMule has eight latent use-after-free sites where raw CKnownFile* / CPartFile* pointers outlive the pointee — surfaced by the ASan crash in issue #755 (CGenericClientListCtrl::m_knownfiles) and the production SEGV chain in #748 (CUpDownClient::m_uploadingfile / m_reqfile, fixed in #749). #749 is also superseded by this PR. Audit + fix as one coherent change: The new MuleNotify::KnownFileBeingDestroyed(file) broadcast fires from every CKnownFile destruction site BEFORE delete. Each subscriber strips references by pointer-value comparison only — never dereferences the pointer (by the time a main-thread queued subscriber runs, the bytes have typically been recycled). Documented contract in GuiEvents.h. Fire sites: - CPartFile::Delete() (user cancel) - CKnownFileList::Clear() (shutdown) - CKnownFileList::PruneDuplicates() × 3 (TTL/cap eviction) - CKnownFilesRem::DeleteItem (amulegui EC_TAG_FILE_REMOVED) Subscribers (GUI): - CGenericClientListCtrl::RemoveKnownFile (#755 crash site — m_knownfiles vector + ClientCtrlItem_Struct::m_owner per-row) - CCommentDialog::DropReferencesTo (open-instance registry + self-dismiss on file destruction) - CCommentDialogLst::DropReferencesTo (same) - CFileDetailDialog::DropReferencesTo (same + stops the 5-second update timer + strips m_files vector entries) Subscribers (amulegui only): - CUpDownClientListRem::DropReferencesTo (#749 content, now triggered through the broadcast instead of a direct call) Subscribers (amule daemon only): - CAICHHashSet::DropReferencesTo (strip-by-ptr on the static m_liRequestedData recovery-request list; pre-existing IsPartFile() guard in ClientAICHRequestFailed can be spoofed by allocator reuse so the strip is needed too) - CPartFileWriteThread::DropReferencesTo (strip pending writes whose pFile matches, deleting the buffered data; protects the write loop from dereffing a freed CPartFile next tick) Validate-before-deref defences added at three event-handler sites where the broadcast pattern doesn't fit (events already in flight on the wx queue, the dangling pointer is in the event payload): - OnFinishedAICHHashing — check IsKnownFile(owner) || IsPartFile(owner) before swapping hashsets; AICH hashing of a huge file can run for minutes during which the CKnownFile can be evicted - OnFinishedAllocation — wxASSERT was no-op in Release; convert to real check on IsPartFile() so the cancel-during-preallocation window (~10 s on slow disks) stops UAF'ing - OnFinishedCompletion — same fix OnFinishedHashing already had the validate-before-deref pattern; left in place. New helper: - CKnownFileList::IsKnownFile(const CKnownFile*) — pointer-value scan over m_knownFileMap, safe to call with a possibly-freed pointer (no deref). Symbol audit recap (see PR description for full table): Site Status CGenericClientListCtrl::m_knownfiles FIXED (#755) ClientCtrlItem_Struct::m_owner FIXED (rows pruned) CCommentDialog::m_file FIXED CCommentDialogLst::m_file FIXED CFileDetailDialog::m_file + m_files FIXED CUpDownClient::m_uploadingfile / m_reqfile FIXED (refactored #749) CAICHHashSet::m_liRequestedData FIXED CHashingEvent::m_owner / m_result FIXED (validate) CAllocFinishedEvent::m_file FIXED (validate) CCompletionEvent::m_owner FIXED (validate) CPartFileWriteThread::m_flushList FIXED CClientRef holders (Kad sources, upload list, friend list, …) SAFE (refcounted) Owner containers (m_Files_map, m_filelist, …) SAFE (owner pairing) CAICHHashSet::m_pOwner, CFileStatistic::fileParent SAFE (1:1 composition) Tested: - amule (full client, Debug, Ubuntu 25.10 aarch64) builds clean - amulegui (remote GUI, Debug) builds clean - amuled (headless daemon, Debug) builds clean - Smoke-test: amuled startup + shutdown clean (no crashes; broadcast fires for every known file during CKnownFileList::Clear without subscriber misbehaving) Supersedes #749. Closes #755. Refs #748.
mrjimenez
pushed a commit
to mrjimenez/amule
that referenced
this pull request
Aug 1, 2026
…ored password (amule-project#749) In the EC protocol the stored password value IS the credential -- the challenge hashes it directly -- so anything holding it can authenticate. amuled handed the amuleapi it spawned a --amule-config-file pointing at amule.conf, and amuleapi read md5(EC password) out of it. A network-facing daemon was therefore holding something equivalent to the user's EC password, for its whole run. It no longer needs one. amuled generates a random 128-bit token at startup, writes it 0600 into the config dir, and accepts it as a second valid EC credential for that run. amuleapi reads it and unlinks it immediately; amuled removes it regardless after ten seconds, so a child that dies before reading cannot leave a secret at rest. Nothing durable is left to steal from the API daemon, and nothing needs rotating if it is compromised -- restarting amuled invalidates the token. No flag carries the path. Both ends derive it from webcommon, because argv is world-readable via ps: passing a path would tell every local user where to look for as long as the file exists. amuled already passes --config-dir, so the child knows the directory. --amule-config-file is gone from amuleapi with it. That flag was the last way amuleapi read a password-equivalent value off disk; a manually started instance uses amuleapi.conf's own [EC]/Password, already written 0600. amuleweb keeps the flag. Two things the design had to account for that are not obvious: The credential also keys the session. ActivateAEAD() derived its ikm from thePrefs::ECPassword() unconditionally, while the client derives its half from whatever secret it presented (RemoteConnect.cpp: m_aeadSecret = pass.Lower()). Accepting a second credential without tracking which one matched would have authenticated the peer and then broken the first sealed packet. The matched secret is now remembered per socket and keys the AEAD. Per socket, specifically. CECServerSocket is instantiated per accepted connection, which is why the salt is already per-instance; putting the matched secret anywhere shared would let a second client overwrite the key material of an established session. amulegui on the password and amuleapi on the token can hold concurrent sessions. The two credentials are compared without short-circuiting. `if (pw) else if (token)` would leak which one matched and, more usefully to an attacker, whether a token is live at all. Supporting changes: WriteFileAtomic0600 moves into webcommon and both existing copies -- one inlined in SaveCredentialsFile, one in amuleapi's AmuleApiConfig.cpp -- now call it. muleappcore's webcommon link becomes PUBLIC so the targets that compile amule.cpp directly get the header path; they already linked the objects transitively. EcTokenFilePath joins through the same helper as CredentialsFilePath so the two file names cannot drift on separator handling, and both are pinned by tests. Verified against a live daemon on macOS: amuled spawns amuleapi, amuleapi authenticates with the token and serves HTTP, and the token file is gone while the daemon is still running. Encryption was negotiated for that session, which is what proves the AEAD keys matched -- a mismatch fails the sealed EC_OP_AUTH_OK and the connection would not have come up. amulecmd authenticated with the configured password against the same daemon at the same time. With the child never reading it, the file appears at t+1s and is removed at t+11s.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #748.
CKnownFilesRem::DeleteItemwas destroying aCKnownFilewithout sweeping the client list to null outCUpDownClient::m_uploadingfile/m_reqfileon clients that still pointed at it. A laterCUpDownClientListRem::DeleteItemon such a client would then walk throughclient->m_uploadingfile->RemoveUploadingClient(client), whichstd::set::erase's the already-freedm_ClientUploadListviaCClientRef::operator<reading.m_clientoff freed memory — SEGV.The bug existed pre-#727 in principle, but the legacy "anything missing from the response is deleted" sweep tended to clean the dangling-pointer clients out before the next time their
DeleteItemran. With explicitEC_TAG_FILE_REMOVEDmarkers (opt-in viaEC_TAG_CAN_PARTIAL_UPDATE), file deletions are rare and clients can hold the stale pointer arbitrarily long — heavy sharesets hit the timing window readily.Fix
One callout from
CKnownFilesRem::DeleteItemto a newCUpDownClientListRem::DropReferencesTo(file)helper. The friend access toCUpDownClient's private fields lives onCUpDownClientListRem(notCKnownFilesRem), hence the helper. O(N_clients) per file removal, bounded becauseEC_TAG_FILE_REMOVEDmarkers are rare.Test plan
amuleguibuilds clean on macOS.