Skip to content

AsyncTask: SIGSEGV (double g_object_unref) during teardown after a snapshot completes #551

Description

@Cigydd

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:

  1. A snapshot is actually created (a due --check --scripted, or --create --scripted).
  2. The rsync child finishes and the two AsyncTask reader threads shut down.
  3. 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:

  1. The *_is_open flags are cleared before the stream is closed, so finish() can fire prematurely.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions