@@ -10,14 +10,27 @@ import (
1010 cockroachdberr "github.com/cockroachdb/errors"
1111)
1212
13- // openSidecarFile opens path for write with truncate+create semantics
14- // and atomically refuses symlinks. The unix build leans on
15- // syscall.O_NOFOLLOW so the kernel returns ELOOP if path is a
16- // symlink, eliminating the TOCTOU race a separate Lstat-then-Create
17- // pattern would have. The Windows build (open_nofollow_windows.go)
18- // implements this with the best alternative available there.
13+ // openSidecarFile opens path for write while refusing both symlink and
14+ // hard-link clobber attacks.
15+ //
16+ // - O_NOFOLLOW makes the kernel return ELOOP atomically if the path
17+ // is a symbolic link — closing the TOCTOU race a separate
18+ // Lstat-then-Create pattern would have.
19+ // - To also refuse hard links to files outside the dump tree, we
20+ // open WITHOUT O_TRUNC, fstat() the descriptor to check the
21+ // link count, and only call Truncate(0) if Nlink == 1. An
22+ // adversary that pre-created strings_ttl.jsonl as a hard link
23+ // to /etc/passwd (or any other writable file outside the dump
24+ // tree) would otherwise see the inode truncated on
25+ // openSidecarFile despite the symlink guard. Codex P2 round 9.
26+ //
27+ // The Windows build (open_nofollow_windows.go) keeps the simpler
28+ // Lstat-then-OpenFile guard because Windows's
29+ // SeCreateSymbolicLinkPrivilege already raises the bar for the
30+ // equivalent attack.
1931func openSidecarFile (path string ) (* os.File , error ) {
20- const flag = os .O_WRONLY | os .O_CREATE | os .O_TRUNC | syscall .O_NOFOLLOW
32+ // Note: NO O_TRUNC here — we truncate after the link-count check.
33+ const flag = os .O_WRONLY | os .O_CREATE | syscall .O_NOFOLLOW
2134 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
2235 if err != nil {
2336 if errors .Is (err , syscall .ELOOP ) {
@@ -26,5 +39,19 @@ func openSidecarFile(path string) (*os.File, error) {
2639 }
2740 return nil , cockroachdberr .WithStack (err )
2841 }
42+ info , err := f .Stat ()
43+ if err != nil {
44+ _ = f .Close ()
45+ return nil , cockroachdberr .WithStack (err )
46+ }
47+ if sysStat , ok := info .Sys ().(* syscall.Stat_t ); ok && sysStat .Nlink > 1 {
48+ _ = f .Close ()
49+ return nil , cockroachdberr .WithStack (cockroachdberr .Newf (
50+ "backup: refusing to overwrite hard-linked file at %s (nlink=%d)" , path , sysStat .Nlink ))
51+ }
52+ if err := f .Truncate (0 ); err != nil {
53+ _ = f .Close ()
54+ return nil , cockroachdberr .WithStack (err )
55+ }
2956 return f , nil
3057}
0 commit comments