Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
618f633
Merge remote-tracking branch 'origin/feat/backup-phase0a-keymap-manif…
bootjp Apr 30, 2026
945d73c
backup: address review on Redis simple-type encoder (PR #713)
bootjp Apr 30, 2026
84a1c5a
Merge remote-tracking branch 'origin/feat/backup-phase0a-keymap-manif…
bootjp Apr 30, 2026
c699c15
backup: dedupe string TTL + symlink-guard JSONL (PR #713, round 2)
bootjp Apr 30, 2026
1c70e83
Merge remote-tracking branch 'origin/feat/backup-phase0a-keymap-manif…
bootjp Apr 30, 2026
d965b65
backup: atomic O_NOFOLLOW + count-only orphan TTL (PR #713, round 3)
bootjp Apr 30, 2026
4cb6d83
backup: emit KEYMAP entries for SHA-fallback Redis keys (PR #713, rou…
bootjp Apr 30, 2026
6f35446
backup: fix Windows build by per-platform sidecar open (PR #713, roun…
bootjp Apr 30, 2026
87971ae
Merge remote-tracking branch 'origin/feat/backup-phase0a-keymap-manif…
bootjp Apr 30, 2026
ae3f0d9
backup: refuse hard-link clobber in openSidecarFile (PR #713, round 6)
bootjp Apr 30, 2026
e061b6d
backup: refuse symlinked ancestors + Windows disk-full (PR #713, roun…
bootjp Apr 30, 2026
20236ac
backup: add non-Unix/Windows OS-helper fallbacks (PR #713, round 8)
bootjp Apr 30, 2026
1a5165e
backup: refuse FIFO/non-regular sidecar paths (PR #713, round 9)
bootjp Apr 30, 2026
4872704
Merge remote-tracking branch 'origin/main' into feat/backup-phase0a-r…
bootjp May 2, 2026
0383924
backup: ancestor-symlink check on TTL sidecar open (PR #713, round 13)
bootjp May 2, 2026
8e8f8dd
backup: re-validate dirsCreated cache hits (PR #713, round 14)
bootjp May 2, 2026
011b031
backup: use x/sys/unix Mkfifo for cross-Unix portability (PR #713, ro…
bootjp May 2, 2026
eb5f3c2
backup: wrap os.OpenFile errors in windows/other sidecar openers
github-actions[bot] May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions internal/backup/disk_full_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build !unix && !windows

package backup

// isDiskFullError is the fallback for non-unix/non-windows targets
// (js, wasip1, plan9). Those runtimes either do not surface a
// disk-full errno through Go's syscall package or do not have a
// meaningful disk concept (wasm with no host filesystem, plan9 with
// its own error vocabulary). Returning false matches the documented
// helper contract: callers treat unrecognised errors as
// non-retriable, which is the safe default. Codex P2 round 10.
func isDiskFullError(err error) bool {
_ = err
return false
}
15 changes: 15 additions & 0 deletions internal/backup/disk_full_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build unix

package backup

import (
"errors"
"syscall"
)

// isDiskFullError reports whether err is a POSIX ENOSPC anywhere in
// the wrap chain. os.File.Write surfaces ENOSPC inside an
// os.PathError which errors.Is unwraps natively.
func isDiskFullError(err error) bool {
return errors.Is(err, syscall.ENOSPC)
}
27 changes: 27 additions & 0 deletions internal/backup/disk_full_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//go:build windows

package backup

import (
"errors"
"syscall"

"golang.org/x/sys/windows"
)

// isDiskFullError reports whether err is a Windows disk-full failure.
// We accept both ERROR_DISK_FULL (Win32 errno 112) and
// ERROR_HANDLE_DISK_FULL (39): the kernel returns 112 for write
// failures driven by free-space exhaustion and 39 for the legacy
// handle-level variant some FS drivers still surface. We also keep
// syscall.ENOSPC in the chain because Go's os package occasionally
// translates platform errors into the POSIX value before returning.
func isDiskFullError(err error) bool {
switch {
case errors.Is(err, windows.ERROR_DISK_FULL),
errors.Is(err, windows.ERROR_HANDLE_DISK_FULL),
errors.Is(err, syscall.ENOSPC):
return true
}
return false
}
29 changes: 29 additions & 0 deletions internal/backup/open_nofollow_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//go:build !unix && !windows

package backup

import (
"os"

cockroachdberr "github.com/cockroachdb/errors"
)

// openSidecarFile is the fallback for non-unix/non-windows targets
// (js, wasip1, plan9). syscall.O_NOFOLLOW and the unix nlink-check
// path are unavailable; we keep a Lstat-then-OpenFile guard to at
// least refuse pre-existing symlinks. The remaining TOCTOU window
// is acceptable here because dump tooling on those targets is
// offline / sandboxed and the threat model that motivated the unix
// hardening (a local adversary swapping the path between syscalls)
// does not apply. Codex P2 round 10.
func openSidecarFile(path string) (*os.File, error) {
if info, err := os.Lstat(path); err == nil && info.Mode()&os.ModeSymlink != 0 {
return nil, cockroachdberr.WithStack(cockroachdberr.Newf(
"backup: refusing to overwrite symlink at %s", path))
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) //nolint:gosec,mnd // path is composed from output-root + fixed file name; 0600 is the standard owner-only mode
if err != nil {
return nil, cockroachdberr.WithStack(err)
}
return f, nil
}
84 changes: 84 additions & 0 deletions internal/backup/open_nofollow_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//go:build unix

package backup

import (
"errors"
"os"
"syscall"

cockroachdberr "github.com/cockroachdb/errors"
)

// openSidecarFile opens path for write while refusing symlink,
// hard-link, and non-regular-file (FIFO / socket / device) clobber
// attacks.
//
// - O_NOFOLLOW makes the kernel return ELOOP atomically if the path
// is a symbolic link — closing the TOCTOU race a separate
// Lstat-then-Create pattern would have.
// - O_NONBLOCK guarantees the open does not hang on a pre-existing
// FIFO that has no reader (POSIX: O_WRONLY|O_NONBLOCK on a
// reader-less FIFO returns ENXIO immediately). Without this, a
// stale or adversarial mkfifo at strings_ttl.jsonl would block
// the first TTL write indefinitely; the symlink and hard-link
// guards do not catch this case (`mkfifo` produces nlink=1 and
// is not a symlink). Codex P2 round 11.
// - To also refuse hard links to files outside the dump tree, we
// open WITHOUT O_TRUNC, fstat() the descriptor to check the
// link count, and only call Truncate(0) if Nlink == 1 AND the
// file is a regular file. An adversary that pre-created
// strings_ttl.jsonl as a hard link to /etc/passwd (or any other
// writable file outside the dump tree) would otherwise see the
// inode truncated on openSidecarFile despite the symlink guard.
// Codex P2 round 9.
//
// The Windows build (open_nofollow_windows.go) keeps the simpler
// Lstat-then-OpenFile guard because Windows's
// SeCreateSymbolicLinkPrivilege already raises the bar for the
// equivalent attack and Windows has no FIFO concept.
func openSidecarFile(path string) (*os.File, error) {
// Note: NO O_TRUNC here — we truncate after the link-count check.
const flag = os.O_WRONLY | os.O_CREATE | syscall.O_NOFOLLOW | syscall.O_NONBLOCK
f, err := os.OpenFile(path, flag, 0o600) //nolint:gosec,mnd // path is composed from output-root + fixed file name; 0600 is the standard owner-only mode
if err != nil {
if errors.Is(err, syscall.ELOOP) {
return nil, cockroachdberr.WithStack(cockroachdberr.Wrapf(err,
"backup: refusing to overwrite symlink at %s", path))
}
// ENXIO surfaces when the path is a FIFO with no reader;
// because O_NONBLOCK turned the would-be hang into an
// immediate error, surface it with a stable message
// rather than letting the bare syscall errno leak out.
if errors.Is(err, syscall.ENXIO) {
return nil, cockroachdberr.WithStack(cockroachdberr.Wrapf(err,
"backup: refusing to write to FIFO at %s", path))
}
return nil, cockroachdberr.WithStack(err)
}
info, err := f.Stat()
if err != nil {
_ = f.Close()
return nil, cockroachdberr.WithStack(err)
}
// Refuse non-regular files. A reader-attached FIFO (where the
// O_NONBLOCK open succeeded), a socket, or a character/block
// device would all otherwise be silently written into and
// `f.Truncate(0)` would be a no-op or fail in a confusing way.
// Codex P2 round 11.
if !info.Mode().IsRegular() {
_ = f.Close()
return nil, cockroachdberr.WithStack(cockroachdberr.Newf(
"backup: refusing to write to non-regular file at %s (mode=%s)", path, info.Mode()))
}
if sysStat, ok := info.Sys().(*syscall.Stat_t); ok && sysStat.Nlink > 1 {
_ = f.Close()
return nil, cockroachdberr.WithStack(cockroachdberr.Newf(
"backup: refusing to overwrite hard-linked file at %s (nlink=%d)", path, sysStat.Nlink))
}
if err := f.Truncate(0); err != nil {
_ = f.Close()
return nil, cockroachdberr.WithStack(err)
}
return f, nil
}
30 changes: 30 additions & 0 deletions internal/backup/open_nofollow_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//go:build windows

package backup

import (
"os"

cockroachdberr "github.com/cockroachdb/errors"
)

// openSidecarFile is the Windows counterpart to the unix
// open_nofollow_unix.go variant. syscall.O_NOFOLLOW is not defined on
// Windows and the platform's symlink/permission model is materially
// different (junction points, ACLs, SeCreateSymbolicLinkPrivilege),
// so we keep the simpler Lstat-then-OpenFile guard. The remaining
// TOCTOU window is acceptable here because mounting a successful
// attack on the dump tree on Windows already requires the attacker to
// hold write access to the output directory plus the symlink-create
// privilege, which is a much higher bar than the unix case.
func openSidecarFile(path string) (*os.File, error) {
if info, err := os.Lstat(path); err == nil && info.Mode()&os.ModeSymlink != 0 {
return nil, cockroachdberr.WithStack(cockroachdberr.Newf(
"backup: refusing to overwrite symlink at %s", path))
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) //nolint:gosec,mnd // path is composed from output-root + fixed file name; 0600 is the standard owner-only mode
if err != nil {
return nil, cockroachdberr.WithStack(err)
}
return f, nil
}
Loading
Loading