Describe the bug
Timeshift sporadically crashes with SIGSEGV during teardown, right after a snapshot has been created successfully. The snapshot itself is fine and no data is lost — only the cleanup crashes. The crash is a double g_object_unref in one of AsyncTask's reader threads while the underlying input stream is being released. It is triggered by the scheduled timeshift --check --scripted run from root cron and recurs over time.
To Reproduce
It is a timing-dependent race and does not reproduce on every run. It happens when:
- A snapshot is actually created (a due
--check --scripted, or --create --scripted).
- The rsync child finishes and the two
AsyncTask reader threads shut down.
- Occasionally the process gets SIGSEGV during teardown (in
g_object_unref).
On the affected machine it was captured from cron three times — 2026-05-02, 2026-05-23 and 2026-06-15 (coredumpctl list /usr/bin/timeshift; older ones already rotated out).
Expected behavior
The process should exit cleanly after creating the snapshot, without crashing during teardown.
System:
- Linux Distribution Name and Version: Linux Mint 22.3 (zena)
- Desktop: MATE
- Timeshift Version: 25.12.4 (package
25.12.4+zena)
- Timeshift Mode: rsync
- Display Server: N/A — crash occurs in the CLI binary launched headless from root cron
Additional context (root-cause analysis)
Symbolized backtrace (coredump resolved against a local debug build of the same source; the packaged binary is stripped):
#0 g_object_unref (libgobject) <- SIGSEGV
#1 (libgio) GFilterInputStream dispose -> unref of base stream
#2 g_object_unref (libgobject)
#3 async_task_read_stdout src/Utility/AsyncTask.vala (dis_out = null)
#4 _async_task_read_stdout_gthread_func
#5 g_thread_proxy (libglib)
The crash is on a reader thread, not the main thread.
In AsyncTask, read_stdout() / read_stderr() clear their stdout_is_open / stderr_is_open flags before closing and releasing the DataInputStream:
stdout_is_open = false; // flag cleared first
if (dis_out != null) { dis_out.close(); } // closed later (can be slow)
dis_out = null;
if (!stdout_is_open && !stderr_is_open) { finish(); }
So the first reader to finish can observe both flags clear and call finish() — which sets status = FINISHED and emits task_complete() — while the other reader is still inside dis_*.close() / dis_* = null. The owner (Main.create_snapshot_with_rsync, which polls while (task.status == AppStatus.RUNNING)) then proceeds to tear the task down while a reader thread is still unreffing its stream, double-unreffing the underlying UnixInputStream → SIGSEGV in g_object_unref.
Two contributing defects:
- The
*_is_open flags are cleared before the stream is closed, so finish() can fire prematurely.
- The reader threads are created detached (
new Thread<void>.try(...)) and never joined, so a reader can outlive completion signalling. The finish_called guard is also read/written without synchronization.
ThreadSanitizer (meson … -Db_sanitize=thread -Db_pie=true, run via setarch -R, on --create) confirms it directly:
WARNING: ThreadSanitizer: data race (AsyncTask.vala:292 in async_task_finish)
Read of size 4 (finish_called) by thread T2:
async_task_finish AsyncTask.vala:292 // if (finish_called)
async_task_read_stdout AsyncTask.vala:220
Previous write of size 4 by thread T3:
async_task_finish AsyncTask.vala:293 // finish_called = true
async_task_read_stderr AsyncTask.vala:255
After the fix, ThreadSanitizer no longer reports the finish_called / concurrent-finish() race; only pre-existing benign lock-free polling races on the progress counters (progress, percent, prg_count, status) remain — those exist in both versions and don't cause the crash.
A pull request with a fix follows.
Describe the bug
Timeshift sporadically crashes with SIGSEGV during teardown, right after a snapshot has been created successfully. The snapshot itself is fine and no data is lost — only the cleanup crashes. The crash is a double
g_object_unrefin one ofAsyncTask's reader threads while the underlying input stream is being released. It is triggered by the scheduledtimeshift --check --scriptedrun from root cron and recurs over time.To Reproduce
It is a timing-dependent race and does not reproduce on every run. It happens when:
--check --scripted, or--create --scripted).AsyncTaskreader threads shut down.g_object_unref).On the affected machine it was captured from cron three times — 2026-05-02, 2026-05-23 and 2026-06-15 (
coredumpctl list /usr/bin/timeshift; older ones already rotated out).Expected behavior
The process should exit cleanly after creating the snapshot, without crashing during teardown.
System:
25.12.4+zena)Additional context (root-cause analysis)
Symbolized backtrace (coredump resolved against a local debug build of the same source; the packaged binary is stripped):
The crash is on a reader thread, not the main thread.
In
AsyncTask,read_stdout()/read_stderr()clear theirstdout_is_open/stderr_is_openflags before closing and releasing theDataInputStream:So the first reader to finish can observe both flags clear and call
finish()— which setsstatus = FINISHEDand emitstask_complete()— while the other reader is still insidedis_*.close()/dis_* = null. The owner (Main.create_snapshot_with_rsync, which pollswhile (task.status == AppStatus.RUNNING)) then proceeds to tear the task down while a reader thread is still unreffing its stream, double-unreffing the underlyingUnixInputStream→ SIGSEGV ing_object_unref.Two contributing defects:
*_is_openflags are cleared before the stream is closed, sofinish()can fire prematurely.new Thread<void>.try(...)) and never joined, so a reader can outlive completion signalling. Thefinish_calledguard is also read/written without synchronization.ThreadSanitizer (
meson … -Db_sanitize=thread -Db_pie=true, run viasetarch -R, on--create) confirms it directly:After the fix, ThreadSanitizer no longer reports the
finish_called/ concurrent-finish()race; only pre-existing benign lock-free polling races on the progress counters (progress,percent,prg_count,status) remain — those exist in both versions and don't cause the crash.A pull request with a fix follows.