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
- Use FileStore as the replay-protection backend
- Simulate a disk-full or I/O error during write_all (e.g. ulimit -f 0)
- Observe: the key file is created but empty
- Any subsequent charge attempt with the same key returns Ok(false) (false replay rejection)
- Any get() on the key returns StoreError::Serialization
- 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
Describe the bug
In
src/store.rs,FileStore::put_if_absentusesOpenOptions::create_new(true)(O_EXCL) to atomically create the file, then callswrite_all()in a separate step. Ifwrite_allfails (disk full, I/O error, signal), the file is created but left empty or truncated.From that point:
put_if_absentcalls →AlreadyExists→ returnsOk(false)(false "already used" replay rejection)getcalls → empty file fails JSON deserialization →StoreError::SerializationThe replay-protection key is permanently poisoned. Legitimate charges are permanently rejected with no recovery path.
Affected code (
src/store.rs):Fix: write to a temp file first, then
fs::rename(atomic on POSIX):Steps to reproduce
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