prepare-data-dir changes the owner of a file outside /data through a hard link in the volume #143

Closed
opened 2026-10-14 16:08:46 +02:00 by clawbot · 1 comment
Collaborator

internal/reportbuf/datadir.go lines 90 to 122 (lchownAll), at v1.0.0-rc1: prepare-data-dir, run as root at every start, calls root.Lchown on every file under /data. A hard link shares its inode with the file it links to, anywhere on the same filesystem, so chowning the link changes the owner of that file too, wherever it lives on the host. chmodDir (lines 129 to 131) guards against hard links for the mode; the owner change does not.

How to see it: on the host, in a directory to be mounted at /data, hard-link a file from outside it (ln /outside/file data/link; with fs.protected_hardlinks=1 the linking user must own the file or have read and write access to it), start the image with -v $PWD/data:/data, and stat -c %u /outside/file: it is now 1000. Seen with the image built from the tag: a file owned by uid 1234 outside the mounted directory was owned by uid 1000 after one start.

What is right: README.md line 437: netwatch-server prepare-data-dir "acts on nothing outside /data". The entry's link count is known before chowning (os.Root.Lstat, Sys().Nlink); a regular file with more than one name should be skipped (and logged), as symbolic links already are followed by nothing.

Severity: low. It needs a hard link planted in the volume by someone who can already read and write the file; the gain is ownership of that file.

Definition of done

  • lchownAll skips (and logs) a non-directory entry whose link count is above one, so no file with a name outside /data changes owner; the open-then-fstat pattern of chmodDir is used so a link swapped in after the check is not followed.
  • A test in internal/reportbuf/datadir_test.go hard-links a file from outside the volume into it and checks that the file's owner is unchanged after PrepareDataDir (it can run as an unprivileged user by checking that no chown was attempted on the link, or be skipped unless root).
  • README.md line 437 stays true.
  • TODO.md updated in the same commit; commit title ends (closes #N) with this issue's number.

Model: fable-5-1

`internal/reportbuf/datadir.go` lines 90 to 122 (`lchownAll`), at `v1.0.0-rc1`: `prepare-data-dir`, run as root at every start, calls `root.Lchown` on every file under `/data`. A hard link shares its inode with the file it links to, anywhere on the same filesystem, so chowning the link changes the owner of that file too, wherever it lives on the host. `chmodDir` (lines 129 to 131) guards against hard links for the mode; the owner change does not. **How to see it**: on the host, in a directory to be mounted at `/data`, hard-link a file from outside it (`ln /outside/file data/link`; with `fs.protected_hardlinks=1` the linking user must own the file or have read and write access to it), start the image with `-v $PWD/data:/data`, and `stat -c %u /outside/file`: it is now 1000. Seen with the image built from the tag: a file owned by uid 1234 outside the mounted directory was owned by uid 1000 after one start. **What is right**: `README.md` line 437: `netwatch-server prepare-data-dir` "acts on nothing outside `/data`". The entry's link count is known before chowning (`os.Root.Lstat`, `Sys().Nlink`); a regular file with more than one name should be skipped (and logged), as symbolic links already are followed by nothing. **Severity**: low. It needs a hard link planted in the volume by someone who can already read and write the file; the gain is ownership of that file. ## Definition of done - [ ] `lchownAll` skips (and logs) a non-directory entry whose link count is above one, so no file with a name outside `/data` changes owner; the open-then-fstat pattern of `chmodDir` is used so a link swapped in after the check is not followed. - [ ] A test in `internal/reportbuf/datadir_test.go` hard-links a file from outside the volume into it and checks that the file's owner is unchanged after `PrepareDataDir` (it can run as an unprivileged user by checking that no chown was attempted on the link, or be skipped unless root). - [ ] `README.md` line 437 stays true. - [ ] `TODO.md` updated in the same commit; commit title ends ` (closes #N)` with this issue's number. Model: fable-5-1
clawbot self-assigned this 2026-10-14 16:08:46 +02:00
Author
Collaborator

Fixed in #165. An entry with a second name (hard link) now keeps its owner and is logged. Owners are set on the opened file, directory or symbolic link, after its link count is read from it, never by name. A symbolic link with one name is still given to the netwatch user, on the link itself.

Model: opus-5-5

Fixed in https://git.eeqj.de/sneak/netwatch/pulls/165. An entry with a second name (hard link) now keeps its owner and is logged. Owners are set on the opened file, directory or symbolic link, after its link count is read from it, never by name. A symbolic link with one name is still given to the netwatch user, on the link itself. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/netwatch#143