Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f235d0af2 |
@@ -23,18 +23,18 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of
|
|||||||
|
|
||||||
- 2026-10-18: `netwatch-server prepare-data-dir` no longer changes the owner of
|
- 2026-10-18: `netwatch-server prepare-data-dir` no longer changes the owner of
|
||||||
a file outside `/data` through a hard link in the volume
|
a file outside `/data` through a hard link in the volume
|
||||||
([#143](https://git.eeqj.de/sneak/netwatch/issues/143)). A regular file or
|
([#143](https://git.eeqj.de/sneak/netwatch/issues/143)). A file with a second
|
||||||
symbolic link with a second name (hard link) keeps its owner, and the start
|
name (hard link) keeps its owner, and the start logs its path. Owners are now
|
||||||
logs its path. Owners are now set on the opened directory, file or link, never
|
set on the opened directory, file or link, never by name, so a hard link
|
||||||
by name, so a hard link swapped in after the check is caught too; a symbolic
|
swapped in after the check is caught too; a symbolic link is opened without
|
||||||
link is opened without following it and given to the netwatch user itself, so
|
following it and given to the netwatch user itself, so the server can still
|
||||||
the server can still follow one in a sticky directory that anyone can write
|
follow one in a sticky directory that anyone can write to. Devices, named
|
||||||
to. Other files that are neither directories, regular files nor symbolic links
|
pipes and sockets keep their owner. Opening a file without following it and
|
||||||
keep their owner. Setting the owner on an open file or link needs Linux, the
|
setting the owner on the open file needs Linux, the system of the image; on
|
||||||
system of the image; on any other system the package still builds, and
|
any other system the package still builds, and `prepare-data-dir` fails at the
|
||||||
`prepare-data-dir` fails at the first regular file or symbolic link. The test
|
first file that is not a directory. The test that runs as root hard-links a
|
||||||
that runs as root hard-links a file from outside the volume into it and finds
|
file, a symbolic link and a named pipe from outside the volume into it and
|
||||||
its owner unchanged
|
finds their owners unchanged
|
||||||
- 2026-10-18: after an interval change, the summary's Reachable slot reads
|
- 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
|
`--/--` 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
|
ends ([#140](https://git.eeqj.de/sneak/netwatch/issues/140)). It used to read
|
||||||
|
|||||||
+6
-5
@@ -67,11 +67,12 @@ done > /etc/nginx/trusted-proxies.conf
|
|||||||
# volume, which may be a host directory owned by root or by another
|
# volume, which may be a host directory owned by root or by another
|
||||||
# uid. Here, as root, netwatch-server prepare-data-dir creates DATA_DIR
|
# uid. Here, as root, netwatch-server prepare-data-dir creates DATA_DIR
|
||||||
# and gives /data and the directories, regular files and symbolic links
|
# and gives /data and the directories, regular files and symbolic links
|
||||||
# in it to the netwatch user, so the host directory needs no preparing,
|
# in it to the netwatch user, so the host directory needs no preparing.
|
||||||
# except a file with a second name (hard link), which keeps its owner,
|
# A file with a second name (hard link) keeps its owner, as that name
|
||||||
# as that name could be outside /data. It stops the start, naming
|
# could be outside /data, and its path is logged. It stops the start,
|
||||||
# DATA_DIR, unless DATA_DIR is /data or a path below it, and it acts on
|
# naming DATA_DIR, unless DATA_DIR is /data or a path below it, and it
|
||||||
# nothing outside /data, whatever symbolic or hard links it meets there.
|
# acts on nothing outside /data, whatever symbolic or hard links it
|
||||||
|
# meets there.
|
||||||
export DATA_DIR="${DATA_DIR:-/data/reports}"
|
export DATA_DIR="${DATA_DIR:-/data/reports}"
|
||||||
netwatch-server prepare-data-dir "$DATA_DIR" || exit 1
|
netwatch-server prepare-data-dir "$DATA_DIR" || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ var ErrDataDirOutsideVolume = errors.New(
|
|||||||
// /data in the image, needs no preparing: it creates dir, gives volume
|
// /data in the image, needs no preparing: it creates dir, gives volume
|
||||||
// and the directories, regular files and symbolic links in it to
|
// and the directories, regular files and symbolic links in it to
|
||||||
// owner, and gives volume and dir the mode the server gives a directory
|
// owner, and gives volume and dir the mode the server gives a directory
|
||||||
// it creates. It leaves as they are a regular file or symbolic link
|
// it creates. It leaves a file with a second name (hard link) as it
|
||||||
// with a second name (hard link), as that name could be outside volume,
|
// is, as that name could be outside volume, and logs its path; it
|
||||||
// and any other file, such as a device (see chownAll). dir must be
|
// leaves a device, named pipe or socket as it is too (see chownFile).
|
||||||
// volume or a path below it, with no '.', '..', empty part or '/' at
|
// dir must be volume or a path below it, with no '.', '..', empty part
|
||||||
// the end. It gives a regular file or symbolic link to owner only on
|
// or '/' at the end. On any system but Linux, the system of the image, it
|
||||||
// Linux, the system of the image; on any other it fails at the first
|
// fails at the first file in volume that is not a directory (see
|
||||||
// one (see chownFile).
|
// chownFile).
|
||||||
//
|
//
|
||||||
// bin/entrypoint.sh runs this as root, which would follow a symbolic
|
// bin/entrypoint.sh runs this as root, which would follow a symbolic
|
||||||
// link anywhere, so every step goes through an os.Root opened on
|
// link anywhere, so every step goes through an os.Root opened on
|
||||||
@@ -85,21 +85,20 @@ func PrepareDataDir(volume, dir string, owner *user.User) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// chownAll gives name, a directory inside root, and the directories,
|
// chownAll gives name, a directory inside root, and the directories,
|
||||||
// regular files and symbolic links in it to uid and gid. It opens each
|
// regular files and symbolic links in it to uid and gid, but not the
|
||||||
// through root and gives the open file to them, as chmodDir sets a
|
// files chownFile leaves. It opens each through root and gives the
|
||||||
// mode on the open directory. Root.Lchown would not hold: it acts on
|
// open file to them, as chmodDir sets a mode on the open directory.
|
||||||
// whatever is at the name by then, and a hard link swapped in there
|
// Root.Lchown would not hold: it acts on whatever is at the name by
|
||||||
// shares its owner with a file that could be outside root. A symbolic
|
// then, and a hard link swapped in there shares its owner with a file
|
||||||
// link is given to them itself, never what it points to, because in a
|
// that could be outside root. A symbolic link is given to them itself,
|
||||||
// directory that anyone can write to and that has the sticky bit set,
|
// never what it points to, because in a directory that anyone can
|
||||||
// as /tmp has, the kernel follows a link only for the link's owner or
|
// write to and that has the sticky bit set, as /tmp has, the kernel
|
||||||
// when the link and the directory have the same owner. chownAll goes
|
// follows a link only for the link's owner or when the link and the
|
||||||
// into an entry only when the read found a directory there, so it
|
// directory have the same owner. chownAll goes into an entry only when
|
||||||
// follows no link it finds; one swapped in afterwards for a directory
|
// the read found a directory there, so it follows no link it finds;
|
||||||
// is followed only as the os.Root allows, and one swapped in for a
|
// one swapped in afterwards for a directory is followed only as the
|
||||||
// regular file or symbolic link is not followed at all (see
|
// os.Root allows, and one swapped in for any other file is not
|
||||||
// chownFile). It leaves any other file, such as a device: the server
|
// followed at all (see chownFile).
|
||||||
// needs to own none of them.
|
|
||||||
func chownAll(root *os.Root, name string, uid, gid int) error {
|
func chownAll(root *os.Root, name string, uid, gid int) error {
|
||||||
entries, err := chownDir(root, name, uid, gid)
|
entries, err := chownDir(root, name, uid, gid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -108,11 +107,9 @@ func chownAll(root *os.Root, name string, uid, gid int) error {
|
|||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
entryName := filepath.Join(name, entry.Name())
|
entryName := filepath.Join(name, entry.Name())
|
||||||
|
if entry.IsDir() {
|
||||||
switch {
|
|
||||||
case entry.IsDir():
|
|
||||||
err = chownAll(root, entryName, uid, gid)
|
err = chownAll(root, entryName, uid, gid)
|
||||||
case entry.Type().IsRegular(), entry.Type() == fs.ModeSymlink:
|
} else {
|
||||||
err = chownFile(root, entryName, uid, gid)
|
err = chownFile(root, entryName, uid, gid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package reportbuf
|
package reportbuf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -10,14 +11,18 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// chownFile gives name, a regular file or symbolic link inside root, to
|
// chownFile gives name, a file inside root that is not a directory, to
|
||||||
// uid and gid, unless it has a second name (hard link): all the names
|
// uid and gid if it is a regular file or symbolic link with one name.
|
||||||
// of a file share its owner, and the other could be outside root. It
|
// It leaves, and logs, a file of any kind with a second name (hard
|
||||||
// opens name through root with O_PATH, which reads nothing from the
|
// link): all the names of a file share its owner, and the other could
|
||||||
// file; os.Root adds O_NOFOLLOW, and the two together open a symbolic
|
// be outside root. It leaves any other file with one name, such as a
|
||||||
// link itself, never what it points to. It reads the number of names
|
// device, named pipe or socket: the server needs to own none of them.
|
||||||
// from the open file and gives that file to them, so a hard link
|
// It opens name through root with O_PATH, which reads nothing from the
|
||||||
// swapped in at name is caught too. It logs a file it leaves.
|
// file, so opening a device or named pipe neither waits nor acts on
|
||||||
|
// it; os.Root adds O_NOFOLLOW, and the two together open a symbolic
|
||||||
|
// link itself, never what it points to. It reads the kind of file and
|
||||||
|
// its number of names from the open file and gives that file to them,
|
||||||
|
// so a hard link swapped in at name is caught too.
|
||||||
//
|
//
|
||||||
// This file is built only on Linux, because O_PATH and AT_EMPTY_PATH
|
// 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
|
// exist only there; prepare-data-dir runs only in the Linux image. On
|
||||||
@@ -35,16 +40,25 @@ func chownFile(root *os.Root, name string, uid, gid int) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||||
|
if !ok || stat.Nlink > 1 {
|
||||||
|
slog.Warn("owner not changed: file has a second name (hard link)",
|
||||||
|
"path", file.Name())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.Mode().IsRegular() && info.Mode().Type() != fs.ModeSymlink {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// File.Chown would fail: fchown refuses a file opened with O_PATH.
|
// 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
|
// Fchownat with an empty name and AT_EMPTY_PATH acts on the open
|
||||||
// file instead.
|
// file instead.
|
||||||
stat, ok := info.Sys().(*syscall.Stat_t)
|
err = unix.Fchownat(int(file.Fd()), "", uid, gid, unix.AT_EMPTY_PATH)
|
||||||
if ok && stat.Nlink == 1 {
|
if err != nil {
|
||||||
return unix.Fchownat(int(file.Fd()), "", uid, gid, unix.AT_EMPTY_PATH)
|
return &fs.PathError{Op: "chown", Path: file.Name(), Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Warn("owner not changed: file has a second name (hard link)",
|
|
||||||
"path", file.Name())
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// chownFile returns an error on every system but Linux. Giving a
|
// chownFile returns an error on every system but Linux. Opening a file
|
||||||
// regular file or symbolic link to a user on the open file, never by
|
// that is not a directory without reading it or following it, and
|
||||||
// name, needs O_PATH and AT_EMPTY_PATH, which only Linux has (see
|
// giving it to a user on the open file, never by name, needs O_PATH
|
||||||
// datadir_linux.go), and prepare-data-dir runs only in the Linux
|
// and AT_EMPTY_PATH, which only Linux has (see datadir_linux.go), and
|
||||||
// image. This file lets the package build elsewhere, such as on macOS,
|
// prepare-data-dir runs only in the Linux image. This file lets the
|
||||||
// where a developer runs make build and make run.
|
// package build elsewhere, such as on macOS, where a developer runs
|
||||||
|
// make build and make run.
|
||||||
func chownFile(_ *os.Root, name string, _, _ int) error {
|
func chownFile(_ *os.Root, name string, _, _ int) error {
|
||||||
return &fs.PathError{Op: "chown", Path: name, Err: errors.ErrUnsupported}
|
return &fs.PathError{Op: "chown", Path: name, Err: errors.ErrUnsupported}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,8 +258,9 @@ func TestPrepareDataDirTakesDirectoryNamedInLatin1(t *testing.T) {
|
|||||||
// files and symbolic links in the volume to a uid and gid that own
|
// files and symbolic links in the volume to a uid and gid that own
|
||||||
// nothing, which only root can do. A symbolic link in the volume to a
|
// nothing, which only root can do. A symbolic link in the volume to a
|
||||||
// directory outside it is given to them itself; what it points to is
|
// directory outside it is given to them itself; what it points to is
|
||||||
// left as it was. A hard link to a file outside the volume, and one to
|
// left as it was. A hard link to a file outside the volume, one to a
|
||||||
// a symbolic link outside it, are left as they were.
|
// symbolic link outside it and one to a named pipe outside it are left
|
||||||
|
// as they were, and so is a named pipe in the volume.
|
||||||
func TestPrepareDataDirGivesVolumeToOwner(t *testing.T) {
|
func TestPrepareDataDirGivesVolumeToOwner(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -309,6 +310,22 @@ func TestPrepareDataDirGivesVolumeToOwner(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = syscall.Mkfifo(filepath.Join(outside, "fifo"), 0o600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Link(filepath.Join(outside, "fifo"),
|
||||||
|
filepath.Join(old, "hardlink-to-fifo"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = syscall.Mkfifo(filepath.Join(old, "fifo"), 0o600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
err = reportbuf.PrepareDataDir(volume, filepath.Join(volume, reports),
|
err = reportbuf.PrepareDataDir(volume, filepath.Join(volume, reports),
|
||||||
&user.User{Uid: "4242", Gid: "4343"})
|
&user.User{Uid: "4242", Gid: "4343"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -324,6 +341,7 @@ func TestPrepareDataDirGivesVolumeToOwner(t *testing.T) {
|
|||||||
|
|
||||||
for _, path := range []string{
|
for _, path := range []string{
|
||||||
outside, filepath.Join(outside, "f"), filepath.Join(outside, "link"),
|
outside, filepath.Join(outside, "f"), filepath.Join(outside, "link"),
|
||||||
|
filepath.Join(outside, "fifo"), filepath.Join(old, "fifo"),
|
||||||
} {
|
} {
|
||||||
requireOwner(t, path, 0, 0)
|
requireOwner(t, path, 0, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user