fill watch: one warning per filesystem (R-1371) — committed, not released
gates / gates (push) Successful in 31s

On a box whose guest has one data volume the two built-in fill targets, "/"
(the controller's overlay on the Docker data volume) and /mnt/sys_drive, are one
filesystem. The target list was de-duplicated by path only, so one fill would
send two warnings under two names.

fillTargets now keeps one target per filesystem; the first stays (app area,
system area, drives). The identity is system.FilesystemKey, not st_dev alone:
inside the container "/" is an overlay with its own device number, so an
overlay path takes the device of a Docker-kept file on the data-root
(/etc/hostname, /etc/hosts, /etc/resolv.conf). Measured read-only on demo-hp
9201/9202 and the N100 on 2026-11-10.

Tests: TestR1371_* (3), TestFilesystemKey_* (2). Red seen three times.
No version, no image, no floor: it waits for a release session.

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
admin committed 2026-11-10 14:18:36 +01:00
1 parent da060d7e7f
commit 844f19f11e
11 files changed
+438 -22

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
package system
// Which filesystem does a path lie on? — the identity the fill watch de-duplicates by (R-1371).
//
// WHY st_dev ALONE IS NOT THE ANSWER. Inside the controller's container "/" is an OVERLAY. An overlay has its own
// device number and its own filesystem id, and both differ from the filesystem it lies on — while statfs reports that
// filesystem's size and free space. Measured 2026-11-10 on demo-hp (guests 9201, 9202) and the N100 (felhom.eu
// documentation/audits/space-list-2026-11-10/evidence/extra/fs-identity.txt):
//
// / type=overlayfs dev=139 blocks=18016108 free=10674453
// /mnt/sys_drive type=ext2/ext3 dev=64519 blocks=18016108 free=10674452
// /etc/hostname type=ext2/ext3 dev=64519 (the same on /etc/hosts and /etc/resolv.conf)
//
// One filesystem, two device numbers. The size-and-free numbers cannot be the identity either: the two `free` values
// above are from ONE read loop and already differ by one block — a filesystem that is filling is being written.
//
// THE RULE. A path that is not on an overlay is identified by its device number. A path on an overlay is identified by
// the device number of a file Docker itself keeps on its data-root and bind-mounts into every container
// (dockerDataRootAnchors): the container's overlay lies on that same data-root. If no anchor can be read, there is NO
// key — and a caller must then treat the path as a filesystem of its own (one warning too many, never one too few).
//
// LIMITS, stated: this holds for a container that Docker runs with Docker's own /etc files (the product's only
// shape). A container whose /etc/hostname is bind-mounted from another filesystem by hand would get the wrong key.
// dockerDataRootAnchors are the files Docker writes under <data-root>/containers/<id>/ and bind-mounts into the
// container. Any one of them names the data-root's real device.
var dockerDataRootAnchors = []string{"/etc/hostname", "/etc/hosts", "/etc/resolv.conf"}
// FilesystemKey returns a number that is equal for two paths exactly when they lie on the same filesystem, as far as
// this process can tell, and ok=false when it cannot tell.
func FilesystemKey(path string) (key uint64, ok bool) {
key, ok = filesystemKeyWith(path, statDev, isOverlayFS, dockerDataRootAnchors)
debugf("[DEBUG] [system] FilesystemKey: path=%q key=%d ok=%v", path, key, ok)
return key, ok
}
// filesystemKeyWith is the rule with its three inputs injected (the tests' seam): dev reads a path's device number,
// overlay says whether a path lies on an overlay; both report ok=false for a path they cannot read.
func filesystemKeyWith(path string, dev func(string) (uint64, bool), overlay func(string) (isOverlay, ok bool), anchors []string) (uint64, bool) {
if path == "" {
return 0, false
}
isOv, ok := overlay(path)
if !ok {
return 0, false
}
if !isOv {
return dev(path)
}
for _, a := range anchors {
if aOv, ok := overlay(a); !ok || aOv {
continue // unreadable, or itself on the overlay (not bind-mounted): it names nothing
}
if d, ok := dev(a); ok {
return d, true
}
}
return 0, false
}
+24
View File
@@ -0,0 +1,24 @@
//go:build linux
package system
import "syscall"
// overlayMagic is statfs's f_type for an overlay filesystem (OVERLAYFS_SUPER_MAGIC, linux/magic.h).
const overlayMagic = 0x794c7630
func statDev(path string) (uint64, bool) {
var st syscall.Stat_t
if err := syscall.Stat(path, &st); err != nil {
return 0, false
}
return uint64(st.Dev), true
}
func isOverlayFS(path string) (isOverlay, ok bool) {
var st syscall.Statfs_t
if err := syscall.Statfs(path, &st); err != nil {
return false, false
}
return int64(st.Type) == overlayMagic, true
}
@@ -0,0 +1,8 @@
//go:build !linux
package system
// Not Linux (dev/testing only): no filesystem identity, so nothing is ever de-duplicated.
func statDev(_ string) (uint64, bool) { return 0, false }
func isOverlayFS(_ string) (isOverlay, ok bool) { return false, false }
+76
View File
@@ -0,0 +1,76 @@
package system
import (
"os"
"path/filepath"
"runtime"
"testing"
)
// R-1371 — the rule, with the numbers measured on demo-hp's guest 9201 (fskey.go).
func TestFilesystemKey_AnOverlayRootIsTheDockerDataRoot(t *testing.T) {
devs := map[string]uint64{"/": 139, "/mnt/sys_drive": 64519, "/etc/hostname": 64519, "/etc/hosts": 64519, "/opt/docker/stacks": 64518}
ovl := map[string]bool{"/": true}
dev := func(p string) (uint64, bool) { d, ok := devs[p]; return d, ok }
overlay := func(p string) (bool, bool) {
if _, ok := devs[p]; !ok {
return false, false
}
return ovl[p], true
}
key := func(p string) (uint64, bool) { return filesystemKeyWith(p, dev, overlay, dockerDataRootAnchors) }
root, ok1 := key("/")
sys, ok2 := key("/mnt/sys_drive")
if !ok1 || !ok2 || root != sys {
t.Fatalf("/ (%d,%v) and /mnt/sys_drive (%d,%v) are ONE filesystem on every box measured — the keys must be equal", root, ok1, sys, ok2)
}
if stacks, ok := key("/opt/docker/stacks"); !ok || stacks == root {
t.Fatalf("/opt/docker/stacks (%d,%v) is another filesystem — its key must differ from %d", stacks, ok, root)
}
if _, ok := key("/mnt/not-mounted"); ok {
t.Fatal("an unreadable path must have no key")
}
if _, ok := key(""); ok {
t.Fatal("an empty path must have no key")
}
// No anchor readable → NO key (never the overlay's own number, which would look like a filesystem of its own
// only by luck, and never a guess).
if k, ok := filesystemKeyWith("/", dev, overlay, []string{"/etc/absent"}); ok {
t.Fatalf("an overlay with no readable anchor got key %d — want no key", k)
}
// An anchor that is itself on the overlay (not bind-mounted) names nothing; the next one is used.
devs["/etc/plain"] = 139
ovl["/etc/plain"] = true
if k, ok := filesystemKeyWith("/", dev, overlay, []string{"/etc/plain", "/etc/hosts"}); !ok || k != 64519 {
t.Fatalf("key = %d,%v — want the first anchor that is NOT on the overlay (64519)", k, ok)
}
}
// The real system calls: two paths on one filesystem share a key, a path on another filesystem does not, and an
// absent path has none. Runs on the machine's real filesystems (and, in a container, through the overlay branch).
func TestFilesystemKey_RealSyscalls(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("the identity is Linux-only")
}
dir := t.TempDir()
sub := filepath.Join(dir, "sub")
if err := os.Mkdir(sub, 0o755); err != nil {
t.Fatal(err)
}
a, okA := FilesystemKey(dir)
b, okB := FilesystemKey(sub)
if !okA || !okB || a != b {
t.Fatalf("a folder (%d,%v) and its sub-folder (%d,%v) must share a key", a, okA, b, okB)
}
if p, ok := FilesystemKey("/proc"); !ok || p == a {
t.Fatalf("/proc (%d,%v) is another filesystem than %s (%d)", p, ok, dir, a)
}
if k, ok := FilesystemKey(filepath.Join(dir, "absent")); ok {
t.Fatalf("an absent path got key %d", k)
}
if ov, ok := isOverlayFS("/proc"); !ok || ov {
t.Fatalf("/proc read as overlay=%v ok=%v", ov, ok)
}
}