Skip to content

timeshift --create hangs indefinitely at exit in infinite read() = 0 loop on aarch64 (Fedora 44 / Kernel 7.2.x) #570

Open

Description

@fredcwells

Describe the bug
When running scheduled or scripted backups via timeshift --create, the underlying rsync execution script completes cleanly with an exit code of 0. However, the parent timeshift process hangs indefinitely, consuming 100% of an ARM core thread. It never writes info.json, leaving the snapshot unregistered and vulnerable to automated purging on subsequent runs.
Running an strace on the stuck parent process PID reveals an unhandled infinite spin loop querying a closed pipe descriptor at End-of-File (EOF):

[pid 6122] read(6, "", 1048576) = 0
[pid 6122] read(6, "", 1048576) = 0
[pid 6122] read(6, "", 1048576) = 0
... (repeats indefinitely)

To Reproduce
Steps to reproduce the behavior:

  1. Run timeshift cron job

Expected behavior
Timeshift completes and registers backup

System:
• OS: Fedora 44 (aarch64 / Raspberry Pi Server)
• Kernel: 7.2.x (Previously functioning normally on Fedora 43 with Kernel 7.1.x)
• Application Version: timeshift 25.12.14 (RSYNC mode)
• Core Libraries: glib2 >= 2.88

Root Cause Analysis:
The issue seems to lie in src/Core/AsyncTask.vala. When AsyncTask sets up asynchronous standard output/error monitoring streams (GIOChannel) to track rsync progress metrics, it handles stream boundaries interactively.
Apparently, under the newer Kernel 7.2 task scheduler and GLib 2.88 on weakly ordered memory architectures (aarch64), the child execution process terminates and its pipe write-end closes rapidly. This immediate closure forces the kernel to yield 0 bytes on the read end instantly. Because the Vala-compiled async loop lacks a hard escape/break condition specifically checking for bytes_read <= 0 or missing the G_SOURCE_REMOVE return callback signal when an empty read hits a non-blocking stream, the application loops infinitely on the same descriptor without ever reaching the downstream finalization sequences (such as writing info.json or unmounting temporary targets).

This issue appears to be heavily architecture/timing dependent, as an identical deployment of timeshift 25.12.14 on an x86_64 architecture running Fedora 44 / Kernel 7.2 executes cleanly without encountering the race condition.

Suggested Code Location for Patching:
The infinite loop occurs within src/Core/AsyncTask.vala inside the asynchronous IOChannel/DataStream channel read callback routines. The stream watch needs an explicit break statement or should return false (G_SOURCE_REMOVE) whenever bytes_read <= 0 or an EOF status condition is encountered, preventing GLib from continuously refiring the empty read event on aarch64.

Activity

  1. michaelkory commented on Oct 10, 2026

    @michaelkory

    I reproduced the same post-rsync ARM64 hang on Ubuntu 26.04 with Timeshift 25.12.4-1, RSYNC mode (headless CLI; no display server). There is a deterministic EOF bug in file_line_count() in src/Utility/TeeJee.FileSystem.vala, independent of async callback timing. This also looks relevant to #523. I have not tested the Fedora package, so this does not establish that every reported hang has the same cause.

    Root cause and affected source

    BufferedInputStream.read_byte() returns an integer, with -1 for EOF. The function narrows it to char before comparing with -1. On the native AArch64 compiler used here, plain char is unsigned (__CHAR_UNSIGNED__ 1), so EOF becomes 255 and the loop never exits. The generated C confirms the narrowing to gchar; retaining gint fixes it.

    The faulty function is identical in Ubuntu's 25.12.4 source and upstream 26.09.0/master at e7e54ab7b96f0ca79774662544ccd93e58059b48. Master was rechecked before posting.

    Minimal patch

    --- a/src/Utility/TeeJee.FileSystem.vala
    +++ b/src/Utility/TeeJee.FileSystem.vala
    @@ -99,12 +99,12 @@
     
     		try {
     			long line_nums = 0;
    -			char symbol;
    +			int symbol;
     
     			File file = File.new_for_path(file_path);
     			FileInputStream inStream = file.read();
     			BufferedInputStream bis = new BufferedInputStream.sized(inStream,  (size_t) (1 * MiB));
    -			while((symbol = (char) bis.read_byte()) != -1) {
    +			while((symbol = bis.read_byte()) != -1) {
     				if(symbol == '\n') {
     					line_nums ++;
     				}

    This preserves newline counting, buffering, stream closure and error handling. Keeping the integer also avoids treating a real byte 0xff as EOF on signed-char targets (that case was not executed on a signed-char target here).

    Focused reproduction without taking a system snapshot

    On native Ubuntu 26.04 AArch64, with Vala 0.56.18, GCC 15.2 and GLib 2.88, extract the original function from the source checkout at the revision above:

    perl -0777 -ne 'm/(\tpublic int64\? file_line_count .*?\n\t\})/s or die "function not found"; print "$1\n"' \
      src/Utility/TeeJee.FileSystem.vala > old-function.vala

    Save this minimal caller as direct-call.vala:

    const int64 MiB = 1024 * 1024;
    void log_error (string message) { stderr.printf ("%s\n", message); }
    int main (string[] args) {
        int64? count = file_line_count (args[1]);
        if (count == null) return 1;
        stdout.printf ("%s\n", count.to_string ());
        return 0;
    }

    Compile and call the verbatim function, not a rewritten model of its loop:

    valac --pkg gio-2.0 -X '-DGETTEXT_PACKAGE="timeshift"' direct-call.vala old-function.vala -o old
    printf '' > empty
    printf 'one\ntwo\n' > two-lines
    printf '\377\n' > ff-line
    for input in empty two-lines ff-line; do
      timeout 2 ./old "$input"; printf 'exit=%s\n' "$?"
    done

    Apply the two-line patch to the source, repeat the same extraction into fixed-function.vala, then compile with that file and -o fixed and run the same three inputs.

    Observed native results (each invocation bounded by timeout 2):

    Input Original Patched
    Empty file timeout, exit 124 count 0, exit 0
    one\ntwo\n timeout, exit 124 count 2, exit 0
    byte 0xff followed by newline timeout, exit 124 count 1, exit 0

    The focused calls and package builds ran in a native ARM64 build container; full Timeshift snapshot validation ran on the Ubuntu guest itself, not inside the container. A locally rebuilt Ubuntu package with only this upstream source change built successfully, and rebuilding the retained corrected source produced a byte-identical binary package. After installing it, real RSYNC snapshots completed, wrote info.json, appeared in native listings and contained the system/dpkg payload. A subsequent snapshot-protected package update also completed successfully. No destructive restore or reboot validation is claimed.

    Posting the diagnosis, direct reproduction and minimal patch here rather than opening a duplicate issue or a fork/PR.

  2. ygerlach commented on Oct 10, 2026

    @ygerlach
    Contributor

    Oops, i think i introduced this bug. I think i did some testing on this function before submitting the PR. Maybe we should add some pipeline tests, so i could have commited the tests and they can also be reviewed...
    I will provide a fix PR this weekend.

  3. fredcwells commented on Oct 29, 2026

    @fredcwells
    Author

    Looks like it's merge-blocked. Not clear why.

  4. fredcwells commented on Nov 1, 2026

    @fredcwells
    Author

    Is this a clue to the merge block?

    Check failure on line R70
    Check failure:
    FileTest ==> file test
    Build / build / build (mint22, linuxmintd/mint22.3-amd64, Mint 22, true) / Mint 22

  5. fredcwells commented on Nov 1, 2026

    @fredcwells
    Author

    So, I applied the @michaelkory minimal patch, successfully rebuilt & tested full snapshot locally on my Fedora 44.
    Sadly, no fix. :(

    CORRECTION: The patch actually did fix the problem - once I actually applied it correctly.

  6. srebrovvilen01 commented on Nov 8, 2026

    @srebrovvilen01

    I had the same problem with Armbian Resolute on my Rock 5 ITX. Applying the patch solved it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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