Skip to content

Commit 09c9f05

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. Ensure those stale or failed write attempts do not leave historical plaintext data in *.tmp files. Co-Authored-By: HAL 9000 This finding was discovered by Project Loupe
1 parent c9260ee commit 09c9f05

2 files changed

Lines changed: 98 additions & 40 deletions

File tree

lightning-persister/src/fs_store/common.rs

Lines changed: 65 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ fn path_to_windows_str<T: AsRef<OsStr>>(path: &T) -> Vec<u16> {
4242
path.as_ref().encode_wide().chain(Some(0)).collect()
4343
}
4444

45+
fn remove_file_if_exists(path: &Path) -> lightning::io::Result<()> {
46+
match fs::remove_file(path) {
47+
Ok(()) => Ok(()),
48+
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
49+
Err(e) => Err(e.into()),
50+
}
51+
}
52+
4553
// The number of times we retry listing keys in `FilesystemStore::list` before we give up reaching
4654
// a consistent view and error out.
4755
const LIST_DIR_CONSISTENCY_RETRIES: usize = 10;
@@ -277,64 +285,81 @@ impl FilesystemStoreInner {
277285
let tmp_file_ext = format!("{}.tmp", self.tmp_file_counter.fetch_add(1, Ordering::AcqRel));
278286
tmp_file_path.set_extension(tmp_file_ext);
279287

280-
{
281-
let mut tmp_file = fs::File::create(&tmp_file_path)?;
282-
tmp_file.write_all(&buf)?;
288+
let tmp_file_res = match fs::File::create(&tmp_file_path) {
289+
Ok(mut tmp_file) => (|| -> lightning::io::Result<()> {
290+
tmp_file.write_all(&buf)?;
283291

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-
}
292+
// If we need to preserve the original mtime (for updates), set it before fsync.
293+
if let Some(mtime) = mtime {
294+
let times = fs::FileTimes::new().set_modified(mtime);
295+
tmp_file.set_times(times)?;
296+
}
289297

290-
tmp_file.sync_all()?;
298+
tmp_file.sync_all()?;
299+
Ok(())
300+
})(),
301+
Err(e) => return Err(e.into()),
302+
};
303+
if let Err(e) = tmp_file_res {
304+
let _ = remove_file_if_exists(&tmp_file_path);
305+
return Err(e);
291306
}
292307

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-
}
308+
let mut tmp_file_needs_cleanup = true;
309+
let write_res =
310+
self.execute_locked_write(inner_lock_ref, dest_file_path.clone(), version, || {
311+
#[cfg(not(target_os = "windows"))]
312+
{
313+
fs::rename(&tmp_file_path, &dest_file_path)?;
314+
tmp_file_needs_cleanup = false;
315+
let dir_file = fs::OpenOptions::new().read(true).open(&parent_directory)?;
316+
dir_file.sync_all()?;
317+
Ok(())
318+
}
301319

302-
#[cfg(target_os = "windows")]
303-
{
304-
let res = if dest_file_path.exists() {
305-
call!(unsafe {
306-
windows_sys::Win32::Storage::FileSystem::ReplaceFileW(
320+
#[cfg(target_os = "windows")]
321+
{
322+
let res = if dest_file_path.exists() {
323+
call!(unsafe {
324+
windows_sys::Win32::Storage::FileSystem::ReplaceFileW(
307325
path_to_windows_str(&dest_file_path).as_ptr(),
308326
path_to_windows_str(&tmp_file_path).as_ptr(),
309327
std::ptr::null(),
310328
windows_sys::Win32::Storage::FileSystem::REPLACEFILE_IGNORE_MERGE_ERRORS,
311329
std::ptr::null_mut() as *const core::ffi::c_void,
312330
std::ptr::null_mut() as *const core::ffi::c_void,
313331
)
314-
})
315-
} else {
316-
call!(unsafe {
317-
windows_sys::Win32::Storage::FileSystem::MoveFileExW(
332+
})
333+
} else {
334+
call!(unsafe {
335+
windows_sys::Win32::Storage::FileSystem::MoveFileExW(
318336
path_to_windows_str(&tmp_file_path).as_ptr(),
319337
path_to_windows_str(&dest_file_path).as_ptr(),
320338
windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH
321339
| windows_sys::Win32::Storage::FileSystem::MOVEFILE_REPLACE_EXISTING,
322340
)
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()),
341+
})
342+
};
343+
344+
match res {
345+
Ok(()) => {
346+
tmp_file_needs_cleanup = false;
347+
// We fsync the dest file in hopes this will also flush the metadata to disk.
348+
let dest_file = fs::OpenOptions::new()
349+
.read(true)
350+
.write(true)
351+
.open(&dest_file_path)?;
352+
dest_file.sync_all()?;
353+
Ok(())
354+
},
355+
Err(e) => Err(e.into()),
356+
}
335357
}
336-
}
337-
})
358+
});
359+
if tmp_file_needs_cleanup {
360+
let _ = remove_file_if_exists(&tmp_file_path);
361+
}
362+
write_res
338363
}
339364

340365
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)