Skip to content

amule-remote-gui: null dangling client→file refs before deleting a CKnownFile - #749

Closed
got3nks wants to merge 1 commit into
amule-project:masterfrom
got3nks:fix/dangling-uploadingfile-after-ec-removal
Closed

amule-remote-gui: null dangling client→file refs before deleting a CKnownFile#749
got3nks wants to merge 1 commit into
amule-project:masterfrom
got3nks:fix/dangling-uploadingfile-after-ec-removal

Conversation

@got3nks

@got3nks got3nks commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #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. A later CUpDownClientListRem::DeleteItem on such a client would then walk through client->m_uploadingfile->RemoveUploadingClient(client), which std::set::erase's the already-freed m_ClientUploadList via CClientRef::operator< reading .m_client off 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 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 hit the timing window readily.

Fix

One callout from CKnownFilesRem::DeleteItem to a new CUpDownClientListRem::DropReferencesTo(file) helper. The friend access to CUpDownClient's private fields lives on CUpDownClientListRem (not CKnownFilesRem), hence the helper. O(N_clients) per file removal, bounded because EC_TAG_FILE_REMOVED markers are rare.

Test plan

  • amulegui builds clean on macOS.
  • No reproducer locally — the trace is enough to identify the dangling-pointer site by inspection. Reporter has a heavy shareset (70k+ files) and is well placed to confirm the fix.

…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 got3nks mentioned this pull request May 27, 2026
@got3nks
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.
@got3nks

got3nks commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #756, which addresses 7 additional UAF sites of the same shape (raw CKnownFile* / CPartFile* outliving the pointee) plus this one through a unified Notify_KnownFileBeingDestroyed broadcast. The CUpDownClientListRem::DropReferencesTo code from this PR is preserved verbatim there, just routed through the broadcast handler for consistency with the other subscribers. Closing.

@got3nks got3nks closed this May 27, 2026
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.
@got3nks
got3nks deleted the fix/dangling-uploadingfile-after-ec-removal branch May 28, 2026 08:58
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.
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.

amulegui crash backtrace

1 participant