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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions