// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package nfs4server_test import ( "errors" "net" "os" "path/filepath" "syscall" "testing" "io/fs" "sourcedock.dev/petrbalvin/nfs/internal/nfs4" "sourcedock.dev/petrbalvin/nfs/internal/nfs4server" "sourcedock.dev/petrbalvin/nfs/internal/nfsclient" "sourcedock.dev/petrbalvin/nfs/internal/nfsfs" "sourcedock.dev/petrbalvin/nfs/internal/server" "sourcedock.dev/petrbalvin/nfs/internal/xdr" ) // serveSquash starts a handler over a loopback listener with the export // holding one file, and answers whether root is squashed. func serveSquash(t *testing.T, squash bool) (addr, file string) { t.Helper() root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "x.txt"), []byte("x"), 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } backend, err := nfsfs.NewLocal(root) if err != nil { t.Fatalf("NewLocal: %v", err) } ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("Listen: %v", err) } t.Cleanup(func() { _ = ln.Close() }) handler := &nfs4server.Handler{FS: backend, RootSquash: squash} go func() { _ = (&server.Server{Handle: handler.HandleConn}).Serve(t.Context(), ln) }() cl, err := nfsclient.Dial(ln.Addr().String()) if err != nil { t.Fatalf("Dial: %v", err) } defer cl.Close() if err := cl.Establish("squash-test"); err != nil { t.Fatalf("Establish: %v", err) } res, _, err := cl.Compound("remove", [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendRemoveArgs(nil, "x.txt"), }) if err != nil { t.Fatalf("remove compound: %v", err) } // A removal inside the export root needs the modify right on the // directory: the superuser grant answers it, the anonymous identity // does not. status := res.Status if squash && status != nfs4.ErrAccess { t.Fatalf("a squashed root removed a file it must not: status %d", status) } if !squash && status != nfs4.ErrOK { t.Fatalf("root lost its grant without squash: status %d", status) } return ln.Addr().String(), filepath.Join(root, "x.txt") } // TestRootSquashRemovesRootGrant covers the core of the feature: the // squashed credential has no superuser grant, so a removal root may not // make is refused with NFS4ERR_ACCESS. func TestRootSquashRemovesRootGrant(t *testing.T) { serveSquash(t, true) } // TestNoSquashKeepsRootGrant covers the default: without the switch the // root claim keeps everything AUTH_SYS grants it. func TestNoSquashKeepsRootGrant(t *testing.T) { addr, file := serveSquash(t, false) if _, err := os.Stat(file); !errors.Is(err, fs.ErrNotExist) { t.Fatalf("the removal did not land: %v", err) } _ = addr } // TestRootSquashOwnerUnderRoot covers the owner attribution on a server // that can chown: a file root creates carries nobody. A server without // the privilege keeps its own identity on the objects it makes, the // documented fallback, so the assertion runs only where it is // guaranteed. func TestRootSquashOwnerUnderRoot(t *testing.T) { if os.Geteuid() != 0 { t.Skip("the owner attribution needs the chown privilege; run as root") } root := t.TempDir() backend, err := nfsfs.NewLocal(root) if err != nil { t.Fatalf("NewLocal: %v", err) } ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("Listen: %v", err) } defer ln.Close() handler := &nfs4server.Handler{FS: backend, RootSquash: true} go func() { _ = (&server.Server{Handle: handler.HandleConn}).Serve(t.Context(), ln) }() cl, err := nfsclient.Dial(ln.Addr().String()) if err != nil { t.Fatalf("Dial: %v", err) } defer cl.Close() if err := cl.Establish("squash-owner"); err != nil { t.Fatalf("Establish: %v", err) } res, bodies, err := cl.Compound("create", [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendOpenArgs(nil, 0, []byte("root"), nfs4.ShareAccessBoth, 0, true, 0o644, "made.txt"), nfs4.AppendGetfh(nil), }) if err != nil || res.Status != nfs4.ErrOK { t.Fatalf("open: status %d, %v", res.Status, err) } var st nfs4.Stateid copy(st[:], bodies[len(bodies)-2]) fh, err := xdr.NewDecoder(bodies[len(bodies)-1]).VarOpaque() if err != nil { t.Fatalf("decode fh: %v", err) } if _, _, err := cl.Compound("close", [][]byte{ nfs4.AppendPutfh(nil, fh), nfs4.AppendCloseArgs(nil, st), }); err != nil { t.Fatalf("close: %v", err) } info, err := os.Stat(filepath.Join(root, "made.txt")) if err != nil { t.Fatalf("stat: %v", err) } sys, ok := info.Sys().(*syscall.Stat_t) if !ok { t.Skip("the raw stat is unavailable on this platform") } if sys.Uid != 65534 { t.Fatalf("a root created file carries uid %d, want 65534", sys.Uid) } }