prepare-data-dir leaves files with a second name to their owner (closes #143) #165

Merged
clawbot merged 1 commits from issue-143-hardlink-chown into next 2026-10-19 01:11:42 +02:00
Collaborator

Fixes #143.

netwatch-server prepare-data-dir, run as root at every start, gave every entry under /data to the netwatch user by name. A hard link shares its owner with its other names, anywhere on the same filesystem, so that file changed owner too.

What changed, in internal/reportbuf:

  • A file with a second name (hard link) keeps its owner, and its path is logged.
  • Owners are set on the open directory or file, after checking it, as chmodDir sets modes, so a hard link swapped in after the check is caught.
  • Files that are not directories are opened with O_PATH, so a named pipe opens without waiting and the owner is set on the open link; os.Root adds O_NOFOLLOW, so a symbolic link itself is opened, not followed. A symbolic link goes to the netwatch user: in a sticky directory anyone can write to, the server follows only a link it or the directory's owner owns.
  • That code is in datadir_linux.go; elsewhere datadir_other.go returns an error, so the package builds on macOS.

Not shown by the diff:

  • Judgement call: devices, named pipes and sockets keep their owner; the server needs none.
  • golang.org/x/sys/unix becomes a direct dependency, for O_PATH and AT_EMPTY_PATH, which syscall lacks.
  • The warning goes to the default slog logger; the server has no logger of its own yet.
  • The test run as root hard-links a file, a symbolic link and a named pipe from outside the volume into it; all keep their owner.
  • Untested: a failed owner change; the tests run as root, which can give any file away.

Model: opus-5-5

Fixes https://git.eeqj.de/sneak/netwatch/issues/143. `netwatch-server prepare-data-dir`, run as root at every start, gave every entry under `/data` to the netwatch user by name. A hard link shares its owner with its other names, anywhere on the same filesystem, so that file changed owner too. What changed, in `internal/reportbuf`: - A file with a second name (hard link) keeps its owner, and its path is logged. - Owners are set on the open directory or file, after checking it, as `chmodDir` sets modes, so a hard link swapped in after the check is caught. - Files that are not directories are opened with `O_PATH`, so a named pipe opens without waiting and the owner is set on the open link; `os.Root` adds `O_NOFOLLOW`, so a symbolic link itself is opened, not followed. A symbolic link goes to the netwatch user: in a sticky directory anyone can write to, the server follows only a link it or the directory's owner owns. - That code is in `datadir_linux.go`; elsewhere `datadir_other.go` returns an error, so the package builds on macOS. Not shown by the diff: - Judgement call: devices, named pipes and sockets keep their owner; the server needs none. - `golang.org/x/sys/unix` becomes a direct dependency, for `O_PATH` and `AT_EMPTY_PATH`, which `syscall` lacks. - The warning goes to the default `slog` logger; the server has no logger of its own yet. - The test run as root hard-links a file, a symbolic link and a named pipe from outside the volume into it; all keep their owner. - Untested: a failed owner change; the tests run as root, which can give any file away. Model: opus-5-5
clawbot added the needs-review label 2026-10-18 22:42:46 +02:00
clawbot self-assigned this 2026-10-18 22:42:50 +02:00
Author
Collaborator
  1. internal/reportbuf/datadir.go lines 94-112: chownAll now leaves every symbolic link with its old owner, because "the server needs to own none of them". That is not true of a link the server has to follow inside a directory that is sticky and writable by everyone (mode 1777, like /tmp). The kernel follows such a link only for the link's owner, or when the link and the directory have the same owner, and prepare-data-dir gives the directory to netwatch but not the link. Take /data/a at mode 1777, /data/a/reports a link to ../store owned by another uid, and DATA_DIR=/data/a/reports: the server now stops at start, where before this change it started. That breaks the rule that the container makes its own data directory usable before the app starts. Acceptable: that layout starts again, still with no owner set by name (for example, a symbolic link with one name is given to netwatch on the link itself, opened without following it), and the doc comment matches.

  2. The branch does not rebase onto the current next: TODO.md conflicts with the entry for #140 at the top of Completed Steps. Acceptable: rebased onto the current next with both entries kept.

  3. bin/entrypoint.sh lines 69-70: the comment says prepare-data-dir gives "the directories and regular files" in /data to the netwatch user, but a regular file with a second name (hard link) keeps its owner. Acceptable: the sentence names that exception.

  4. internal/reportbuf/reportbuf.go lines 1-3: the package comment covers buffering reports and writing and deleting report files. It does not cover getting the data directory ready, which is what the new chownDir and chownFile do. Acceptable: the package comment also covers the work of PrepareDataDir.

  5. The PR body is about 300 words, over the limit of about 250. Acceptable: about 250 words or fewer.

Model: opus-5-5

1. `internal/reportbuf/datadir.go` lines 94-112: `chownAll` now leaves every symbolic link with its old owner, because "the server needs to own none of them". That is not true of a link the server has to follow inside a directory that is sticky and writable by everyone (mode 1777, like `/tmp`). The kernel follows such a link only for the link's owner, or when the link and the directory have the same owner, and `prepare-data-dir` gives the directory to `netwatch` but not the link. Take `/data/a` at mode 1777, `/data/a/reports` a link to `../store` owned by another uid, and `DATA_DIR=/data/a/reports`: the server now stops at start, where before this change it started. That breaks the rule that the container makes its own data directory usable before the app starts. Acceptable: that layout starts again, still with no owner set by name (for example, a symbolic link with one name is given to `netwatch` on the link itself, opened without following it), and the doc comment matches. 2. The branch does not rebase onto the current `next`: `TODO.md` conflicts with the entry for https://git.eeqj.de/sneak/netwatch/issues/140 at the top of Completed Steps. Acceptable: rebased onto the current `next` with both entries kept. 3. `bin/entrypoint.sh` lines 69-70: the comment says `prepare-data-dir` gives "the directories and regular files" in `/data` to the netwatch user, but a regular file with a second name (hard link) keeps its owner. Acceptable: the sentence names that exception. 4. `internal/reportbuf/reportbuf.go` lines 1-3: the package comment covers buffering reports and writing and deleting report files. It does not cover getting the data directory ready, which is what the new `chownDir` and `chownFile` do. Acceptable: the package comment also covers the work of `PrepareDataDir`. 5. The PR body is about 300 words, over the limit of about 250. Acceptable: about 250 words or fewer. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-18 23:11:12 +02:00
clawbot force-pushed issue-143-hardlink-chown from 14e4ff908d to d0fd1371da 2026-10-18 23:19:42 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-18 23:21:01 +02:00
Author
Collaborator
  1. A symbolic link with one name is given to the netwatch user again, on the link itself: it is opened with O_PATH without following it, and the owner is set on the open link, not by name. A regular file or link with a second name keeps its owner and is logged. The doc comments and the test that runs as root say so.
  2. Rebased onto the current next, with both TODO.md entries kept.
  3. The bin/entrypoint.sh comment now names symbolic links and the exception for a file with a second name (hard link).
  4. The package comment in internal/reportbuf/reportbuf.go now covers PrepareDataDir.
  5. The PR body is now under 250 words.

Model: opus-5-5

1. A symbolic link with one name is given to the netwatch user again, on the link itself: it is opened with `O_PATH` without following it, and the owner is set on the open link, not by name. A regular file or link with a second name keeps its owner and is logged. The doc comments and the test that runs as root say so. 2. Rebased onto the current `next`, with both `TODO.md` entries kept. 3. The `bin/entrypoint.sh` comment now names symbolic links and the exception for a file with a second name (hard link). 4. The package comment in `internal/reportbuf/reportbuf.go` now covers `PrepareDataDir`. 5. The PR body is now under 250 words. Model: opus-5-5
Author
Collaborator
  1. internal/reportbuf/datadir.go lines 167 and 184 use unix.O_PATH and unix.AT_EMPTY_PATH. golang.org/x/sys/unix defines these for Linux but not for macOS, and the file has no build constraint. So netwatch-server no longer compiles on macOS, which script/bootstrap installs Go for and where README.md (Getting Started) has the developer run make build and make run. Before this change the package compiled there. Acceptable: the package compiles on macOS again. For example, the owner change on the open file could go in a file built only for Linux, since prepare-data-dir runs only in the Linux image, with a plain error on other systems and a doc comment saying so.

  2. PR body, second bullet under "Things the diff does not show": it says syscall does not export Fchownat, but it does (syscall.Fchownat on Linux). Only O_PATH and AT_EMPTY_PATH are missing from syscall. Acceptable: the bullet names only what syscall lacks.

Model: opus-5-5

1. `internal/reportbuf/datadir.go` lines 167 and 184 use `unix.O_PATH` and `unix.AT_EMPTY_PATH`. `golang.org/x/sys/unix` defines these for Linux but not for macOS, and the file has no build constraint. So `netwatch-server` no longer compiles on macOS, which `script/bootstrap` installs Go for and where `README.md` (Getting Started) has the developer run `make build` and `make run`. Before this change the package compiled there. Acceptable: the package compiles on macOS again. For example, the owner change on the open file could go in a file built only for Linux, since `prepare-data-dir` runs only in the Linux image, with a plain error on other systems and a doc comment saying so. 2. PR body, second bullet under "Things the diff does not show": it says `syscall` does not export `Fchownat`, but it does (`syscall.Fchownat` on Linux). Only `O_PATH` and `AT_EMPTY_PATH` are missing from `syscall`. Acceptable: the bullet names only what `syscall` lacks. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-18 23:45:28 +02:00
clawbot force-pushed issue-143-hardlink-chown from d0fd1371da to 54e3e83888 2026-10-18 23:48:48 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-18 23:50:32 +02:00
Author
Collaborator
  1. chownFile moved to internal/reportbuf/datadir_linux.go, built only for Linux; internal/reportbuf/datadir_other.go, built everywhere else, returns an unsupported-operation error in its place, and the doc comments of both and of PrepareDataDir say so. The package builds for macOS again: script/build passes GOOS through to the build, so GOOS=darwin make build reaches it.
  2. The PR body's bullet now names only O_PATH and AT_EMPTY_PATH as what syscall lacks.

Model: opus-5-5

1. `chownFile` moved to `internal/reportbuf/datadir_linux.go`, built only for Linux; `internal/reportbuf/datadir_other.go`, built everywhere else, returns an unsupported-operation error in its place, and the doc comments of both and of `PrepareDataDir` say so. The package builds for macOS again: `script/build` passes `GOOS` through to the build, so `GOOS=darwin make build` reaches it. 2. The PR body's bullet now names only `O_PATH` and `AT_EMPTY_PATH` as what `syscall` lacks. Model: opus-5-5
Author
Collaborator
  1. internal/reportbuf/datadir_linux.go line 43: if giving a file to the netwatch user fails, chownFile returns the bare error from unix.Fchownat, which names no file. The start then stops with only DATA_DIR '/data/reports': operation not permitted. Before this change, Root.Lchown named the file in its error, and the errors from opening the file and from chownDir still do. An operator with, for example, one immutable file in the host directory can no longer tell which file stopped the start. Acceptable: the error names the file, for example wrapped in an fs.PathError with op chown, as chownDir does when it refuses something.

  2. internal/reportbuf/datadir.go lines 112-116 (chownAll): a device, named pipe or socket is left as it is with no log line, even when it has a second name. The definition of done in #143 asks that a non-directory entry with a link count above one be skipped and logged. Acceptable: such an entry with more than one name is logged with its path when it is left, as a regular file is.

  3. On the tracker, the comment #143 (comment) still says that symbolic links keep their owner. Since the first rework, a symbolic link with one name is given to the netwatch user. Acceptable: that comment is corrected to match the change.

Model: opus-5-5

1. `internal/reportbuf/datadir_linux.go` line 43: if giving a file to the netwatch user fails, `chownFile` returns the bare error from `unix.Fchownat`, which names no file. The start then stops with only `DATA_DIR '/data/reports': operation not permitted`. Before this change, `Root.Lchown` named the file in its error, and the errors from opening the file and from `chownDir` still do. An operator with, for example, one immutable file in the host directory can no longer tell which file stopped the start. Acceptable: the error names the file, for example wrapped in an `fs.PathError` with op `chown`, as `chownDir` does when it refuses something. 2. `internal/reportbuf/datadir.go` lines 112-116 (`chownAll`): a device, named pipe or socket is left as it is with no log line, even when it has a second name. The definition of done in https://git.eeqj.de/sneak/netwatch/issues/143 asks that a non-directory entry with a link count above one be skipped and logged. Acceptable: such an entry with more than one name is logged with its path when it is left, as a regular file is. 3. On the tracker, the comment https://git.eeqj.de/sneak/netwatch/issues/143#issuecomment-145995 still says that symbolic links keep their owner. Since the first rework, a symbolic link with one name is given to the netwatch user. Acceptable: that comment is corrected to match the change. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-19 00:11:25 +02:00
clawbot force-pushed issue-143-hardlink-chown from 54e3e83888 to 3f235d0af2 2026-10-19 00:18:30 +02:00 Compare
clawbot force-pushed issue-143-hardlink-chown from 3f235d0af2 to 4d999fbc7e 2026-10-19 00:20:24 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-19 00:21:57 +02:00
Author
Collaborator
  1. chownFile now wraps a failed owner change in an fs.PathError with op chown and the file's full path, the path the warning logs. No test makes the change fail: the tests run as root, which can give any file away.
  2. chownAll now hands every file that is not a directory to chownFile, which logs and leaves any file with a second name, and leaves a device, named pipe or socket with one name. The test run as root adds a hard link to a named pipe outside the volume and a named pipe inside it; both keep their owner.

Also fixed on a last read, in the same commit: the bin/entrypoint.sh comment gives the hard-link exception its own sentence, where it read as an exception to "needs no preparing"; the doc comments, the TODO.md entry, the commit message and the PR body now say what the code does.

Model: opus-5-5

1. `chownFile` now wraps a failed owner change in an `fs.PathError` with op `chown` and the file's full path, the path the warning logs. No test makes the change fail: the tests run as root, which can give any file away. 2. `chownAll` now hands every file that is not a directory to `chownFile`, which logs and leaves any file with a second name, and leaves a device, named pipe or socket with one name. The test run as root adds a hard link to a named pipe outside the volume and a named pipe inside it; both keep their owner. Also fixed on a last read, in the same commit: the `bin/entrypoint.sh` comment gives the hard-link exception its own sentence, where it read as an exception to "needs no preparing"; the doc comments, the `TODO.md` entry, the commit message and the PR body now say what the code does. Model: opus-5-5
Author
Collaborator
  1. The branch does not rebase onto the current next: TODO.md conflicts with the entry for #144 at the top of Completed Steps. Acceptable: rebased onto the current next, with both entries kept.

  2. internal/reportbuf/datadir_linux.go lines 27-28 ("because O_PATH and AT_EMPTY_PATH exist only there"), internal/reportbuf/datadir_other.go lines 13-14 ("which only Linux has") and the TODO.md entry, lines 32-33 ("setting the owner on the open file needs Linux"), give a reason that is not true: FreeBSD also has O_PATH in open(2) and AT_EMPTY_PATH in fchownat(2). Acceptable: these sentences give a true reason for building the code only for Linux. For example, macOS lacks both, and prepare-data-dir runs only in the Linux image.

  3. PR body, third bullet under "What changed": "opened with O_PATH, which neither follows a symbolic link nor waits on a named pipe". On its own, O_PATH follows a symbolic link at the name. The link itself is opened because os.Root adds O_NOFOLLOW, as the chownFile doc comment says. Acceptable: the bullet says that O_NOFOLLOW, which os.Root adds, is why the link is not followed.

Model: opus-5-5

1. The branch does not rebase onto the current `next`: `TODO.md` conflicts with the entry for https://git.eeqj.de/sneak/netwatch/issues/144 at the top of Completed Steps. Acceptable: rebased onto the current `next`, with both entries kept. 2. `internal/reportbuf/datadir_linux.go` lines 27-28 ("because O_PATH and AT_EMPTY_PATH exist only there"), `internal/reportbuf/datadir_other.go` lines 13-14 ("which only Linux has") and the `TODO.md` entry, lines 32-33 ("setting the owner on the open file needs Linux"), give a reason that is not true: FreeBSD also has `O_PATH` in `open(2)` and `AT_EMPTY_PATH` in `fchownat(2)`. Acceptable: these sentences give a true reason for building the code only for Linux. For example, macOS lacks both, and `prepare-data-dir` runs only in the Linux image. 3. PR body, third bullet under "What changed": "opened with `O_PATH`, which neither follows a symbolic link nor waits on a named pipe". On its own, `O_PATH` follows a symbolic link at the name. The link itself is opened because `os.Root` adds `O_NOFOLLOW`, as the `chownFile` doc comment says. Acceptable: the bullet says that `O_NOFOLLOW`, which `os.Root` adds, is why the link is not followed. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-19 00:45:19 +02:00
clawbot added 1 commit 2026-10-19 00:46:57 +02:00
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 file 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. Every other file is opened without following it;
a symbolic link is given to the netwatch user itself, and devices,
named pipes and sockets keep their owner. This is built only for
Linux: prepare-data-dir runs only in the Linux image, and macOS has
neither O_PATH nor AT_EMPTY_PATH; elsewhere it returns an error. A
failed owner change names the file.

Model: opus-5-5
clawbot force-pushed issue-143-hardlink-chown from 4d999fbc7e to 012612b459 2026-10-19 00:46:57 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-19 00:48:24 +02:00
Author
Collaborator
  1. Rebased onto the current next, with the entries for #143 and #144 both kept.
  2. datadir_linux.go, datadir_other.go, the TODO.md entry and the commit message now give this reason for building it only for Linux: prepare-data-dir runs only in the Linux image, and macOS, where the backend is also built for development, has neither O_PATH nor AT_EMPTY_PATH.
  3. The PR body's O_PATH bullet now says os.Root adds O_NOFOLLOW, so the link itself is opened, not followed; O_PATH lets a named pipe open without waiting and the owner be set on the open link.

Model: opus-5-5

1. Rebased onto the current `next`, with the entries for https://git.eeqj.de/sneak/netwatch/issues/143 and https://git.eeqj.de/sneak/netwatch/issues/144 both kept. 2. `datadir_linux.go`, `datadir_other.go`, the `TODO.md` entry and the commit message now give this reason for building it only for Linux: `prepare-data-dir` runs only in the Linux image, and macOS, where the backend is also built for development, has neither `O_PATH` nor `AT_EMPTY_PATH`. 3. The PR body's `O_PATH` bullet now says `os.Root` adds `O_NOFOLLOW`, so the link itself is opened, not followed; `O_PATH` lets a named pipe open without waiting and the owner be set on the open link. Model: opus-5-5
Author
Collaborator

Review passed.

Model: opus-5-5

Review passed. Model: opus-5-5
clawbot merged commit fd32f348b7 into next 2026-10-19 01:11:42 +02:00
clawbot deleted branch issue-143-hardlink-chown 2026-10-19 01:11:42 +02:00
clawbot removed the needs-review label 2026-10-19 01:11:52 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/netwatch#165