Skip to content
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0e15f02
s3keys: export ParseBlobKey for offline blob-key consumers
bootjp Apr 30, 2026
554dd42
backup: S3 encoder for buckets, objects, and blob reassembly (Phase 0a)
bootjp Apr 30, 2026
b3f2842
Merge remote-tracking branch 'origin/feat/backup-phase0a-dynamodb' in…
bootjp Apr 30, 2026
19ae328
backup: address review on S3 encoder (PR #718)
bootjp Apr 30, 2026
33bff13
Merge remote-tracking branch 'origin/feat/backup-phase0a-dynamodb' in…
bootjp Apr 30, 2026
92ee22e
backup: address codex review on S3 encoder (PR #718, round 2)
bootjp Apr 30, 2026
2094e3e
Merge remote-tracking branch 'origin/feat/backup-phase0a-dynamodb' in…
bootjp Apr 30, 2026
2c44292
backup: handle file-vs-directory S3 key collisions (PR #718, round 3)
bootjp Apr 30, 2026
844fd49
Merge remote-tracking branch 'origin/feat/backup-phase0a-keymap-manif…
bootjp Apr 30, 2026
ba33df8
backup: validate chunk completeness + reject empty slash segments (PR…
bootjp Apr 30, 2026
2f87b84
Merge remote-tracking branch 'origin/feat/backup-phase0a-keymap-manif…
bootjp Apr 30, 2026
2febd42
backup: reject leading-slash S3 object keys (PR #718, round 5)
bootjp Apr 30, 2026
09c2a0e
Merge remote-tracking branch 'origin/feat/backup-phase0a-redis-simple…
bootjp Apr 30, 2026
a4fce85
backup: reject backslashes in S3 object keys (PR #718, round 6)
bootjp Apr 30, 2026
00819af
Merge remote-tracking branch 'origin/feat/backup-phase0a-keymap-manif…
bootjp Apr 30, 2026
6dd4575
Merge remote-tracking branch 'origin/feat/backup-phase0a-redis-simple…
bootjp Apr 30, 2026
1fa9345
Merge remote-tracking branch 'origin/feat/backup-phase0a-redis-simple…
bootjp Apr 30, 2026
2a154af
Merge remote-tracking branch 'origin/feat/backup-phase0a-keymap-manif…
bootjp Apr 30, 2026
0f390b8
Merge remote-tracking branch 'origin/feat/backup-phase0a-redis-simple…
bootjp Apr 30, 2026
19d33a6
Merge remote-tracking branch 'origin/feat/backup-phase0a-sqs' into fe…
bootjp Apr 30, 2026
7a40ae8
Merge remote-tracking branch 'origin/feat/backup-phase0a-keymap-manif…
bootjp Apr 30, 2026
ab38eb0
Merge remote-tracking branch 'origin/feat/backup-phase0a-redis-simple…
bootjp Apr 30, 2026
46cb56f
backup: close S3 KEYMAP fd + use openSidecarFile (PR #718, round 7)
bootjp Apr 30, 2026
b196bf7
Merge remote-tracking branch 'origin/feat/backup-phase0a-redis-simple…
bootjp Apr 30, 2026
1dc6884
Merge remote-tracking branch 'origin/feat/backup-phase0a-sqs' into fe…
bootjp Apr 30, 2026
402f6e5
Merge remote-tracking branch 'origin/feat/backup-phase0a-sqs' into fe…
bootjp Apr 30, 2026
b65c06b
Merge remote-tracking branch 'origin/feat/backup-phase0a-dynamodb' in…
bootjp Apr 30, 2026
90d33fe
backup: rename-target collision check + populate last_modified (PR #7…
bootjp Apr 30, 2026
6395937
Merge remote-tracking branch 'origin/feat/backup-phase0a-dynamodb' in…
bootjp Apr 30, 2026
9a63e32
Merge remote-tracking branch 'origin/feat/backup-phase0a-sqs' into fe…
bootjp Apr 30, 2026
d016ea7
backup: refuse dot-segment scratch paths in HandleBlob (PR #718, roun…
bootjp Apr 30, 2026
8881bf3
Merge remote-tracking branch 'origin/feat/backup-phase0a-redis-simple…
bootjp Apr 30, 2026
e91f086
Merge remote-tracking branch 'origin/feat/backup-phase0a-sqs' into fe…
bootjp Apr 30, 2026
4505df3
backup: set-based chunk completeness check (PR #718, round 10)
bootjp Apr 30, 2026
0e6a140
Merge remote-tracking branch 'origin/feat/backup-phase0a-dynamodb' in…
bootjp Apr 30, 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
}
25 changes: 25 additions & 0 deletions internal/backup/open_nofollow_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//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))
}
return 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
}
45 changes: 36 additions & 9 deletions internal/backup/open_nofollow_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,67 @@ import (
cockroachdberr "github.com/cockroachdb/errors"
)

// openSidecarFile opens path for write while refusing both symlink and
// hard-link clobber attacks.
// 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. 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.
// 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.
// 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
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(
Expand Down
48 changes: 48 additions & 0 deletions internal/backup/redis_string_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//go:build unix

package backup

import (
"os"
"path/filepath"
"strings"
"syscall"
"testing"
)

// TestRedisDB_OpenJSONLRefusesFIFO is the regression for Codex P2
// round 11: a pre-existing FIFO at strings_ttl.jsonl / hll_ttl.jsonl
// would block the first TTL write indefinitely (POSIX: opening a
// reader-less FIFO with O_WRONLY blocks until a reader attaches).
// O_NONBLOCK turns that into an immediate ENXIO; the post-open
// Stat() check then refuses any non-regular file (FIFO with reader,
// socket, device). The symlink and hard-link guards alone do not
// catch this — mkfifo produces nlink=1 and is not a symlink.
//
// Lives in a unix-only test file because syscall.Mkfifo is undefined
// on Windows and js/wasm.
func TestRedisDB_OpenJSONLRefusesFIFO(t *testing.T) {
t.Parallel()
db, root := newRedisDB(t)
dir := filepath.Join(root, "redis", "db_0")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
fifoPath := filepath.Join(dir, redisStringsTTLFile)
if err := syscall.Mkfifo(fifoPath, 0o600); err != nil {
t.Skipf("mkfifo not supported on this platform: %v", err)
}
err := db.HandleString([]byte("k"), encodeNewStringValue(t, []byte("v"), fixedExpireMs))
if err == nil {
t.Fatalf("expected refusal of FIFO sidecar, got nil")
}
// Either ENXIO ("FIFO at <path>") on platforms that surface it
// at open, or "non-regular file" if a (rare) reader is around
// to make the open succeed. Both are acceptable as long as the
// open does not hang and the encoder refuses to truncate the
// pipe target.
msg := err.Error()
if !strings.Contains(msg, "FIFO at") && !strings.Contains(msg, "non-regular file") {
t.Fatalf("expected FIFO refusal message, got %v", err)
}
}
Loading
Loading