Skip to content

FileStore::put_if_absent permanently poisons replay protection keys on partial write failure #394

Description

@Sertug17

Describe the bug

In src/store.rs, FileStore::put_if_absent uses OpenOptions::create_new(true) (O_EXCL) to atomically create the file, then calls write_all() in a separate step. If write_all fails (disk full, I/O error, signal), the file is created but left empty or truncated.

From that point:

  • Future put_if_absent calls → AlreadyExists → returns Ok(false) (false "already used" replay rejection)
  • Future get calls → empty file fails JSON deserialization → StoreError::Serialization

The replay-protection key is permanently poisoned. Legitimate charges are permanently rejected with no recovery path.

Affected code (src/store.rs):

Ok(mut f) => {
    f.write_all(serialized.as_bytes())
        .map_err(|e| StoreError::Internal(e.to_string()))?;
    Ok(true)
    // If write_all fails: file exists but is empty. Key is now poisoned forever.
}

Fix: write to a temp file first, then fs::rename (atomic on POSIX):

let tmp_path = path.with_extension("tmp");
{
    let mut f = File::create(&tmp_path)?;
    f.write_all(serialized.as_bytes())?;
    f.sync_all()?;
}
fs::rename(&tmp_path, &path)?;
Ok(true)

Steps to reproduce

  1. Use FileStore as the replay-protection backend
  2. Simulate a disk-full or I/O error during write_all (e.g. ulimit -f 0)
  3. Observe: the key file is created but empty
  4. Any subsequent charge attempt with the same key returns Ok(false) (false replay rejection)
  5. Any get() on the key returns StoreError::Serialization
  6. There is no recovery the key is permanently poisoned

Logs


Platform(s)

Linux (x86), Linux (ARM)

Container Type

Not running in a container

What version/commit are you on?

85c62c5

If you've built from source, provide the full command you used

No response

Code of Conduct

  • I agree to follow the Code of Conduct

Activity

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