1 Commits
Author SHA1 Message Date
sneak 54e3e83888 prepare-data-dir leaves files with a second name to their owner (closes #143)
check / check (push) Canceled after 0s
A hard link in /data shares its owner with a file that can be anywhere
on the same filesystem, so chowning it gave that file to the netwatch
user. A regular file or symbolic link with more than one name now
keeps its owner and its path is logged.

Owners are now set on the opened directory, file or link after
checking it, as chmodDir sets modes, so a hard link swapped in
afterwards is caught. A symbolic link is opened without following it
and given to the netwatch user itself. Setting the owner on an open
file or link needs Linux, the image's system; elsewhere it returns an
error. Other files, such as devices, keep their owner.

Model: opus-5-5
2026-10-18 21:48:41 +00:00
4 changed files with 77 additions and 41 deletions
+5 -2
View File
@@ -30,8 +30,11 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of
link is opened without following it and given to the netwatch user itself, so
the server can still follow one in a sticky directory that anyone can write
to. Other files that are neither directories, regular files nor symbolic links
keep their owner. The test that runs as root hard-links a file from outside
the volume into it and finds its owner unchanged
keep their owner. Setting the owner on an open file or link needs Linux, the
system of the image; on any other system the package still builds, and
`prepare-data-dir` fails at the first regular file or symbolic link. The test
that runs as root hard-links a file from outside the volume into it and finds
its owner unchanged
- 2026-10-18: after an interval change, the summary's Reachable slot reads
`--/--` in white, as at page load, until the second round since the change
ends ([#140](https://git.eeqj.de/sneak/netwatch/issues/140)). It used to read
+3 -39
View File
@@ -3,14 +3,11 @@ package reportbuf
import (
"errors"
"io/fs"
"log/slog"
"os"
"os/user"
"path/filepath"
"strconv"
"syscall"
"golang.org/x/sys/unix"
)
// ErrDataDirOutsideVolume is returned by PrepareDataDir for a DATA_DIR
@@ -27,7 +24,9 @@ var ErrDataDirOutsideVolume = errors.New(
// with a second name (hard link), as that name could be outside volume,
// and any other file, such as a device (see chownAll). dir must be
// volume or a path below it, with no '.', '..', empty part or '/' at
// the end.
// the end. It gives a regular file or symbolic link to owner only on
// Linux, the system of the image; on any other it fails at the first
// one (see chownFile).
//
// bin/entrypoint.sh runs this as root, which would follow a symbolic
// link anywhere, so every step goes through an os.Root opened on
@@ -155,41 +154,6 @@ func chownDir(root *os.Root, name string, uid, gid int) ([]fs.DirEntry, error) {
return dir.ReadDir(-1)
}
// chownFile gives name, a regular file or symbolic link inside root, to
// uid and gid, unless it has a second name (hard link): all the names
// of a file share its owner, and the other could be outside root. It
// opens name through root with O_PATH, which reads nothing from the
// file; os.Root adds O_NOFOLLOW, and the two together open a symbolic
// link itself, never what it points to. It reads the number of names
// from the open file and gives that file to them, so a hard link
// swapped in at name is caught too. It logs a file it leaves.
func chownFile(root *os.Root, name string, uid, gid int) error {
file, err := root.OpenFile(name, unix.O_PATH, 0)
if err != nil {
return err
}
defer func() { _ = file.Close() }()
info, err := file.Stat()
if err != nil {
return err
}
// File.Chown would fail: fchown refuses a file opened with O_PATH.
// Fchownat with an empty name and AT_EMPTY_PATH acts on the open
// file instead.
stat, ok := info.Sys().(*syscall.Stat_t)
if ok && stat.Nlink == 1 {
return unix.Fchownat(int(file.Fd()), "", uid, gid, unix.AT_EMPTY_PATH)
}
slog.Warn("owner not changed: file has a second name (hard link)",
"path", file.Name())
return nil
}
// chmodDir gives name, a directory inside root, the mode the server
// gives a directory it creates. Root.Chmod would not hold: on Linux it
// checks that name is not a symbolic link, then sets the mode by name,
+50
View File
@@ -0,0 +1,50 @@
//go:build linux
package reportbuf
import (
"log/slog"
"os"
"syscall"
"golang.org/x/sys/unix"
)
// chownFile gives name, a regular file or symbolic link inside root, to
// uid and gid, unless it has a second name (hard link): all the names
// of a file share its owner, and the other could be outside root. It
// opens name through root with O_PATH, which reads nothing from the
// file; os.Root adds O_NOFOLLOW, and the two together open a symbolic
// link itself, never what it points to. It reads the number of names
// from the open file and gives that file to them, so a hard link
// swapped in at name is caught too. It logs a file it leaves.
//
// This file is built only on Linux, because O_PATH and AT_EMPTY_PATH
// exist only there; prepare-data-dir runs only in the Linux image. On
// any other system chownFile returns an error (see datadir_other.go).
func chownFile(root *os.Root, name string, uid, gid int) error {
file, err := root.OpenFile(name, unix.O_PATH, 0)
if err != nil {
return err
}
defer func() { _ = file.Close() }()
info, err := file.Stat()
if err != nil {
return err
}
// File.Chown would fail: fchown refuses a file opened with O_PATH.
// Fchownat with an empty name and AT_EMPTY_PATH acts on the open
// file instead.
stat, ok := info.Sys().(*syscall.Stat_t)
if ok && stat.Nlink == 1 {
return unix.Fchownat(int(file.Fd()), "", uid, gid, unix.AT_EMPTY_PATH)
}
slog.Warn("owner not changed: file has a second name (hard link)",
"path", file.Name())
return nil
}
+19
View File
@@ -0,0 +1,19 @@
//go:build !linux
package reportbuf
import (
"errors"
"io/fs"
"os"
)
// chownFile returns an error on every system but Linux. Giving a
// regular file or symbolic link to a user on the open file, never by
// name, needs O_PATH and AT_EMPTY_PATH, which only Linux has (see
// datadir_linux.go), and prepare-data-dir runs only in the Linux
// image. This file lets the package build elsewhere, such as on macOS,
// where a developer runs make build and make run.
func chownFile(_ *os.Root, name string, _, _ int) error {
return &fs.PathError{Op: "chown", Path: name, Err: errors.ErrUnsupported}
}