Save metadata and journal staleness: second-pass fixes

- Ownership is applied before permissions: chown clears setuid/setgid,
  so saving a 06755 file left it 0755.
- The umask is read once during static initialisation. Reading it at
  save time needed umask(0)/umask(old), briefly changing it for the swap
  writer thread creating files.
- Before rewriting a hard-linked file in place, the new content is
  written and synced to a copy beside it; if the in-place write fails
  (ENOSPC, EIO) the copy is kept and named in the error.
- Journal headers also record a CRC-32 of the base file (flags bit 1,
  bytes 40..43). A changed mtime with the same size and content
  (touch, checkout back and forth) no longer marks a journal stale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
Claude
2026-09-24 09:16:04 -07:00
committed by Kyle Isom
parent 6c2727789e
commit 1005f5dbe8
6 changed files with 162 additions and 14 deletions
+54 -8
View File
@@ -136,6 +136,16 @@ best_effort_fsync_dir(const std::string &path)
}
// The process umask, read once during static initialisation (before any
// thread exists): reading it later needs umask(0) + umask(old), which briefly
// changes it for the swap writer thread creating files concurrently.
static const mode_t g_process_umask = [] {
const mode_t m = ::umask(0);
(void) ::umask(m);
return m;
}();
// Rewrite `path` in place (truncate, write, fsync). Used for files with
// several hard links, which a temp-file-and-rename save would split.
static bool
@@ -160,6 +170,31 @@ write_in_place(const std::string &path, const char *data, std::size_t len, std::
}
// Create `path` (which must not exist) with the content, fsynced.
static bool
write_new_file(const std::string &path, const char *data, std::size_t len, std::string &err)
{
int flags = O_WRONLY | O_CREAT | O_EXCL;
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
const int fd = kte::syscall::Open(path.c_str(), flags, 0600);
if (fd < 0) {
err = "Failed to create " + path + ": " + std::strerror(errno);
return false;
}
bool ok = write_all_fd(fd, data, len, err);
if (ok && kte::syscall::Fsync(fd) != 0) {
err = std::string("fsync failed: ") + std::strerror(errno);
ok = false;
}
(void) kte::syscall::Close(fd);
if (!ok)
(void) ::unlink(path.c_str());
return ok;
}
static bool
atomic_write_file(const std::string &path_in, const char *data, std::size_t len, std::string &err)
{
@@ -183,8 +218,20 @@ atomic_write_file(const std::string &path_in, const char *data, std::size_t len,
// A file with other hard links must be rewritten in place, or those
// names would keep the old content.
if (dst_exists && S_ISREG(dst_st.st_mode) && dst_st.st_nlink > 1)
return write_in_place(path, data, len, err);
if (dst_exists && S_ISREG(dst_st.st_mode) && dst_st.st_nlink > 1) {
// Truncating in place risks the only copy if the write then fails
// (ENOSPC, EIO, crash), so first write and sync a copy beside it.
const std::string copy = path + ".kte-save";
if (!write_new_file(copy, data, len, err))
return false;
if (!write_in_place(path, data, len, err)) {
err += " (the new content is in " + copy + ")";
return false;
}
(void) ::unlink(copy.c_str());
best_effort_fsync_dir(path);
return true;
}
// Create a temp file in the same directory so rename() is atomic.
std::filesystem::path p(path);
@@ -216,17 +263,16 @@ atomic_write_file(const std::string &path_in, const char *data, std::size_t len,
std::string tmp_path(buf.data());
if (dst_exists) {
// Carry over permissions and (best effort; needs privilege to give a
// file away) ownership of the file being replaced.
(void) kte::syscall::Fchmod(fd, dst_st.st_mode & 07777);
// Carry over ownership (best effort; needs privilege to give a file
// away) and then permissions: chown clears setuid/setgid, so the
// mode has to be applied after it.
if (::fchown(fd, dst_st.st_uid, dst_st.st_gid) != 0) {
// Expected without privilege when the owner differs; keep ours.
}
(void) kte::syscall::Fchmod(fd, dst_st.st_mode & 07777);
} else {
// mkstemp creates 0600; a new file gets the usual 0666 & ~umask.
const mode_t mask = ::umask(0);
(void) ::umask(mask);
(void) kte::syscall::Fchmod(fd, 0666 & ~mask);
(void) kte::syscall::Fchmod(fd, 0666 & ~g_process_umask);
}
bool ok = write_all_fd(fd, data, len, err);