Skip to content

Commit 1a875a7

Browse files
committed
Avoid leaking stale filesystem store temp files
Async filesystem writes may be awaited out of order, making older writes stale after their temporary data has already been created. Clean up stale or failed write attempts on a best-effort basis so they do not leave historical plaintext data in *.tmp files, without reporting cleanup failures as persistence failures. Co-Authored-By: HAL 9000 This finding was discovered by Project Loupe
1 parent c9260ee commit 1a875a7

2 files changed

Lines changed: 91 additions & 41 deletions

File tree

lightning-persister/src/fs_store/common.rs

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -277,64 +277,81 @@ impl FilesystemStoreInner {
277277
let tmp_file_ext = format!("{}.tmp", self.tmp_file_counter.fetch_add(1, Ordering::AcqRel));
278278
tmp_file_path.set_extension(tmp_file_ext);
279279

280-
{
281-
let mut tmp_file = fs::File::create(&tmp_file_path)?;
282-
tmp_file.write_all(&buf)?;
283-
284-
// If we need to preserve the original mtime (for updates), set it before fsync.
285-
if let Some(mtime) = mtime {
286-
let times = fs::FileTimes::new().set_modified(mtime);
287-
tmp_file.set_times(times)?;
288-
}
280+
let tmp_file_res = match fs::File::create(&tmp_file_path) {
281+
Ok(mut tmp_file) => (|| -> lightning::io::Result<()> {
282+
tmp_file.write_all(&buf)?;
283+
284+
// If we need to preserve the original mtime (for updates), set it before fsync.
285+
if let Some(mtime) = mtime {
286+
let times = fs::FileTimes::new().set_modified(mtime);
287+
tmp_file.set_times(times)?;
288+
}
289289

290-
tmp_file.sync_all()?;
290+
tmp_file.sync_all()?;
291+
Ok(())
292+
})(),
293+
Err(e) => return Err(e.into()),
294+
};
295+
if let Err(e) = tmp_file_res {
296+
let _ = fs::remove_file(&tmp_file_path);
297+
return Err(e);
291298
}
292299

293-
self.execute_locked_write(inner_lock_ref, dest_file_path.clone(), version, || {
294-
#[cfg(not(target_os = "windows"))]
295-
{
296-
fs::rename(&tmp_file_path, &dest_file_path)?;
297-
let dir_file = fs::OpenOptions::new().read(true).open(&parent_directory)?;
298-
dir_file.sync_all()?;
299-
Ok(())
300-
}
300+
let mut tmp_file_needs_cleanup = true;
301+
let write_res =
302+
self.execute_locked_write(inner_lock_ref, dest_file_path.clone(), version, || {
303+
#[cfg(not(target_os = "windows"))]
304+
{
305+
fs::rename(&tmp_file_path, &dest_file_path)?;
306+
tmp_file_needs_cleanup = false;
307+
let dir_file = fs::OpenOptions::new().read(true).open(&parent_directory)?;
308+
dir_file.sync_all()?;
309+
Ok(())
310+
}
301311

302-
#[cfg(target_os = "windows")]
303-
{
304-
let res = if dest_file_path.exists() {
305-
call!(unsafe {
306-
windows_sys::Win32::Storage::FileSystem::ReplaceFileW(
312+
#[cfg(target_os = "windows")]
313+
{
314+
let res = if dest_file_path.exists() {
315+
call!(unsafe {
316+
windows_sys::Win32::Storage::FileSystem::ReplaceFileW(
307317
path_to_windows_str(&dest_file_path).as_ptr(),
308318
path_to_windows_str(&tmp_file_path).as_ptr(),
309319
std::ptr::null(),
310320
windows_sys::Win32::Storage::FileSystem::REPLACEFILE_IGNORE_MERGE_ERRORS,
311321
std::ptr::null_mut() as *const core::ffi::c_void,
312322
std::ptr::null_mut() as *const core::ffi::c_void,
313323
)
314-
})
315-
} else {
316-
call!(unsafe {
317-
windows_sys::Win32::Storage::FileSystem::MoveFileExW(
324+
})
325+
} else {
326+
call!(unsafe {
327+
windows_sys::Win32::Storage::FileSystem::MoveFileExW(
318328
path_to_windows_str(&tmp_file_path).as_ptr(),
319329
path_to_windows_str(&dest_file_path).as_ptr(),
320330
windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH
321331
| windows_sys::Win32::Storage::FileSystem::MOVEFILE_REPLACE_EXISTING,
322332
)
323-
})
324-
};
325-
326-
match res {
327-
Ok(()) => {
328-
// We fsync the dest file in hopes this will also flush the metadata to disk.
329-
let dest_file =
330-
fs::OpenOptions::new().read(true).write(true).open(&dest_file_path)?;
331-
dest_file.sync_all()?;
332-
Ok(())
333-
},
334-
Err(e) => Err(e.into()),
333+
})
334+
};
335+
336+
match res {
337+
Ok(()) => {
338+
tmp_file_needs_cleanup = false;
339+
// We fsync the dest file in hopes this will also flush the metadata to disk.
340+
let dest_file = fs::OpenOptions::new()
341+
.read(true)
342+
.write(true)
343+
.open(&dest_file_path)?;
344+
dest_file.sync_all()?;
345+
Ok(())
346+
},
347+
Err(e) => Err(e.into()),
348+
}
335349
}
336-
}
337-
})
350+
});
351+
if tmp_file_needs_cleanup {
352+
let _ = fs::remove_file(&tmp_file_path);
353+
}
354+
write_res
338355
}
339356

340357
fn remove_version(

lightning-persister/src/fs_store/v2.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,39 @@ mod tests {
444444
assert_eq!(listed_keys.len(), 0);
445445
}
446446

447+
#[cfg(feature = "tokio")]
448+
#[tokio::test]
449+
async fn stale_write_does_not_leak_tmp_file() {
450+
use lightning::util::persist::KVStore;
451+
452+
let mut temp_path = std::env::temp_dir();
453+
temp_path.push("test_stale_write_does_not_leak_tmp_file_v2");
454+
let _ = fs::remove_dir_all(&temp_path);
455+
let fs_store = FilesystemStoreV2::new(temp_path.clone()).unwrap();
456+
457+
let data1 = vec![1u8; 32];
458+
let data2 = vec![2u8; 32];
459+
460+
let primary = "testspace";
461+
let secondary = "testsubspace";
462+
let key = "testkey";
463+
464+
let fut1 = KVStore::write(&fs_store, primary, secondary, key, data1);
465+
let fut2 = KVStore::write(&fs_store, primary, secondary, key, data2);
466+
467+
fut2.await.unwrap();
468+
fut1.await.unwrap();
469+
470+
let dir = temp_path.join(primary).join(secondary);
471+
let tmp_files: Vec<_> = fs::read_dir(&dir)
472+
.unwrap()
473+
.filter_map(|e| e.ok())
474+
.map(|e| e.path())
475+
.filter(|p| p.extension().map_or(false, |ext| ext == "tmp"))
476+
.collect();
477+
assert!(tmp_files.is_empty(), "Found leaked tmp files: {:?}", tmp_files);
478+
}
479+
447480
#[test]
448481
fn test_data_migration() {
449482
let mut source_temp_path = std::env::temp_dir();

0 commit comments

Comments
 (0)