Skip to content

Fix/prevent backup thread from posting wx events after shutdown - #1002

Closed
gushanji wants to merge 9 commits into
Snapmaker:build_platform_engineeringfrom
gushanji:fix/backup-callback-shutdown-race
Closed

gushanji wants to merge 9 commits into
Snapmaker:build_platform_engineeringfrom
gushanji:fix/backup-callback-shutdown-race

Conversation

@gushanji

@gushanji gushanji commented Oct 13, 2026 •

Copy link
Copy Markdown

Fixes the crash tracked in Sentry issue #7134242393 (SNAPMAKER_ORCA-7R, "wxPostEvent", EXC_BAD_ACCESS, still active on 2.4.0). The reported stack is the backup worker thread (boost::thread_proxy -> _BBS_Backup_Manager::operator() -> std::function -> MainFrame ctor lambda -> wxPostEvent) — a data race between the backup worker and the UI thread, platform-neutral UB.

Cause

_BBS_Backup_Manager's worker loop snapshots m_post_callback into a local while holding m_mutex, releases the lock for process_task(), then invokes the stale copy after re-locking. Meanwhile MainFrame::shutdown() — called on app exit and on language-switch recreate — calls Slic3r::set_backup_callback(nullptr) and destroys MainFrame. If shutdown lands inside that window, the worker still calls the old callback, which wxPostEvents into the already-destroyed MainFrame → use-after-free.

The change

Add m_post_callback_mutex serializing callback swaps against in-flight invocations (bbs_3mf.cpp only, no public API change):

  • set_post_callback() swaps under the new mutex
  • the worker re-reads m_post_callback under it at fire time — no stale local copy

Guarantee: once set_backup_callback(nullptr) returns, the worker can never invoke an old callback. Lock order m_mutex -> m_post_callback_mutex is documented at the member; m_mutex critical sections are otherwise unchanged (the callback was already invoked under it). The UI-thread read in process_ui_task() (callback(1)) is untouched — same-thread with the only writer, no race there.

Verification

A/B tested on Windows (MSVC 2022, RelWithDebInfo) with a test-only build aid: a 2.5 s sleep in the worker's post-task window plus error-level logging at the callback swap/invoke sites (never committed). Quit during a backup cycle, 5 trials per build:

  • pre-fix: set_backup_callback(nullptr) landed mid-window in 4/5 runs; in every one the worker invoked the stale callback 150–540 ms after the swap, entering the lambda that captures the dying MainFrame (logged). Across runs this manifested as silent invalid calls, hangs at exit, and abnormal exit codes — the same UAF Sentry reports as EXC_BAD_ACCESS on macOS.
  • fixed: identical interleaving (swap mid-window 5/5), but the worker re-read m_post_callback as null under the new mutex in 5/5 and skipped the call — zero invocations after the swap, no hangs or crashes, all exits clean.

Sentry cross-check: the issue's newest event stack matches the fixed path frame-by-frame (worker → std::function → MainFrame lambda → wxPostEvent).

PILIPALA030 and others added 9 commits October 8, 2026 15:25
Add missing <cassert> include to Color.hpp to fix Flatpak GCC 15 build
…napmaker#974)

The root popup maps a hidden selection onto a group header row:
selectedItem() falls back to the header of the group that holds the
selected preset. render() then painted both the selection rectangle and
the check mark on that header, so the parent menu read as if the vendor
itself were the selected entry.

drawn_row_for_item() adds the missing distinction: it resolves the
selection like selected_row_for_item() but reports -1 whenever the
mapped row is a group header. A header is also what that mapping returns
for the first preset of a group, not only through its fold fallback, so
the check cannot be folded into the call. render() uses it, so the
header row draws neither the rectangle nor the check mark.
ensure_selection_at_top() keeps the original mapping, so opening the
popup still scrolls the group that holds the selection to the top, and
the submenu highlight is unchanged.

The mapping test covers the folded selection, a visible selection, a
submenu row and the out-of-range cases.

Co-authored-by: zhongzhipan <zhongzhipan@snapmaker.com>
…aker#958)

Tool change: by object, move to an absolute height just above the
tallest object printed so far, capped at the max print height; by layer,
keep the stock relative 1.5 mm lift. main currently lifts by the target
height on top of the current height, which rises further than needed and
can exceed the printable height on tall prints. The absolute move from
Snapmaker#453 was applied to by-layer prints too, sending the nozzle to Z 1.5,
and dropped to Z 1.5 when the tallest object was within 2 mm of the max
height.

End G-code: cap the pre-homing lift at the max print height instead of
the uncapped max_layer_z + 2, which the printer rejects near the top of
travel. Declare move_z as a decimal first, since max_print_height is
passed as an integer and truncated the lift to whole millimeters.

Applies to the 0.2, 0.4, 0.6 and 0.8 nozzle profiles.
…tems in GPU pipeline (Snapmaker#983)

Unchecking a feature-type legend item (e.g. Top surface) and checking it
back could leave Top surface / Bridging / Internal bridging / Internal
solid infill unrendered, depending on earlier slider interaction. Two
defects in refresh_render_paths_gpu(), both stale state at event time:

- visibility inputs were forwarded to the path stack only by
  render_toolpaths() on the next frame, so the step-table rebuild and
  the slider-endpoint computation inside refresh_render_paths_gpu() ran
  with the PREVIOUS state: unchecking kept the endpoints unshrunk,
  re-checking shrank them under the old visibility, leaving the
  sequential-view state one event behind. Forward the role flags,
  filament visibles and view type to the stack at the top of the
  refresh, mirroring the legacy pipeline which reads the current flags
  synchronously.

- the refresh reset m_sequential_view.current.last without re-syncing
  the stack's move window, whose only writer was the bottom-slider
  callback (update_sequential_view_current). PathRenderer::RenderLayers
  always clips the top layer's draw to that window, so after a slider
  drag any subsequent legend/view toggle left the top layer truncated
  at the stale playback position -- exactly where the trailing
  top-surface moves live. The refresh now re-syncs the move window to
  the playback position it established.

Verified: standard cube sliced, feature-type legend items toggled
off/on with and without prior bottom-slider interaction -- paths and
slider numbers restore as before.
…ravel values (Snapmaker#980)

- first_layer_travel_acceleration/first_layer_travel_jerk are FloatOrPercent options (default 100%); the single-arg get_abs_value() resolved percentages via the static ratio_over chain, using extruder 0's raw config value and bypassing process_flow_value()
- in GCode::travel_to(), resolve percentages against process_flow_value(travel_acceleration/travel_jerk), the same values emitted for normal layers
- apply the same resolution to the second-layer travel accel/jerk reset condition so the reset tracks the same code path as the first-layer override
…r#986)

- Remove the GUI check that auto-reset paint penetration layers when they exceeded
  the matching shell layers, including the red highlight infrastructure
  (Field highlight API, highlight callback wiring in Tab/ObjectSettings/ObjectTable)
- Remove slice-time checks that blocked slicing when top/bottom shell layers or
  paint penetration layers exceeded the model total layer count (PEN-015)
- The paint penetration parameters and their slicing behavior are unchanged;
  only the validity constraints are dropped
…hangs (Snapmaker#995)

Tree (manual) support is supposed to grow supports only where the user
paints, but any layer with painted enforcers also grew tree supports over
its unpainted overhang regions. The port of Bambu c03e13340
(STUDIO-11484) flattened the native auto/manual if/else into an
unconditional overhang diff + union, so painted layers admitted
full-layer auto overhangs into support generation.

Restore the mode gate from Bambu c03e13340: in manual mode keep only the
painted enforcer overhangs (expanded by the tip diameter per STUDIO-7538),
never the full-layer auto diff. Auto mode is unchanged, painted overhangs
still merge through OverhangCluster across layers, and blockers keep
being subtracted downstream.

Bug: 7131842245

Co-authored-by: Zackary <zacakry_shen@qq.com>
_BBS_Backup_Manager's worker loop copied m_post_callback to a local,
unlocked, did file IO, then invoked the stale copy. If MainFrame::
shutdown() swapped in nullptr during that window, the worker still
called the old callback, wxPostEvent-ing into an already destroyed
MainFrame (EXC_BAD_ACCESS, Sentry #7134242393).

Serialize callback swaps against in-flight invocations with
m_post_callback_mutex and re-read m_post_callback under it, so once
set_backup_callback(nullptr) returns the worker can never invoke an
old callback. Lock order: m_mutex -> m_post_callback_mutex.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@gushanji gushanji closed this Oct 13, 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.

7 participants