gitbutlerapp/gitbutler · error

BUG: a signal has caused the tempfile to be removed, but we

Error message

BUG: a signal has caused the tempfile to be removed, but we didn't install a handler

What it means

`gix::tempfile::Handle::persist` returns `Ok(None)` when the tempfile no longer exists - normally because a signal handler removed it. but-utils did not install a signal handler for this handle, so this state means a signal (or another gix component's signal cleanup in the same process) destroyed the tempfile between write and rename (crates/but-utils/src/lib.rs:167).

Source

Thrown at crates/but-utils/src/lib.rs:167

    contents: impl AsRef<[u8]>,
) -> std::io::Result<()> {
    let mut temp_file = gix::tempfile::new(
        file_path.as_ref().parent().unwrap(),
        ContainingDirectory::CreateAllRaceProof(Retries::default()),
        AutoRemove::Tempfile,
    )?;
    temp_file.write_all(contents.as_ref())?;
    persist_tempfile(temp_file, file_path)
}

fn persist_tempfile(
    tempfile: gix::tempfile::Handle<gix::tempfile::handle::Writable>,
    to_path: impl AsRef<Path>,
) -> std::io::Result<()> {
    match tempfile.persist(to_path) {
        Ok(Some(_opened_file)) => Ok(()),
        Ok(None) => {
            unreachable!(
                "BUG: a signal has caused the tempfile to be removed, but we didn't install a handler"
            )
        }
        Err(err) => Err(err.error),
    }
}

/// Reads and parses the state file.
///
/// If the file does not exist, it will be created.
pub fn read_toml_file_or_default<T: DeserializeOwned + Default>(path: &Path) -> Result<T> {
    let mut file = match File::open(path) {
        Ok(f) => f,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(T::default()),
        Err(err) => return Err(err.into()),
    };
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Retry the whole write operation: recreate the tempfile, rewrite contents, persist again - the destination file was never touched, so a retry is safe
  2. Ensure only one component in the process installs gix signal handlers, so stray cleanup does not drop handles it does not own
  3. If the signal was the user quitting, propagate the termination instead of looping

Example fix

// before
fn persist_tempfile(tempfile: gix::tempfile::Handle<gix::tempfile::handle::Writable>, to_path: impl AsRef<Path>) -> std::io::Result<()> {
    match tempfile.persist(to_path) {
        Ok(Some(_)) => Ok(()),
        Ok(None) => unreachable!("BUG: a signal has caused the tempfile to be removed..."),
        Err(err) => Err(err.error),
    }
}

// after - signal removal is transient; caller recreates and retries
match tempfile.persist(to_path) {
    Ok(Some(_)) => Ok(()),
    Ok(None) => Err(std::io::Error::new(std::io::ErrorKind::Interrupted, "tempfile removed by signal before persist")),
    Err(err) => Err(err.error),
}
// caller: on ErrorKind::Interrupted, rebuild the tempfile and write again
Defensive patterns

Strategy: retry

Validate before calling

// Nothing to validate pre-call; the state is created by a signal between write and persist.
// Reduce the window: write fast, and avoid running state writes during shutdown handlers.

Try / catch

// Treat Ok(None)/Interrupted as transient and redo the whole write
fn write_atomic_retry(path: &Path, contents: &[u8]) -> std::io::Result<()> {
    for _ in 0..3 {
        match persist_tempfile(make_and_write_tempfile(contents)?, path) {
            Ok(()) => return Ok(()),
            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
            Err(e) => return Err(e),
        }
    }
    Err(std::io::Error::new(std::io::ErrorKind::Interrupted, "tempfile kept being removed by signals"))
}

Prevention

When it happens

Trigger: The process receives SIGINT/SIGTERM precisely while an atomic state-file write is in flight (tempfile created and written, persist not yet done); or an embedded gix layer registered signal cleanup that closed this handle.

Common situations: Ctrl-C / service stop during a config or state write; a long-running host process mixing gix components with differing signal-handling setups; container teardown signals.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/648d4f0abdbabe79. Report an issue: GitHub.