Skip to content

Commit 3090f2f

Browse files
committed
fix(compaction): reject cross-filesystem working directory up front
- a cross-filesystem --working_dir made rename(tmp, from_dir) fail; with --make_backup this was especially problematic: after from_dir was renamed away to the backup, that rename failed and left from_dir not properly restored, so the database could not start even though the data itself survived in the backup (reported by Copilot review) - check that from_dir and the working directory are on the same filesystem right after the working directory is determined, before any destructive step, so both default and --make_backup modes fail with a clear message - cover the cross-filesystem rejection for both modes and assert the log directory is left byte-for-byte intact with no backup directory created
1 parent b1a6030 commit 3090f2f

2 files changed

Lines changed: 102 additions & 54 deletions

File tree

src/limestone/dblogutil/dblogutil.cpp

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
#include <sys/stat.h>
18+
1719
#include <iostream>
1820
#include <stdlib.h> // NOLINT(*-deprecated-headers): <cstdlib> does not provide std::mkdtemp
1921
#include <glog/logging.h>
@@ -182,6 +184,24 @@ boost::filesystem::path make_backup_dir_next_to(const boost::filesystem::path& t
182184
return make_tmp_dir_next_to(target_dir, ".backup_XXXXXX");
183185
}
184186

187+
/**
188+
* @brief tests whether two existing directories reside on the same filesystem.
189+
* @param a a path to an existing directory
190+
* @param b a path to an existing directory
191+
* @return true if both are on the same filesystem (same st_dev), false otherwise
192+
*/
193+
bool on_same_filesystem(boost::filesystem::path const& a, boost::filesystem::path const& b) {
194+
struct stat sa {};
195+
struct stat sb {};
196+
if (::stat(a.c_str(), &sa) != 0) {
197+
LOG_AND_THROW_IO_EXCEPTION("stat failed: " + a.string(), errno);
198+
}
199+
if (::stat(b.c_str(), &sb) != 0) {
200+
LOG_AND_THROW_IO_EXCEPTION("stat failed: " + b.string(), errno);
201+
}
202+
return sa.st_dev == sb.st_dev;
203+
}
204+
185205
// Carry over the existing compaction catalog (and its backup) from from_dir into the
186206
// work directory tmp, then register the freshly produced compacted file. tmp will
187207
// replace from_dir, so carrying over the catalog preserves the high-water marks
@@ -289,10 +309,9 @@ void copy_directory_recursively(
289309
* Without this step the blob data would be lost when from_dir is removed (or renamed away for
290310
* backup) and replaced by the working directory. When make_backup is requested the blob
291311
* directory is copied so that the backup keeps its own blob data; otherwise it is moved by
292-
* rename. The rename requires from_dir and tmp to be on the same filesystem; this holds for
293-
* the default working directory (created next to from_dir), and the final rename(tmp, from_dir)
294-
* in compaction() relies on the same condition, so a cross-filesystem working directory is
295-
* rejected here with a clear message rather than failing obscurely later.
312+
* rename. The move requires from_dir and tmp to be on the same filesystem; compaction()
313+
* already rejects a cross-filesystem working directory up front, so the rename below is a
314+
* defense-in-depth check that should not normally fail.
296315
* @param from_dir the original log directory
297316
* @param tmp the working directory the compacted log is assembled in
298317
* @param make_backup true if from_dir is kept as a backup, so blob data must remain in it
@@ -356,6 +375,19 @@ void compaction(dblog_scan &ds, std::optional<epoch_id_type> epoch) {
356375
}
357376
std::cout << "working-directory: " << tmp << std::endl;
358377

378+
// The working directory must be on the same filesystem as the dblogdir. Compaction
379+
// carries the log directory contents over into tmp and finally renames tmp onto
380+
// from_dir; both the blob move and that final rename fail across filesystems. With
381+
// --make_backup this would be especially harmful: from_dir is first renamed away to
382+
// the backup, and only then does rename(tmp, from_dir) fail, leaving no from_dir
383+
// restored. Reject a cross-filesystem working directory up front, before any
384+
// destructive step, regardless of --make_backup.
385+
if (!on_same_filesystem(from_dir, tmp)) {
386+
LOG(ERROR) << "working directory must be on the same filesystem as the dblogdir: "
387+
<< "dblogdir=" << from_dir << ", working-directory=" << tmp;
388+
log_and_exit(64);
389+
}
390+
359391
if (!FLAGS_force) {
360392
// prompt
361393
char yn = 'N';

test/limestone/compaction/offline_compaction_test.cpp

Lines changed: 66 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,64 @@ class offline_compaction_test : public compaction_test {
126126
}
127127
}
128128

129+
// Shared driver for the cross-filesystem working-directory tests. Prepares a log
130+
// directory with blob data, then runs offline compaction with --working_dir on a
131+
// different filesystem (tmpfs under /dev/shm), optionally with --make_backup.
132+
// Compaction must be rejected up front (before any destructive step) and must leave the
133+
// original log directory fully intact. Skips when /dev/shm is unavailable or happens to
134+
// be on the same filesystem as the location.
135+
void run_cross_filesystem_working_dir_case(bool make_backup) {
136+
if (!boost::filesystem::exists("/dev/shm")) {
137+
GTEST_SKIP() << "/dev/shm is not available on this system";
138+
}
139+
140+
gen_datastore();
141+
datastore_->switch_epoch(1);
142+
lc0_->begin_session();
143+
lc0_->add_entry(1, "blob_key", "blob_value", {1, 0}, {1001});
144+
lc0_->end_session();
145+
create_dummy_blob_files(1001);
146+
datastore_->set_next_blob_id(1002);
147+
datastore_->switch_epoch(2);
148+
datastore_->shutdown();
149+
datastore_ = nullptr;
150+
151+
std::map<std::string, std::string> before = snapshot_tree(location);
152+
153+
// Prepare a working directory on another filesystem (tmpfs).
154+
std::string wd_template = "/dev/shm/offline_compaction_wd_XXXXXX";
155+
ASSERT_NE(::mkdtemp(wd_template.data()), nullptr) << strerror(errno);
156+
boost::filesystem::path working_dir{wd_template};
157+
158+
// Skip when the test location happens to live on the same filesystem as /dev/shm
159+
// (e.g. /tmp mounted on the same tmpfs); the cross-filesystem path is unreachable.
160+
struct stat st_location {};
161+
struct stat st_working_dir {};
162+
ASSERT_EQ(::stat(location, &st_location), 0) << strerror(errno);
163+
ASSERT_EQ(::stat(working_dir.c_str(), &st_working_dir), 0) << strerror(errno);
164+
if (st_location.st_dev == st_working_dir.st_dev) {
165+
boost::filesystem::remove_all(working_dir);
166+
GTEST_SKIP() << "test location and /dev/shm are on the same filesystem";
167+
}
168+
169+
std::string out;
170+
std::string command = std::string(util_command) + " compaction --force" +
171+
(make_backup ? " --make_backup" : "") + " --working_dir=" +
172+
working_dir.string() + " " + std::string(location) + " 2>&1";
173+
int rc = invoke(command, out);
174+
EXPECT_NE(rc, 0) << "compaction should fail on a cross-filesystem working directory";
175+
EXPECT_NE(out.find("working directory must be on the same filesystem as the dblogdir"),
176+
std::string::npos)
177+
<< "tglogutil output:\n" << out;
178+
179+
// The upfront rejection must leave the original log directory fully intact, and no
180+
// backup directory must have been created.
181+
EXPECT_EQ(snapshot_tree(location), before);
182+
EXPECT_TRUE(find_backup_dirs().empty());
183+
184+
boost::filesystem::remove_all(working_dir);
185+
}
186+
129187
// Collect the set of top-level entry names (files and directories) directly under dir.
130188
static std::set<std::string> list_top_level_entries(boost::filesystem::path const& dir) {
131189
std::set<std::string> names;
@@ -519,58 +577,16 @@ TEST_F(offline_compaction_test, offline_compaction_backup_matches_pre_compaction
519577
remove_backup_dirs();
520578
}
521579

522-
// When --working_dir points to a different filesystem, moving the blob directory by
523-
// rename cannot work (EXDEV), and neither can the final rename(tmp, from_dir); compaction
524-
// must fail fast with a clear message and leave the original log directory untouched.
580+
// A cross-filesystem --working_dir must be rejected up front (default / move mode).
525581
TEST_F(offline_compaction_test, offline_compaction_fails_when_working_dir_is_on_different_filesystem) {
526-
if (!boost::filesystem::exists("/dev/shm")) {
527-
GTEST_SKIP() << "/dev/shm is not available on this system";
528-
}
529-
530-
gen_datastore();
531-
datastore_->switch_epoch(1);
532-
lc0_->begin_session();
533-
lc0_->add_entry(1, "blob_key", "blob_value", {1, 0}, {1001});
534-
lc0_->end_session();
535-
boost::filesystem::path blob_path = create_dummy_blob_files(1001);
536-
datastore_->set_next_blob_id(1002);
537-
datastore_->switch_epoch(2);
538-
datastore_->shutdown();
539-
datastore_ = nullptr;
540-
541-
// Prepare a working directory on another filesystem (tmpfs).
542-
std::string wd_template = "/dev/shm/offline_compaction_wd_XXXXXX";
543-
ASSERT_NE(::mkdtemp(wd_template.data()), nullptr) << strerror(errno);
544-
boost::filesystem::path working_dir{wd_template};
545-
546-
// Skip when the test location happens to live on the same filesystem as /dev/shm
547-
// (e.g. /tmp mounted on the same tmpfs); rename would then succeed and the error
548-
// path under test would not be reachable.
549-
struct stat st_location {};
550-
struct stat st_working_dir {};
551-
ASSERT_EQ(::stat(location, &st_location), 0) << strerror(errno);
552-
ASSERT_EQ(::stat(working_dir.c_str(), &st_working_dir), 0) << strerror(errno);
553-
if (st_location.st_dev == st_working_dir.st_dev) {
554-
boost::filesystem::remove_all(working_dir);
555-
GTEST_SKIP() << "test location and /dev/shm are on the same filesystem";
556-
}
557-
558-
std::string out;
559-
std::string command = std::string(util_command) + " compaction --force --working_dir=" +
560-
working_dir.string() + " " + std::string(location) + " 2>&1";
561-
int rc = invoke(command, out);
562-
// Compaction must fail: a cross-filesystem working directory cannot work, because both
563-
// the carry-over of the log directory contents and the final rename(tmp, from_dir)
564-
// require the same filesystem. We only assert the failure and that the original log
565-
// directory is left intact, not a specific message: which cross-device operation trips
566-
// first (carrying over the manifest, moving the blob directory, or the final rename)
567-
// depends on the platform's copy/rename behavior.
568-
EXPECT_NE(rc, 0) << "compaction should fail on a cross-filesystem working directory";
569-
570-
// The failure must leave the original log directory untouched.
571-
EXPECT_TRUE(boost::filesystem::exists(blob_path));
582+
run_cross_filesystem_working_dir_case(/*make_backup=*/false);
583+
}
572584

573-
boost::filesystem::remove_all(working_dir);
585+
// A cross-filesystem --working_dir must be rejected up front in --make_backup mode too.
586+
// Otherwise from_dir would be renamed away to the backup and the final rename(tmp, from_dir)
587+
// would fail, leaving no from_dir restored.
588+
TEST_F(offline_compaction_test, offline_compaction_with_backup_fails_when_working_dir_is_on_different_filesystem) {
589+
run_cross_filesystem_working_dir_case(/*make_backup=*/true);
574590
}
575591

576592
// When the blob directory contains a broken entry that cannot be copied, the backup-mode

0 commit comments

Comments
 (0)