Skip to content

Print planned blocks from the grbl thread, not the simulator thread - #26

Merged
terjeio merged 1 commit into
grblHAL:masterfrom
DavidLDawes:printblock-race-upstream
Sep 25, 2026
Merged

terjeio merged 1 commit into
grblHAL:masterfrom
DavidLDawes:printblock-race-upstream

Conversation

@DavidLDawes

Copy link
Copy Markdown

Problem

The simulator occasionally segfaults, most often just after startup. Every crash dump I captured (7 from real runs, 5 of them from a plain master build) has the same stack on the simulator thread:

#0 plan_get_recent_block ()
#1 printBlock ()
#2 sim_loop ()
#3 main ()

printBlock() reads the planner through plan_get_recent_block() (block_buffer.head->prev), but it's called from grbl_per_byte() on the simulator thread. That call is unconditional whenever no client is connected, which includes startup. Meanwhile plan_reset() on the grbl thread malloc()s the block ring and relinks it. The simulator thread can therefore read head before prev is linked, get uninitialized heap as a non-NULL block pointer, and crash dereferencing it.

Fix

The printBlock() call moves into sim_process_realtime(), the grbl.on_execute_realtime hook that already runs on the grbl thread on every main-loop pass. The planner is then only read by the thread that modifies it. The existing rule of not printing blocks to a live console in socket mode is unchanged.

Evidence

400 launches of grblHAL_sim -p … -n -t 0 -b <file>, counting segfaults in the first 250 ms:

Build Segfaults
master 91eda77 2 / 400
master + this commit 0 / 400
A build with the same printBlock() code plus #25 11 / 400 (race timing varies between builds)
That build + this commit 0 / 400
  • Block output is unchanged: socket mode with -b file logs one line per move with the same step counts; stdin mode prints blocks to stdout and exits cleanly on ^F.
  • Downstream: in a project whose CI drives the simulator over -p on every push, this crash was the cause of intermittent CI failures, and it hasn't recurred with this change.

Notes

  • Independent of Make the driver's atomic flag helpers actually atomic #25 (the atomic flag helpers): this commit applies directly on master and touches different code.
  • A separate issue in core, not addressed here: AddressSanitizer also flags settings_write_build_info() in core's settings.c. It copies sizeof(stored_line_t) bytes from BUILD_INFO, a string literal that can be a single byte, an over-read on first boot.
  • Built and tested with gcc on Linux only.

🤖 Generated with Claude Code

printBlock() reads grblHAL's planner through plan_get_recent_block()
(block_buffer.head->prev), but it was called from grbl_per_byte() on the
simulator thread - unconditionally whenever no client is connected, which
includes startup. plan_reset() on the grbl thread malloc()s and relinks
that ring, so the simulator thread could read `head` before `prev` was
linked and dereference uninitialized heap: SIGSEGV in printBlock().

WSL crash dumps from real runs all showed sim_loop -> printBlock ->
plan_get_recent_block. Launching the simulator 400 times segfaulted 11 of
them at startup before this change, 0 of 400 after.

The call moves to sim_process_realtime(), the grbl.on_execute_realtime
hook that already runs on the grbl thread each main-loop pass, so the
planner is only read by the thread that mutates it. The existing rule of
not printing blocks to a live console in socket mode is kept. Block
output verified unchanged in both socket (-b file) and stdin (stdout) mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@terjeio
terjeio merged commit 226e2e8 into grblHAL:master Sep 25, 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.

3 participants