hts_newthread() returns as soon as the thread is created, but process_chain++ runs inside the new thread's entry point (htsthread.c:104). A htsthread_wait() that gets there first sees process_chain == 0 and returns immediately, leaving the caller free to tear down state the child is about to read.
During a crawl the threads start long before teardown, so the window rarely opens. It does open: htsthread_uninit() and httrack.c's "wait for pending threads" both trust the count.
Hit while writing a self-test that spawns four notifier threads and then joins with htsthread_wait(). On a loaded machine the wait returned at once, the caller freed the option block, and a straggler took SIGSEGV inside it. That self-test now joins on a counter of its own.
Fix shape: increment the count in hts_newthread() before it returns, and decrement it in the child or on a create failure.
hts_newthread()returns as soon as the thread is created, butprocess_chain++runs inside the new thread's entry point (htsthread.c:104). Ahtsthread_wait()that gets there first seesprocess_chain == 0and returns immediately, leaving the caller free to tear down state the child is about to read.During a crawl the threads start long before teardown, so the window rarely opens. It does open:
htsthread_uninit()and httrack.c's "wait for pending threads" both trust the count.Hit while writing a self-test that spawns four notifier threads and then joins with
htsthread_wait(). On a loaded machine the wait returned at once, the caller freed the option block, and a straggler took SIGSEGV inside it. That self-test now joins on a counter of its own.Fix shape: increment the count in
hts_newthread()before it returns, and decrement it in the child or on a create failure.