Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
Assisted-by: GLM 5.3 Flash
2167 lines
69 KiB
Go
2167 lines
69 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package nfs4server
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
|
|
"sourcedock.dev/petrbalvin/nfs/internal/nfsclient"
|
|
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
|
|
"sourcedock.dev/petrbalvin/nfs/internal/rpc"
|
|
"sourcedock.dev/petrbalvin/nfs/internal/server"
|
|
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
|
|
)
|
|
|
|
func testTree(t *testing.T) *Handler {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(root, "a.txt"), []byte("hello nfs"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(root, "sub"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "sub", "b.txt"), []byte("inner"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink("a.txt", filepath.Join(root, "link")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backend, err := nfsfs.NewLocal(root)
|
|
if err != nil {
|
|
t.Fatalf("backend: %v", err)
|
|
}
|
|
return &Handler{FS: backend}
|
|
}
|
|
|
|
func compoundOps(t *testing.T, h *Handler, minor uint32, ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
|
t.Helper()
|
|
if minor == nfs4.MinorVersion {
|
|
sid, seq := newSession(t, h)
|
|
ops = append([][]byte{nfs4.AppendSequenceArgs(nil, sid, seq, 0, defaultSlots-1, true)}, ops...)
|
|
}
|
|
payload := nfs4.AppendCompoundArgs(nil, "test", minor, ops)
|
|
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("well formed arguments were treated as garbage")
|
|
}
|
|
res, bodies, err := nfs4.DecodeCompoundResBodies(body)
|
|
if err != nil {
|
|
t.Fatalf("decode result: %v", err)
|
|
}
|
|
if minor == nfs4.MinorVersion {
|
|
res.Ops = res.Ops[1:]
|
|
bodies = bodies[1:]
|
|
}
|
|
return res, bodies
|
|
}
|
|
|
|
// newSession runs EXCHANGE_ID and CREATE_SESSION against the handler,
|
|
// each in its own COMPOUND with a fresh identity, and returns the session
|
|
// id and the sequence the next SEQUENCE must carry.
|
|
func newSession(t *testing.T, h *Handler) (nfs4.SessionID, uint32) {
|
|
t.Helper()
|
|
n := h.probeCount()
|
|
verifier := [8]byte{byte(n), 3, 3, 7, 7, 7, 7, 7}
|
|
owner := []byte{byte(n), 'o', 'w', 'n'}
|
|
|
|
ex := nfs4.AppendExchangeIDArgs(nil, verifier, owner, 0)
|
|
payload := nfs4.AppendCompoundArgs(nil, "ex", nfs4.MinorVersion, [][]byte{ex})
|
|
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("exchange id treated as garbage")
|
|
}
|
|
res, bodies, err := nfs4.DecodeCompoundResBodies(body)
|
|
if err != nil || res.Status != nfs4.ErrOK {
|
|
t.Fatalf("exchange id: status %d, %v", res.Status, err)
|
|
}
|
|
clientid, err := xdr.NewDecoder(bodies[0]).Uint64()
|
|
if err != nil {
|
|
t.Fatalf("clientid: %v", err)
|
|
}
|
|
|
|
cs := nfs4.AppendCreateSessionArgs(nil, clientid, 1, 0,
|
|
nfs4.DefaultForeChannel, nfs4.DefaultBackChannel, 0)
|
|
payload = nfs4.AppendCompoundArgs(nil, "cs", nfs4.MinorVersion, [][]byte{cs})
|
|
body, ok = h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("create session treated as garbage")
|
|
}
|
|
res, bodies, err = nfs4.DecodeCompoundResBodies(body)
|
|
if err != nil || res.Status != nfs4.ErrOK {
|
|
t.Fatalf("create session: status %d, %v", res.Status, err)
|
|
}
|
|
var sid nfs4.SessionID
|
|
copy(sid[:], bodies[0])
|
|
return sid, 1
|
|
}
|
|
|
|
func wantStatus(t *testing.T, where string, got, want uint32) {
|
|
t.Helper()
|
|
if got != want {
|
|
t.Fatalf("%s: status %d, want %d", where, got, want)
|
|
}
|
|
}
|
|
|
|
func TestMountLikeSession(t *testing.T) {
|
|
h := testTree(t)
|
|
var ops [][]byte
|
|
ops = append(ops, nfs4.AppendPutRootfh(nil))
|
|
ops = append(ops, nfs4.AppendGetfh(nil))
|
|
ops = append(ops, nfs4.AppendGetattr(nil, nfs4.OfBits(
|
|
nfs4.AttrType, nfs4.AttrSize, nfs4.AttrFSID, nfs4.AttrMode,
|
|
nfs4.AttrChange, nfs4.AttrMountedOnFileID, nfs4.AttrNumlinks)))
|
|
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 3 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
fh, err := xdr.NewDecoder(bodies[1]).VarOpaque()
|
|
if err != nil || len(fh) == 0 {
|
|
t.Fatalf("root handle: %x, %v", fh, err)
|
|
}
|
|
response, attrs, err := nfs4.DecodeGetattrBody(bodies[2])
|
|
if err != nil {
|
|
t.Fatalf("getattr body: %v", err)
|
|
}
|
|
if !response.Has(nfs4.AttrType) || attrs.Type != nfs4.NF4Dir {
|
|
t.Fatalf("root is not a directory: type %d", attrs.Type)
|
|
}
|
|
if attrs.FSID[0] == 0 {
|
|
t.Fatalf("fsid major is zero: %+v", attrs.FSID)
|
|
}
|
|
}
|
|
|
|
func TestLookupReadFile(t *testing.T) {
|
|
h := testTree(t)
|
|
var ops [][]byte
|
|
ops = append(ops, nfs4.AppendPutRootfh(nil))
|
|
ops = append(ops, nfs4.AppendLookup(nil, "a.txt"))
|
|
ops = append(ops, nfs4.AppendGetfh(nil))
|
|
ops = append(ops, nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType, nfs4.AttrSize)))
|
|
ops = append(ops, nfs4.AppendRead(nil, nfs4.AllZero, 6, 64))
|
|
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 5 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
_, attrs, err := nfs4.DecodeGetattrBody(bodies[3])
|
|
if err != nil || attrs.Type != nfs4.NF4Reg || attrs.Size != 9 {
|
|
t.Fatalf("a.txt attrs: %+v, %v", attrs, err)
|
|
}
|
|
eof, data, err := nfs4.DecodeReadBody(bodies[4])
|
|
if err != nil || string(data) != "nfs" {
|
|
t.Fatalf("read tail: %q, eof %v, %v", data, eof, err)
|
|
}
|
|
// A read from the start that fills the whole file reports EOF.
|
|
ops = append(ops[:4], nfs4.AppendRead(nil, nfs4.AllZero, 0, 64))
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("full read status %d", res.Status)
|
|
}
|
|
eof, data, err = nfs4.DecodeReadBody(bodies[4])
|
|
if err != nil || !eof || string(data) != "hello nfs" {
|
|
t.Fatalf("full read: %q eof %v, %v", data, eof, err)
|
|
}
|
|
}
|
|
|
|
func TestLookupMissingFailsTheCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
var ops [][]byte
|
|
ops = append(ops, nfs4.AppendPutRootfh(nil))
|
|
ops = append(ops, nfs4.AppendLookup(nil, "missing"))
|
|
ops = append(ops, nfs4.AppendGetfh(nil))
|
|
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "compound", res.Status, nfs4.ErrNoEnt)
|
|
if len(res.Ops) != 2 {
|
|
t.Fatalf("%d results, want 2: the third operation never ran", len(res.Ops))
|
|
}
|
|
wantStatus(t, "lookup", res.Ops[1].Status, nfs4.ErrNoEnt)
|
|
if len(bodies) > 2 && len(bodies[2]) != 0 {
|
|
t.Fatalf("the skipped operation produced a body")
|
|
}
|
|
}
|
|
|
|
func TestMinorVersionMismatch(t *testing.T) {
|
|
h := testTree(t)
|
|
res, _ := compoundOps(t, h, 1, [][]byte{nfs4.AppendPutRootfh(nil)})
|
|
wantStatus(t, "compound", res.Status, nfs4.ErrMinorVersMismatch)
|
|
if len(res.Ops) != 0 {
|
|
t.Fatalf("a mismatch carries %d results", len(res.Ops))
|
|
}
|
|
}
|
|
|
|
func TestUnsupportedOperation(t *testing.T) {
|
|
h := testTree(t)
|
|
var ops [][]byte
|
|
ops = append(ops, nfs4.AppendPutRootfh(nil))
|
|
ops = append(ops, nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType)))
|
|
// SET_SSV carries no meaning without the SSV mechanism and is
|
|
// answered NOT_SUPP.
|
|
ops[len(ops)-1] = []byte{0, 0, 0, byte(nfs4.OpSetSsv)}
|
|
res, _ := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "compound", res.Status, nfs4.ErrNotSupp)
|
|
wantStatus(t, "ssv", res.Ops[1].Status, nfs4.ErrNotSupp)
|
|
}
|
|
|
|
func TestGetfhWithoutCurrent(t *testing.T) {
|
|
h := testTree(t)
|
|
res, _ := compoundOps(t, h, nfs4.MinorVersion, [][]byte{nfs4.AppendGetfh(nil)})
|
|
wantStatus(t, "getfh", res.Ops[0].Status, nfs4.ErrNoFileHandle)
|
|
}
|
|
|
|
func TestSaveRestore(t *testing.T) {
|
|
h := testTree(t)
|
|
var ops [][]byte
|
|
ops = append(ops, nfs4.AppendPutRootfh(nil))
|
|
ops = append(ops, nfs4.AppendLookup(nil, "sub"))
|
|
ops = append(ops, nfs4.AppendSavefh(nil))
|
|
ops = append(ops, nfs4.AppendLookup(nil, "b.txt"))
|
|
ops = append(ops, nfs4.AppendRestorefh(nil))
|
|
ops = append(ops, nfs4.AppendGetfh(nil))
|
|
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 6 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
fh, err := xdr.NewDecoder(bodies[5]).VarOpaque()
|
|
if err != nil || len(fh) == 0 {
|
|
t.Fatalf("restored handle: %v", err)
|
|
}
|
|
info, err := h.FS.Getattr(fh)
|
|
if err != nil || !info.IsDir() {
|
|
t.Fatalf("the restored handle is not sub: %v, %v", info, err)
|
|
}
|
|
}
|
|
|
|
func TestAccessAsRootAndOther(t *testing.T) {
|
|
h := testTree(t)
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendAccess(nil, nfs4.AccessRead|nfs4.AccessModify|nfs4.AccessExec),
|
|
}
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("status %d", res.Status)
|
|
}
|
|
d := xdr.NewDecoder(bodies[2])
|
|
supported, err := d.Uint32()
|
|
granted, err := d.Uint32()
|
|
if err != nil || supported != supportedAccess {
|
|
t.Fatalf("supported mask %x, %v", supported, err)
|
|
}
|
|
if granted&(nfs4.AccessRead|nfs4.AccessModify) == 0 {
|
|
t.Fatalf("root was denied read or modify: %x", granted)
|
|
}
|
|
|
|
// The same call under an unrelated identity loses modify on 0644.
|
|
// The compound runs in its own session, built for the other identity.
|
|
sid, seq := newSession(t, h)
|
|
otherOps := append([][]byte{
|
|
nfs4.AppendSequenceArgs(nil, sid, seq, 0, defaultSlots-1, true),
|
|
}, ops...)
|
|
payload := nfs4.AppendCompoundArgs(nil, "test", nfs4.MinorVersion, otherOps)
|
|
body, ok := h.compound(payload, cred{uid: 60000, gid: 60000})
|
|
if !ok {
|
|
t.Fatal("garbage")
|
|
}
|
|
res, bodies, err = nfs4.DecodeCompoundResBodies(body)
|
|
if err != nil || res.Status != nfs4.ErrOK {
|
|
t.Fatalf("other: %d, %v", res.Status, err)
|
|
}
|
|
// The results carry the SEQUENCE first; the ACCESS body is the fourth.
|
|
d = xdr.NewDecoder(bodies[3])
|
|
_, _ = d.Uint32()
|
|
granted, err = d.Uint32()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if granted&nfs4.AccessModify != 0 || granted&nfs4.AccessRead == 0 {
|
|
t.Fatalf("an other access of %x on a 0644 file", granted)
|
|
}
|
|
}
|
|
|
|
func TestReaddirRoot(t *testing.T) {
|
|
h := testTree(t)
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendReaddir(nil, 0, [8]byte{}, 1<<16, 1<<16,
|
|
nfs4.OfBits(nfs4.AttrType, nfs4.AttrSize)),
|
|
}
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("status %d", res.Status)
|
|
}
|
|
_, entries, eof, err := nfs4.DecodeReadDirBody(bodies[1])
|
|
if err != nil || !eof {
|
|
t.Fatalf("listing: eof %v, %v", eof, err)
|
|
}
|
|
var names []string
|
|
for _, e := range entries {
|
|
names = append(names, e.Name)
|
|
}
|
|
want := []string{"a.txt", "link", "sub"}
|
|
if len(names) != len(want) {
|
|
t.Fatalf("listing %v, want %v", names, want)
|
|
}
|
|
for i := range want {
|
|
if names[i] != want[i] {
|
|
t.Fatalf("listing %v, want %v", names, want)
|
|
}
|
|
}
|
|
if entries[0].Attrs.Type != nfs4.NF4Reg || entries[1].Attrs.Type != nfs4.NF4Lnk ||
|
|
entries[2].Attrs.Type != nfs4.NF4Dir {
|
|
t.Fatalf("entry types %d %d %d", entries[0].Attrs.Type,
|
|
entries[1].Attrs.Type, entries[2].Attrs.Type)
|
|
}
|
|
}
|
|
|
|
func TestReaddirSeesFifo(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := syscall.Mkfifo(filepath.Join(root, "pipe"), 0o644); err != nil {
|
|
t.Skipf("mkfifo: %v", err)
|
|
}
|
|
backend, err := nfsfs.NewLocal(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := &Handler{FS: backend}
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendReaddir(nil, 0, [8]byte{}, 0, 1<<16, nfs4.OfBits(nfs4.AttrType)),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("status %d", res.Status)
|
|
}
|
|
_, entries, _, err := nfs4.DecodeReadDirBody(bodies[1])
|
|
if err != nil || len(entries) != 1 || entries[0].Attrs.Type != nfs4.NF4Fifo {
|
|
t.Fatalf("fifo entry: %+v, %v", entries, err)
|
|
}
|
|
}
|
|
|
|
func TestGarbageArguments(t *testing.T) {
|
|
h := testTree(t)
|
|
// A tag, a minor version, a count of two and one operation: the second
|
|
// operation cannot be read.
|
|
payload := nfs4.AppendCompoundArgs(nil, "x", nfs4.MinorVersion, [][]byte{nfs4.AppendPutRootfh(nil)})
|
|
payload = payload[:len(payload)-4] // the count already says two
|
|
if _, ok := h.compound(payload, cred{}); ok {
|
|
t.Fatal("truncated arguments were not treated as garbage")
|
|
}
|
|
}
|
|
|
|
func TestWriteOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendWriteArgs(nil, nfs4.AllZero, 6, nfs4.StableUnstable, []byte("NFS")),
|
|
}
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 3 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
count, committed, verf, err := nfs4.DecodeWriteRes(bodies[2])
|
|
if err != nil || count != 3 || committed != nfs4.StableFileSync {
|
|
t.Fatalf("write res: %d %d, %v", count, committed, err)
|
|
}
|
|
if verf == ([8]byte{}) {
|
|
t.Fatal("the write verifier is zero")
|
|
}
|
|
// The data landed on the disk of the backend, not in some buffer.
|
|
got, err := h.FS.Read(mustLookup(t, h, "a.txt"), 0, 64)
|
|
if err != nil || string(got) != "hello NFS" {
|
|
t.Fatalf("after write: %q, %v", got, err)
|
|
}
|
|
// The boot verifier survives across calls and COMPOUNDs.
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
_, _, verf2, err := nfs4.DecodeWriteRes(bodies[2])
|
|
if err != nil || verf2 != verf {
|
|
t.Fatalf("the verifier changed between calls: %x vs %x", verf, verf2)
|
|
}
|
|
}
|
|
|
|
// a readOnlyFS hides the Writer of the backend behind the read only
|
|
// interface, the way a backend that cannot write would.
|
|
type readOnlyFS struct {
|
|
nfsfs.FS
|
|
}
|
|
|
|
func TestWriteAndCreateOnReadOnlyBackend(t *testing.T) {
|
|
h := testTree(t)
|
|
h2 := &Handler{FS: readOnlyFS{FS: h.FS}}
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendWriteArgs(nil, nfs4.AllZero, 0, nfs4.StableFileSync, []byte("x")),
|
|
}
|
|
res, _ := compoundOps(t, h2, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "write on read only", res.Status, nfs4.ErrROFS)
|
|
|
|
ops = [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendCreateArgs(nil, nfs4.NF4Dir, "d", "", 0, 0,
|
|
0o755),
|
|
}
|
|
res, _ = compoundOps(t, h2, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "create on read only", res.Status, nfs4.ErrROFS)
|
|
}
|
|
|
|
func TestCreateOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
// CREATE leaves the new object as the current handle, so the attributes
|
|
// that follow the creation are its own.
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendCreateArgs(nil, nfs4.NF4Dir, "newdir", "", 0, 0,
|
|
0o750),
|
|
nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType, nfs4.AttrMode)),
|
|
}
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 3 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
_, attrs, err := nfs4.DecodeGetattrBody(bodies[2])
|
|
if err != nil || attrs.Type != nfs4.NF4Dir || attrs.Mode != 0o750 {
|
|
t.Fatalf("the created dir: %+v, %v", attrs, err)
|
|
}
|
|
// The object is reachable from the root under its name.
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "newdir"),
|
|
nfs4.AppendGetfh(nil),
|
|
nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType)),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("lookup of the created dir: status %d", res.Status)
|
|
}
|
|
|
|
// A symlink carries its target and reports as NF4LNK.
|
|
ops = [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendCreateArgs(nil, nfs4.NF4Lnk, "zlink", "a.txt", 0, 0,
|
|
0o644),
|
|
nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType)),
|
|
}
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("symlink status %d", res.Status)
|
|
}
|
|
_, attrs, err = nfs4.DecodeGetattrBody(bodies[2])
|
|
if err != nil || attrs.Type != nfs4.NF4Lnk {
|
|
t.Fatalf("the created symlink: %+v, %v", attrs, err)
|
|
}
|
|
|
|
// An existing target is NFS4ERR_EXIST.
|
|
ops = [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendCreateArgs(nil, nfs4.NF4Dir, "newdir", "", 0, 0,
|
|
0o755),
|
|
}
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "create existing", res.Status, nfs4.ErrExist)
|
|
|
|
// CREATE leaves the new object as the current handle, checked from the
|
|
// outside with a fifo of a known type.
|
|
ops = [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendCreateArgs(nil, nfs4.NF4Fifo, "probe.fifo", "", 0, 0,
|
|
0o644),
|
|
nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType)),
|
|
}
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("fifo status %d", res.Status)
|
|
}
|
|
_, attrs, err = nfs4.DecodeGetattrBody(bodies[2])
|
|
if err != nil || attrs.Type != nfs4.NF4Fifo {
|
|
t.Fatalf("the created fifo: %+v, %v", attrs, err)
|
|
}
|
|
}
|
|
|
|
func mustLookup(t *testing.T, h *Handler, name string) nfsfs.Handle {
|
|
t.Helper()
|
|
root, err := h.FS.Root()
|
|
if err != nil {
|
|
t.Fatalf("root: %v", err)
|
|
}
|
|
child, _, err := h.FS.Lookup(root, name)
|
|
if err != nil {
|
|
t.Fatalf("lookup %s: %v", name, err)
|
|
}
|
|
return child
|
|
}
|
|
|
|
func TestUnknownBackendErrorMapsToServerFault(t *testing.T) {
|
|
if got := mapErr(bytes.ErrTooLarge); got != nfs4.ErrServerFault {
|
|
t.Fatalf("an unknown error mapped to %d", got)
|
|
}
|
|
for _, tc := range []struct {
|
|
err error
|
|
want uint32
|
|
}{
|
|
{nfsfs.ErrNoEnt, nfs4.ErrNoEnt},
|
|
{nfsfs.ErrNotDir, nfs4.ErrNotDir},
|
|
{nfsfs.ErrIsDir, nfs4.ErrIsDir},
|
|
{nfsfs.ErrStale, nfs4.ErrStale},
|
|
{nfsfs.ErrNameTooLong, nfs4.ErrNameTooLong},
|
|
{nfsfs.ErrBadName, nfs4.ErrBadName},
|
|
{nfsfs.ErrPermission, nfs4.ErrAccess},
|
|
{nfsfs.ErrIO, nfs4.ErrIO},
|
|
} {
|
|
if got := mapErr(tc.err); got != tc.want {
|
|
t.Fatalf("%v mapped to %d, want %d", tc.err, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleConnOverPipe(t *testing.T) {
|
|
h := testTree(t)
|
|
serverConn, clientConn := net.Pipe()
|
|
done := make(chan struct{})
|
|
go func() {
|
|
h.HandleConn(serverConn)
|
|
close(done)
|
|
}()
|
|
|
|
credBody, err := (rpc.AuthSys{Machine: "probe", UID: 0, GID: 0}).Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sysCall := rpc.Call{Program: nfs4.Program, Version: nfs4.Version,
|
|
Cred: rpc.Auth{Flavor: rpc.FlavorSys, Body: credBody}}
|
|
|
|
// NULL: answered with success and the connection stays.
|
|
sysCall.XID, sysCall.Procedure = 1, nfs4.ProcNull
|
|
rec, err := rpc.AppendCall(nil, sysCall)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := rpc.WriteRecord(clientConn, rec); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rep, err := rpc.ReadRecord(clientConn, 1<<20)
|
|
if err != nil {
|
|
t.Fatalf("null: %v", err)
|
|
}
|
|
reply, err := rpc.DecodeReply(rep)
|
|
if err != nil || reply.Status != rpc.AcceptSuccess || reply.XID != 1 {
|
|
t.Fatalf("null reply: %d, %v", reply.Status, err)
|
|
}
|
|
|
|
// An unknown program is answered PROG_UNAVAIL and the session lives.
|
|
sysCall.XID, sysCall.Program = 2, 99999
|
|
rec, _ = rpc.AppendCall(nil, sysCall)
|
|
if err := rpc.WriteRecord(clientConn, rec); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rep, _ = rpc.ReadRecord(clientConn, 1<<20)
|
|
reply, err = rpc.DecodeReply(rep)
|
|
if err != nil || reply.Status != rpc.AcceptProgUnavail {
|
|
t.Fatalf("unknown program: %d, %v", reply.Status, err)
|
|
}
|
|
|
|
// An unknown procedure is answered PROC_UNAVAIL.
|
|
sysCall.XID, sysCall.Program, sysCall.Procedure = 3, nfs4.Program, 999
|
|
rec, _ = rpc.AppendCall(nil, sysCall)
|
|
if err := rpc.WriteRecord(clientConn, rec); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rep, _ = rpc.ReadRecord(clientConn, 1<<20)
|
|
reply, err = rpc.DecodeReply(rep)
|
|
if err != nil || reply.Status != rpc.AcceptProcUnavail {
|
|
t.Fatalf("unknown procedure: %d, %v", reply.Status, err)
|
|
}
|
|
|
|
// A record that is no ONC RPC call ends the session.
|
|
if err := rpc.WriteRecord(clientConn, []byte{0xde, 0xad}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := rpc.ReadRecord(clientConn, 1<<20); err == nil {
|
|
t.Fatal("the connection survived a malformed call")
|
|
}
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("HandleConn did not return")
|
|
}
|
|
}
|
|
|
|
func TestReadDirTooSmallAndWrongVerifier(t *testing.T) {
|
|
h := testTree(t)
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendReaddir(nil, 0, [8]byte{}, 1<<16, 512,
|
|
nfs4.OfBits(nfs4.AttrType)),
|
|
})
|
|
wantStatus(t, "too small", res.Status, nfs4.ErrTooSmall)
|
|
if len(bodies[1]) != 0 {
|
|
t.Fatal("a failed readdir carried a body")
|
|
}
|
|
|
|
// A first page earns the verifier a later call must echo. The root
|
|
// verifier is the directory mtime in nanoseconds, never zero here.
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendReaddir(nil, 1, [8]byte{}, 1<<16, 1<<16,
|
|
nfs4.OfBits(nfs4.AttrType)),
|
|
})
|
|
wantStatus(t, "wrong verifier", res.Status, nfs4.ErrNotSame)
|
|
}
|
|
|
|
func TestReaddirPagingUnderBudget(t *testing.T) {
|
|
// Ten entries and a maxcount that takes only some of them: the page
|
|
// ends early, reports that, and the client resumes from the last
|
|
// cookie with the verifier it earned.
|
|
root := t.TempDir()
|
|
for i := range 100 {
|
|
if err := os.WriteFile(filepath.Join(root, fmt.Sprintf("f%03d", i)), []byte("x"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
backend, err := nfsfs.NewLocal(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := &Handler{FS: backend}
|
|
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendReaddir(nil, 0, [8]byte{}, 0, 1024,
|
|
nfs4.OfBits(nfs4.AttrType)),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("first page status %d", res.Status)
|
|
}
|
|
verifier, first, eof, err := nfs4.DecodeReadDirBody(bodies[1])
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if eof || len(first) == 0 || len(first) == 100 {
|
|
t.Fatalf("first page: %d entries, eof %v", len(first), eof)
|
|
}
|
|
|
|
// Resume from the cookie of the last entry; the same verifier is
|
|
// accepted and the rest arrives.
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendReaddir(nil, first[len(first)-1].Cookie, verifier, 0, 1<<16,
|
|
nfs4.OfBits(nfs4.AttrType)),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("second page status %d", res.Status)
|
|
}
|
|
_, second, eof2, err := nfs4.DecodeReadDirBody(bodies[1])
|
|
if err != nil || !eof2 {
|
|
t.Fatalf("second page: eof %v, %v", eof2, err)
|
|
}
|
|
if len(first)+len(second) != 100 {
|
|
t.Fatalf("%d entries over two pages, want 100", len(first)+len(second))
|
|
}
|
|
}
|
|
|
|
func TestReadOnDirectory(t *testing.T) {
|
|
h := testTree(t)
|
|
res, _ := compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "sub"),
|
|
nfs4.AppendRead(nil, nfs4.AllZero, 0, 64),
|
|
})
|
|
wantStatus(t, "read a directory", res.Status, nfs4.ErrIsDir)
|
|
}
|
|
|
|
func TestDecodeCred(t *testing.T) {
|
|
// A well formed AUTH_SYS claim arrives as the identity it carries.
|
|
body, err := (rpc.AuthSys{UID: 5, GID: 6, GIDs: []uint32{6, 7}}).Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := decodeCred(rpc.Auth{Flavor: rpc.FlavorSys, Body: body}); got.uid != 5 || got.gid != 6 || len(got.groups) != 2 {
|
|
t.Fatalf("sys cred: %+v", got)
|
|
}
|
|
// A broken claim and a flavourless credential both become nobody.
|
|
if got := decodeCred(rpc.Auth{Flavor: rpc.FlavorSys, Body: []byte{1}}); got.uid != 0xffffffff {
|
|
t.Fatalf("a broken sys cred became %+v", got)
|
|
}
|
|
if got := decodeCred(rpc.AuthNull); got.uid != 0xffffffff || got.gid != 0xffffffff {
|
|
t.Fatalf("a null cred became %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestWriteAndCreateGarbageAndErrors(t *testing.T) {
|
|
h := testTree(t)
|
|
|
|
// A WRITE whose stateid is cut short is garbage, not a status. The
|
|
// compound runs inside a session, so the WRITE is reached at all.
|
|
sid, seq := newSession(t, h)
|
|
ops := [][]byte{
|
|
nfs4.AppendSequenceArgs(nil, sid, seq, 0, defaultSlots-1, true),
|
|
nfs4.AppendPutRootfh(nil),
|
|
append(append([]byte{}, nfs4.AppendWriteArgs(nil, nfs4.AllZero, 0, 0, nil)[:8]...), 0, 0),
|
|
}
|
|
payload := nfs4.AppendCompoundArgs(nil, "x", nfs4.MinorVersion, ops)
|
|
if _, ok := h.compound(payload, cred{}); ok {
|
|
t.Fatal("a truncated WRITE was not garbage")
|
|
}
|
|
|
|
// A truncated CREATE attribute list is garbage, not a status.
|
|
sid, seq2 := newSession(t, h)
|
|
create := nfs4.AppendCreateArgs(nil, nfs4.NF4Dir, "d", "", 0, 0, 0o755)
|
|
ops = [][]byte{
|
|
nfs4.AppendSequenceArgs(nil, sid, seq2, 0, defaultSlots-1, true),
|
|
nfs4.AppendPutRootfh(nil),
|
|
create[:len(create)-1],
|
|
}
|
|
payload = nfs4.AppendCompoundArgs(nil, "x", nfs4.MinorVersion, ops)
|
|
if _, ok := h.compound(payload, cred{}); ok {
|
|
t.Fatal("a truncated CREATE was not garbage")
|
|
}
|
|
|
|
// A CREATE over a fresh name succeeds; the same name again answers
|
|
// EXIST.
|
|
ops = [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendCreateArgs(nil, nfs4.NF4Fifo, "ex.fifo", "", 0, 0, 0o644),
|
|
}
|
|
res, _ := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "create", res.Status, nfs4.ErrOK)
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "create again", res.Status, nfs4.ErrExist)
|
|
|
|
// A block device needs privileges this process may not have; the
|
|
// failure is permission or success, never a crash.
|
|
ops = [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendCreateArgs(nil, nfs4.NF4Blk, "dev", "", 1, 3,
|
|
0o644),
|
|
}
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK && res.Status != nfs4.ErrAccess && res.Status != nfs4.ErrPerm && res.Status != nfs4.ErrIO {
|
|
t.Fatalf("block device create: status %d", res.Status)
|
|
}
|
|
}
|
|
|
|
func TestRemoveOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendRemoveArgs(nil, "a.txt"),
|
|
}
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 2 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
if len(bodies[1]) != 20 {
|
|
t.Fatalf("the change info carries %d bytes, want 20", len(bodies[1]))
|
|
}
|
|
// The name is gone: the next LOOKUP reports it.
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
})
|
|
wantStatus(t, "lookup after remove", res.Status, nfs4.ErrNoEnt)
|
|
}
|
|
|
|
func TestRenameOverCompoundWithSavefh(t *testing.T) {
|
|
h := testTree(t)
|
|
|
|
// The canonical shape: the source directory goes into the saved
|
|
// handle, the target directory into the current one.
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil), // current = root
|
|
nfs4.AppendSavefh(nil), // saved = root
|
|
nfs4.AppendLookup(nil, "sub"), // current = sub
|
|
nfs4.AppendRenameArgs(nil, "a.txt", "moved.txt"),
|
|
}
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 4 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
if len(bodies[3]) != 40 {
|
|
t.Fatalf("two change infos carry %d bytes, want 40", len(bodies[3]))
|
|
}
|
|
// The file lives under the new directory and under no other.
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
})
|
|
wantStatus(t, "old name", res.Status, nfs4.ErrNoEnt)
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "sub"),
|
|
nfs4.AppendLookup(nil, "moved.txt"),
|
|
nfs4.AppendGetfh(nil),
|
|
nfs4.AppendRead(nil, nfs4.AllZero, 0, 64),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("new name status %d", res.Status)
|
|
}
|
|
eof, data, err := nfs4.DecodeReadBody(bodies[4])
|
|
if err != nil || !eof || string(data) != "hello nfs" {
|
|
t.Fatalf("moved content: %q, %v", data, err)
|
|
}
|
|
|
|
// Without SAVEFH there is no saved handle and RENAME refuses.
|
|
ops = [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendRenameArgs(nil, "a.txt", "b.txt"),
|
|
}
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "rename without savefh", res.Ops[1].Status, nfs4.ErrNoFileHandle)
|
|
|
|
// A rename of a missing source is NFS4ERR_NOENT.
|
|
ops = [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendSavefh(nil),
|
|
nfs4.AppendRenameArgs(nil, "missing", "x"),
|
|
}
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "rename missing", res.Status, nfs4.ErrNoEnt)
|
|
|
|
// REMOVE refuses to take a full directory.
|
|
ops = [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendRemoveArgs(nil, "sub"),
|
|
}
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "remove full dir", res.Status, nfs4.ErrNotEmpty)
|
|
}
|
|
|
|
func TestSetattrOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendSetattrArgs(nil, nfs4.AllZero,
|
|
nfs4.OfBits(nfs4.AttrMode, nfs4.AttrSize),
|
|
nfs4.Attrs{Mode: 0o600, Size: 4}),
|
|
}
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 3 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
attrsset, err := nfs4.ReadBitmap(xdr.NewDecoder(bodies[2]))
|
|
if err != nil {
|
|
t.Fatalf("attrsset: %v", err)
|
|
}
|
|
if !attrsset.Has(nfs4.AttrMode) || !attrsset.Has(nfs4.AttrSize) {
|
|
t.Fatalf("attrsset misses the changes: %v", attrsset.Words())
|
|
}
|
|
// The backend carries the result.
|
|
info, err := h.FS.Getattr(mustLookup(t, h, "a.txt"))
|
|
if err != nil || info.Size != 4 || info.Mode.Perm() != 0o600 {
|
|
t.Fatalf("after setattr: %d bytes, mode %o, %v", info.Size, info.Mode.Perm(), err)
|
|
}
|
|
|
|
// A SETATTR naming an attribute the server does not set is
|
|
// NFS4ERR_ATTRNOTSUPP, not a silent half answer. RAWDEV carries no
|
|
// value in Attrs, so it is built by hand: a bitmap naming it and an
|
|
// eight byte specdata value inside the attribute list.
|
|
attrlist := append(xdr.AppendUint32(nil, 0), xdr.AppendUint32(nil, 0)...)
|
|
fattr := xdr.AppendVarOpaque(nfs4.OfBits(nfs4.AttrRawDev).AppendTo(nil), attrlist)
|
|
rawdevArg := append(xdr.AppendUint32(nil, nfs4.OpSetattr), nfs4.AllZero[:]...)
|
|
rawdevArg = append(rawdevArg, fattr...)
|
|
ops = [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
rawdevArg,
|
|
}
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "setattr rawdev", res.Status, nfs4.ErrAttrNotSupp)
|
|
}
|
|
|
|
func TestLinkOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
// The saved handle is the object, the current one the directory.
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendSavefh(nil),
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLinkArgs(nil, "linked.txt"),
|
|
nfs4.AppendRestorefh(nil),
|
|
nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrNumlinks)),
|
|
}
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 7 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
_, attrs, err := nfs4.DecodeGetattrBody(bodies[6])
|
|
if err != nil || attrs.Numlinks != 2 {
|
|
t.Fatalf("link count: %d, %v", attrs.Numlinks, err)
|
|
}
|
|
// The link reads the same content from the root directory.
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "linked.txt"),
|
|
nfs4.AppendRead(nil, nfs4.AllZero, 0, 64),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("read of the link: status %d", res.Status)
|
|
}
|
|
_, data, err := nfs4.DecodeReadBody(bodies[2])
|
|
if err != nil || string(data) != "hello nfs" {
|
|
t.Fatalf("through the link: %q, %v", data, err)
|
|
}
|
|
|
|
// Without SAVEFH the LINK refuses.
|
|
ops = [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLinkArgs(nil, "x"),
|
|
}
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "link without savefh", res.Ops[1].Status, nfs4.ErrNoFileHandle)
|
|
}
|
|
|
|
func TestSetattrTimesOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendSetattrArgs(nil, nfs4.AllZero,
|
|
nfs4.OfBits(nfs4.AttrTimeAccessSet, nfs4.AttrTimeModifySet),
|
|
nfs4.Attrs{}),
|
|
}
|
|
// The Attrs shape cannot carry the settime4 encoding, so the argop
|
|
// comes from the structured encoder: access = server time, modify =
|
|
// explicit time 1e9.
|
|
ops[2] = nfs4.AppendSetattrArgsUpdates(nil, nfs4.AllZero, nfs4.SetAttrUpdates{
|
|
Atime: &nfs4.NfsTimeSet{Server: true},
|
|
Mtime: &nfs4.NfsTimeSet{Time: nfs4.NfsTime{Seconds: 1_000_000_000, Nseconds: 1}},
|
|
})
|
|
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 3 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
attrsset, err := nfs4.ReadBitmap(xdr.NewDecoder(bodies[2]))
|
|
if err != nil || !attrsset.Has(nfs4.AttrTimeAccessSet) || !attrsset.Has(nfs4.AttrTimeModifySet) {
|
|
t.Fatalf("attrsset misses the times: %v, %v", attrsset.Words(), err)
|
|
}
|
|
}
|
|
|
|
func TestReadlinkAndCommitOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
|
|
// READLINK on the symlink of the fixture reports its target.
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "link"),
|
|
nfs4.AppendReadlinkArgs(nil),
|
|
})
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 3 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
target, err := xdr.NewDecoder(bodies[2]).String()
|
|
if err != nil || target != "a.txt" {
|
|
t.Fatalf("readlink: %q, %v", target, err)
|
|
}
|
|
|
|
// READLINK on a regular file is NFS4ERR_INVAL.
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendReadlinkArgs(nil),
|
|
})
|
|
wantStatus(t, "readlink of a file", res.Status, nfs4.ErrInval)
|
|
|
|
// COMMIT on a file answers with the boot verifier, and the verifier is
|
|
// stable across calls.
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendCommitArgs(nil, 0, 0, [8]byte{}),
|
|
}
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 3 {
|
|
t.Fatalf("commit: status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
verf1, err := xdr.NewDecoder(bodies[2]).Raw(8)
|
|
if err != nil {
|
|
t.Fatalf("commit verifier: %v", err)
|
|
}
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
verf2, err := xdr.NewDecoder(bodies[2]).Raw(8)
|
|
if err != nil || string(verf1) != string(verf2) {
|
|
t.Fatalf("the commit verifier changed: %x vs %x", verf1, verf2)
|
|
}
|
|
|
|
// COMMIT on a read only backend is NFS4ERR_ROFS.
|
|
h2 := &Handler{FS: readOnlyFS{FS: h.FS}}
|
|
res, _ = compoundOps(t, h2, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "commit on read only", res.Status, nfs4.ErrROFS)
|
|
}
|
|
|
|
func TestSecinfoOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
|
|
// SECINFO for an explicit name answers AUTH_SYS.
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendSecinfoArgs(nil, "a.txt"),
|
|
})
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 2 {
|
|
t.Fatalf("status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
entries, err := nfs4.DecodeSecinfoRes(bodies[1])
|
|
if err != nil || len(entries) != 1 || entries[0].Flavor != nfs4.SecFlavorSys {
|
|
t.Fatalf("secinfo: %+v, %v", entries, err)
|
|
}
|
|
|
|
// Even a name that does not exist is answered: probing security is the
|
|
// purpose of the operation.
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendSecinfoArgs(nil, "missing"),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("secinfo of a missing name: status %d", res.Status)
|
|
}
|
|
|
|
// SECINFO_NO_NAME with the current file handle: no LOOKUP needed,
|
|
// the argument is the style enum alone, RFC 8881 section 18.44.
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendSecinfoNoNameArgs(nil, nfs4.StyleCurrentFH),
|
|
})
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 2 {
|
|
t.Fatalf("current handle: status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
entries, err = nfs4.DecodeSecinfoRes(bodies[1])
|
|
if err != nil || len(entries) != 1 || entries[0].Flavor != nfs4.SecFlavorSys {
|
|
t.Fatalf("current handle secinfo: %+v, %v", entries, err)
|
|
}
|
|
|
|
// The parent style names the parent of the current handle; no name
|
|
// rides the wire there either.
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendSecinfoNoNameArgs(nil, nfs4.StyleParent),
|
|
})
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 3 {
|
|
t.Fatalf("parent style: status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
entries, err = nfs4.DecodeSecinfoRes(bodies[2])
|
|
if err != nil || len(entries) != 1 {
|
|
t.Fatalf("parent secinfo: %+v, %v", entries, err)
|
|
}
|
|
|
|
// An unknown style is NFS4ERR_INVAL.
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendSecinfoNoNameArgs(nil, 99),
|
|
})
|
|
wantStatus(t, "unknown style", res.Status, nfs4.ErrInval)
|
|
}
|
|
|
|
func TestSessionReplayAndMisorder(t *testing.T) {
|
|
h := testTree(t)
|
|
sid, seq := newSession(t, h)
|
|
|
|
mk := func(s uint32) []byte {
|
|
ops := [][]byte{
|
|
nfs4.AppendSequenceArgs(nil, sid, s, 0, defaultSlots-1, true),
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType)),
|
|
}
|
|
return nfs4.AppendCompoundArgs(nil, "replay", nfs4.MinorVersion, ops)
|
|
}
|
|
|
|
// The first run executes and its answer is cached.
|
|
first, ok := h.compound(mk(seq), cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("first run was garbage")
|
|
}
|
|
// The same sequence replays the very same bytes.
|
|
second, ok := h.compound(mk(seq), cred{uid: 0, gid: 0})
|
|
if !ok || string(first) != string(second) {
|
|
t.Fatal("a repeated sequence did not replay the cached answer")
|
|
}
|
|
// A skipped sequence is misordered.
|
|
skipped, ok := h.compound(mk(seq+7), cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("misordered treated as garbage")
|
|
}
|
|
res, _, err := nfs4.DecodeCompoundResBodies(skipped)
|
|
if err != nil || res.Status != nfs4.ErrSeqMisordered {
|
|
t.Fatalf("a skipped sequence: status %d, %v", res.Status, err)
|
|
}
|
|
|
|
// DESTROY_SESSION closes the session; SEQUENCE on it is BADSESSION.
|
|
destroy := nfs4.AppendCompoundArgs(nil, "destroy", nfs4.MinorVersion,
|
|
[][]byte{nfs4.AppendDestroySessionArgs(nil, sid)})
|
|
res, _, err = nfs4.DecodeCompoundResBodies(mustCompound(t, h, destroy))
|
|
if err != nil || res.Status != nfs4.ErrOK {
|
|
t.Fatalf("destroy: status %d, %v", res.Status, err)
|
|
}
|
|
dead, ok := h.compound(mk(seq+1), cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("dead session treated as garbage")
|
|
}
|
|
res, _, err = nfs4.DecodeCompoundResBodies(dead)
|
|
if err != nil || res.Status != nfs4.ErrBadSession {
|
|
t.Fatalf("sequence on a destroyed session: status %d, %v", res.Status, err)
|
|
}
|
|
}
|
|
|
|
func TestOpsWithoutSessionAreRefused(t *testing.T) {
|
|
h := testTree(t)
|
|
payload := nfs4.AppendCompoundArgs(nil, "nosess", nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
})
|
|
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("garbage")
|
|
}
|
|
res, _, err := nfs4.DecodeCompoundResBodies(body)
|
|
if err != nil || res.Status != nfs4.ErrOpNotInSession {
|
|
t.Fatalf("a stateless call inside 4.2: status %d, %v", res.Status, err)
|
|
}
|
|
}
|
|
|
|
func mustCompound(t *testing.T, h *Handler, payload []byte) []byte {
|
|
t.Helper()
|
|
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("garbage")
|
|
}
|
|
return body
|
|
}
|
|
|
|
func TestExchangeIDClientReboot(t *testing.T) {
|
|
h := testTree(t)
|
|
|
|
mk := func(v byte) []byte {
|
|
return nfs4.AppendExchangeIDArgs(nil, [8]byte{v, 1, 1, 1, 1, 1, 1, 1},
|
|
[]byte("same-owner"), 0)
|
|
}
|
|
// The first identity is assigned and confirmed on reuse.
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, [][]byte{mk(1)})
|
|
clientid1, err := xdr.NewDecoder(bodies[0]).Uint64()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, [][]byte{mk(1)})
|
|
rd := xdr.NewDecoder(bodies[0])
|
|
_, _ = rd.Uint64()
|
|
_, _ = rd.Uint32()
|
|
flags, err := rd.Uint32()
|
|
if err != nil || flags&nfs4.ExchgIDConfirmedR == 0 {
|
|
t.Fatalf("reuse without the confirmed flag: %x, %v", flags, err)
|
|
}
|
|
_ = res
|
|
|
|
// A new verifier for the same owner is a reboot: a new client id and
|
|
// no confirmed flag, and the sessions of the old id are gone.
|
|
sid, _, csStatus := h.sessions().createSession(clientid1, 0, 0)
|
|
if csStatus != nfs4.ErrOK {
|
|
t.Fatalf("create session for the confirmed client: %d", csStatus)
|
|
}
|
|
res, bodies = compoundOps(t, h, nfs4.MinorVersion, [][]byte{mk(2)})
|
|
clientid2, err := xdr.NewDecoder(bodies[0]).Uint64()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if clientid2 == clientid1 {
|
|
t.Fatal("a rebooted client kept its id")
|
|
}
|
|
rd = xdr.NewDecoder(bodies[0])
|
|
_, _ = rd.Uint64()
|
|
_, _ = rd.Uint32()
|
|
flags, err = rd.Uint32()
|
|
if err != nil || flags&nfs4.ExchgIDConfirmedR != 0 {
|
|
t.Fatalf("a new client id carries the confirmed flag: %x, %v", flags, err)
|
|
}
|
|
if _, _, status := h.sessions().sequence(sid, 1, 0); status != nfs4.ErrBadSession {
|
|
t.Fatalf("a session of a rebooted client: status %d", status)
|
|
}
|
|
}
|
|
|
|
func TestOpenCloseOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
sid, seq := newSession(t, h)
|
|
|
|
// Every COMPOUND opens with its own SEQUENCE over the same session.
|
|
run := func(t *testing.T, s uint32, ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
|
t.Helper()
|
|
all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, s, 0, defaultSlots-1, true)}, ops...)
|
|
payload := nfs4.AppendCompoundArgs(nil, "test", nfs4.MinorVersion, all)
|
|
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("well formed arguments were treated as garbage")
|
|
}
|
|
r, bodies, derr := nfs4.DecodeCompoundResBodies(body)
|
|
if derr != nil {
|
|
t.Fatalf("decode: %v", derr)
|
|
}
|
|
return r, bodies
|
|
}
|
|
|
|
// OPEN with create: the file springs into existence with a stateid.
|
|
open := nfs4.AppendOpenArgs(nil, 0x1234, []byte("owner-1"),
|
|
nfs4.ShareAccessBoth, 0, true, 0o640, "created.txt")
|
|
res, bodies := run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
open,
|
|
})
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 3 {
|
|
t.Fatalf("open: status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
st, err := xdr.NewDecoder(bodies[2]).Raw(16)
|
|
if err != nil || st[0] != 0 || st[3] != 1 {
|
|
t.Fatalf("open stateid: %x, %v", st, err)
|
|
}
|
|
var stateid nfs4.Stateid
|
|
copy(stateid[:], st)
|
|
|
|
// WRITE under the open stateid lands on the disk.
|
|
res, _ = run(t, seq+1, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "created.txt"),
|
|
nfs4.AppendWriteArgs(nil, stateid, 0, nfs4.StableFileSync, []byte("opened!")),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("write: status %d", res.Status)
|
|
}
|
|
|
|
// A conflicting OPEN is NFS4ERR_SHARE_DENIED: the first open asked
|
|
// both access modes with no deny, so a deny-write open collides.
|
|
conflict := nfs4.AppendOpenArgs(nil, 0x1234, []byte("owner-2"),
|
|
nfs4.ShareAccessRead, nfs4.ShareDenyWrite, false, 0, "created.txt")
|
|
res, _ = run(t, seq+2, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
conflict,
|
|
})
|
|
wantStatus(t, "share conflict", res.Status, nfs4.ErrShareDenied)
|
|
|
|
// CLOSE ends the state; the answer carries the dead stateid.
|
|
res, bodies = run(t, seq+3, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "created.txt"),
|
|
nfs4.AppendCloseArgs(nil, stateid),
|
|
})
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 4 {
|
|
t.Fatalf("close: status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
closed, cerr := xdr.NewDecoder(bodies[3]).Raw(16)
|
|
if cerr != nil || closed[3] != stateid[3]+1 {
|
|
t.Fatalf("closed stateid: %x, %v", closed, cerr)
|
|
}
|
|
|
|
// The dead stateid is OLD, not merely bad, when reused for a write.
|
|
res, _ = run(t, seq+4, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "created.txt"),
|
|
nfs4.AppendWriteArgs(nil, stateid, 0, nfs4.StableFileSync, []byte("x")),
|
|
})
|
|
wantStatus(t, "write after close", res.Status, nfs4.ErrOldStateid)
|
|
}
|
|
|
|
func TestOpenUnknownStateid(t *testing.T) {
|
|
h := testTree(t)
|
|
// A WRITE with a stateid nobody issued, on a file nobody opened.
|
|
var bogus nfs4.Stateid
|
|
bogus[0] = 1
|
|
bogus[4] = 0xaa
|
|
ops := [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendWriteArgs(nil, bogus, 0, nfs4.StableFileSync, []byte("x")),
|
|
}
|
|
res, _ := compoundOps(t, h, nfs4.MinorVersion, ops)
|
|
wantStatus(t, "bogus stateid", res.Status, nfs4.ErrBadStateid)
|
|
}
|
|
|
|
func TestCreateSessionReplay(t *testing.T) {
|
|
h := testTree(t)
|
|
clientid, _, _, _ := h.sessions().exchangeID([8]byte{9}, []byte("replay-owner"), time.Now())
|
|
id, _, status := h.sessions().createSession(clientid, 0, 0)
|
|
if status != nfs4.ErrOK {
|
|
t.Fatalf("first create: %d", status)
|
|
}
|
|
// The same sequence replays: the same session comes back.
|
|
id2, replay, status := h.sessions().createSession(clientid, 0, 0)
|
|
if status != nfs4.ErrOK || !replay || id2 != id {
|
|
t.Fatalf("replay: id %d replay %v status %d", id2, replay, status)
|
|
}
|
|
// A higher sequence replaces the slot table fresh.
|
|
_, replay, status = h.sessions().createSession(clientid, 1, 0)
|
|
if status != nfs4.ErrOK || replay {
|
|
t.Fatalf("second create: replay %v status %d", replay, status)
|
|
}
|
|
// A sequence behind the last one is misordered.
|
|
_, _, status = h.sessions().createSession(clientid, 0, 0)
|
|
if status != nfs4.ErrSeqMisordered {
|
|
t.Fatalf("old create sequence: %d", status)
|
|
}
|
|
// An unknown client id is stale.
|
|
if _, _, status = h.sessions().createSession(0xdead, 0, 0); status != nfs4.ErrStaleClientID {
|
|
t.Fatalf("unknown client: %d", status)
|
|
}
|
|
}
|
|
|
|
func TestLockLocktLockuOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
sid, seq := newSession(t, h)
|
|
|
|
run := func(t *testing.T, s uint32, ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
|
t.Helper()
|
|
all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, s, 0, defaultSlots-1, true)}, ops...)
|
|
payload := nfs4.AppendCompoundArgs(nil, "lock", nfs4.MinorVersion, all)
|
|
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("well formed arguments were treated as garbage")
|
|
}
|
|
r, bodies, derr := nfs4.DecodeCompoundResBodies(body)
|
|
if derr != nil {
|
|
t.Fatalf("decode: %v", derr)
|
|
}
|
|
return r, bodies
|
|
}
|
|
|
|
// Owner A opens the file and takes a write lock over the whole file.
|
|
open := nfs4.AppendOpenArgs(nil, 0x1111, []byte("owner-a"),
|
|
nfs4.ShareAccessBoth, 0, true, 0o644, "locked.txt")
|
|
res, bodies := run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
open,
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("open: status %d", res.Status)
|
|
}
|
|
var openStateid nfs4.Stateid
|
|
copy(openStateid[:], bodies[2])
|
|
|
|
seq++
|
|
lock := nfs4.AppendLockArgsNew(nil, openStateid, 0x1111, []byte("locker-a"),
|
|
nfs4.LockTypeWrite, false, 0, 0)
|
|
res, bodies = run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "locked.txt"),
|
|
lock,
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("lock: status %d", res.Status)
|
|
}
|
|
var lockStateid nfs4.Stateid
|
|
copy(lockStateid[:], bodies[3])
|
|
|
|
// Owner B probes the same range: LOCKT reports the write lock denied,
|
|
// naming owner A's client.
|
|
seq++
|
|
probe := nfs4.AppendLocktArgs(nil, nfs4.LockTypeWrite, 0, 100, 0x2222, []byte("locker-b"))
|
|
res, bodies = run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "locked.txt"),
|
|
probe,
|
|
})
|
|
if res.Status != nfs4.ErrDenied {
|
|
t.Fatalf("lockt: status %d", res.Status)
|
|
}
|
|
denied, err := nfs4.DecodeLocktResBody(res.Status, bodies[3])
|
|
// The lock is registered under the client of the session that took
|
|
// it, never under a clientid the wire alone claimed.
|
|
if err != nil || denied.ClientID != sid.ClientIDOf() || denied.LockType != nfs4.LockTypeWrite {
|
|
t.Fatalf("denied: %+v, %v", denied, err)
|
|
}
|
|
// A read probe is denied just the same against a write lock.
|
|
seq++
|
|
probe = nfs4.AppendLocktArgs(nil, nfs4.LockTypeRead, 0, 100, 0x2222, []byte("locker-b"))
|
|
res, _ = run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "locked.txt"),
|
|
probe,
|
|
})
|
|
if res.Status != nfs4.ErrDenied {
|
|
t.Fatalf("read probe: status %d", res.Status)
|
|
}
|
|
|
|
// Owner B's actual LOCK over the same range is denied too.
|
|
seq++
|
|
lockB := nfs4.AppendLockArgsNew(nil, openStateid, 0x2222, []byte("locker-b"),
|
|
nfs4.LockTypeWrite, false, 0, 100)
|
|
res, _ = run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "locked.txt"),
|
|
lockB,
|
|
})
|
|
if res.Status != nfs4.ErrDenied || len(res.Ops) != 4 {
|
|
t.Fatalf("lock of owner b: status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
|
|
// Owner A unlocks the first hundred bytes; a lock by B over that range
|
|
// then succeeds, while the tail stays held.
|
|
seq++
|
|
unlock := nfs4.AppendLockuArgs(nil, lockStateid, 0, 100)
|
|
res, bodies = run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "locked.txt"),
|
|
unlock,
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("unlock: status %d", res.Status)
|
|
}
|
|
var unlockedStateid nfs4.Stateid
|
|
copy(unlockedStateid[:], bodies[3])
|
|
if unlockedStateid[3] != lockStateid[3]+1 {
|
|
t.Fatalf("unlock seqid %d, want %d", unlockedStateid[3], lockStateid[3]+1)
|
|
}
|
|
seq++
|
|
lockB = nfs4.AppendLockArgsNew(nil, openStateid, 0x2222, []byte("locker-b"),
|
|
nfs4.LockTypeWrite, false, 0, 100)
|
|
res, _ = run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "locked.txt"),
|
|
lockB,
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("lock into the freed range: status %d", res.Status)
|
|
}
|
|
|
|
// CLOSE with locks still held on the tail is NFS4ERR_LOCKS_HELD.
|
|
seq++
|
|
res, _ = run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "locked.txt"),
|
|
nfs4.AppendCloseArgs(nil, openStateid),
|
|
})
|
|
wantStatus(t, "close with locks", res.Status, nfs4.ErrLocksHeld)
|
|
}
|
|
|
|
func TestOpenDowngradeOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
sid, seq := newSession(t, h)
|
|
run := func(t *testing.T, s uint32, ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
|
t.Helper()
|
|
all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, s, 0, defaultSlots-1, true)}, ops...)
|
|
payload := nfs4.AppendCompoundArgs(nil, "test", nfs4.MinorVersion, all)
|
|
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("garbage")
|
|
}
|
|
r, bodies, derr := nfs4.DecodeCompoundResBodies(body)
|
|
if derr != nil {
|
|
t.Fatalf("decode: %v", derr)
|
|
}
|
|
return r, bodies
|
|
}
|
|
|
|
// Open read-write, downgrade to read-only.
|
|
res, bodies := run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, 0x1111, []byte("owner-a"),
|
|
nfs4.ShareAccessBoth, 0, true, 0o644, "down.txt"),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("open: status %d", res.Status)
|
|
}
|
|
var st nfs4.Stateid
|
|
copy(st[:], bodies[2])
|
|
|
|
seq++
|
|
res, _ = run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "down.txt"),
|
|
nfs4.AppendOpenDowngradeArgs(nil, st, nfs4.ShareAccessRead, 0),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("downgrade: status %d", res.Status)
|
|
}
|
|
|
|
// The old stateid is now OLD; the write under it is refused.
|
|
seq++
|
|
res, _ = run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "down.txt"),
|
|
nfs4.AppendWriteArgs(nil, st, 0, nfs4.StableFileSync, []byte("x")),
|
|
})
|
|
wantStatus(t, "write with old stateid", res.Status, nfs4.ErrOldStateid)
|
|
|
|
// A new open of the same file with deny-write now succeeds, because
|
|
// the live open only holds read access.
|
|
seq++
|
|
res, _ = run(t, seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, 0x2222, []byte("owner-b"),
|
|
nfs4.ShareAccessWrite, nfs4.ShareDenyWrite, false, 0, "down.txt"),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("open after downgrade: status %d", res.Status)
|
|
}
|
|
}
|
|
|
|
func TestDestroyClientIDOverCompound(t *testing.T) {
|
|
h := testTree(t)
|
|
sid, _ := newSession(t, h)
|
|
clientid := sid.ClientIDOf()
|
|
|
|
// OPEN first, then DESTROY_CLIENTID: the state goes with the client.
|
|
res, _, err := nfs4.DecodeCompoundResBodies(mustCompound(t, h, nfs4.AppendCompoundArgs(nil, "open",
|
|
nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendSequenceArgs(nil, sid, 1, 0, defaultSlots-1, true),
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, clientid, []byte("o"),
|
|
nfs4.ShareAccessBoth, 0, true, 0o644, "f.txt"),
|
|
})))
|
|
if err != nil || res.Status != nfs4.ErrOK {
|
|
t.Fatalf("open: status %d, %v", res.Status, err)
|
|
}
|
|
|
|
res, _, err = nfs4.DecodeCompoundResBodies(mustCompound(t, h, nfs4.AppendCompoundArgs(nil, "destroy",
|
|
nfs4.MinorVersion, [][]byte{nfs4.AppendDestroyClientIDArgs(nil, clientid)})))
|
|
if err != nil || res.Status != nfs4.ErrOK {
|
|
t.Fatalf("destroy: status %d, %v", res.Status, err)
|
|
}
|
|
|
|
// SEQUENCE on the destroyed session is BADSESSION.
|
|
res, _, err = nfs4.DecodeCompoundResBodies(mustCompound(t, h, nfs4.AppendCompoundArgs(nil, "after",
|
|
nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendSequenceArgs(nil, sid, 2, 0, defaultSlots-1, true),
|
|
nfs4.AppendPutRootfh(nil),
|
|
})))
|
|
if err != nil || res.Status != nfs4.ErrBadSession {
|
|
t.Fatalf("sequence after destroy: status %d, %v", res.Status, err)
|
|
}
|
|
|
|
// DESTROY of an unknown client id is STALE_CLIENTID.
|
|
res, _, err = nfs4.DecodeCompoundResBodies(mustCompound(t, h, nfs4.AppendCompoundArgs(nil, "destroy2",
|
|
nfs4.MinorVersion, [][]byte{nfs4.AppendDestroyClientIDArgs(nil, 0xbeef)})))
|
|
if err != nil || res.Status != nfs4.ErrStaleClientID {
|
|
t.Fatalf("destroy unknown: status %d, %v", res.Status, err)
|
|
}
|
|
}
|
|
|
|
func TestReclaimComplete(t *testing.T) {
|
|
h := testTree(t)
|
|
sid, _ := newSession(t, h)
|
|
|
|
seq := uint32(1)
|
|
run := func(ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
|
r, bodies, derr := nfs4.DecodeCompoundResBodies(mustCompound(t, h, nfs4.AppendCompoundArgs(nil, "rc",
|
|
nfs4.MinorVersion, append([][]byte{
|
|
nfs4.AppendSequenceArgs(nil, sid, seq, 0, defaultSlots-1, true),
|
|
}, ops...))))
|
|
if derr != nil {
|
|
t.Fatalf("decode: %v", derr)
|
|
}
|
|
seq++
|
|
return r, bodies
|
|
}
|
|
res, _ := run([][]byte{nfs4.AppendReclaimCompleteArgs(nil, false)})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("reclaim complete: status %d", res.Status)
|
|
}
|
|
res, _ = run([][]byte{nfs4.AppendReclaimCompleteArgs(nil, false)})
|
|
if res.Status != nfs4.ErrCompleteAlready {
|
|
t.Fatalf("second reclaim complete: status %d", res.Status)
|
|
}
|
|
}
|
|
|
|
func TestLeaseExpiry(t *testing.T) {
|
|
s := newSessionStore([4]byte{})
|
|
v := [8]byte{1}
|
|
clientid, _, _, _ := s.exchangeID(v, []byte("lease-owner"), time.Now())
|
|
|
|
// A fresh client with a renewed lease is not expired.
|
|
now := time.Now()
|
|
s.renew(clientid, now)
|
|
if s.leaseExpired(clientid, 40*time.Millisecond, now.Add(20*time.Millisecond)) {
|
|
t.Fatal("a fresh lease expired")
|
|
}
|
|
// Past the period it is.
|
|
if !s.leaseExpired(clientid, 40*time.Millisecond, now.Add(60*time.Millisecond)) {
|
|
t.Fatal("a lapsed lease did not expire")
|
|
}
|
|
// A zero period disables enforcement entirely.
|
|
if s.leaseExpired(clientid, 0, now.Add(time.Hour)) {
|
|
t.Fatal("enforcement was not disabled")
|
|
}
|
|
}
|
|
|
|
func TestBackChannelProbe(t *testing.T) {
|
|
h := testTree(t)
|
|
root := t.TempDir()
|
|
backend, err := nfsfs.NewLocal(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h.FS = backend
|
|
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := &server.Server{Handle: h.HandleConn}
|
|
go srv.Serve(t.Context(), ln)
|
|
|
|
cl, err := nfsclient.Dial(ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer cl.Close()
|
|
if err := cl.Establish("cb-probe"); err != nil {
|
|
t.Fatalf("establish: %v", err)
|
|
}
|
|
sid, ok := cl.SessionID()
|
|
if !ok {
|
|
t.Fatal("no session after establish")
|
|
}
|
|
|
|
// The server delivers a CB_COMPOUND over the same wire and reads the
|
|
// answer the client's callback server produced. sendCB opens with its
|
|
// own CB_SEQUENCE.
|
|
probe := [][]byte{
|
|
nfs4.AppendCBRecallArgs(nil, nfs4.AllZero, false, []byte{1, 2, 3}),
|
|
}
|
|
res, bodies, err := h.SendCB(sid, "probe", probe)
|
|
if err != nil {
|
|
t.Fatalf("sendCB: %v", err)
|
|
}
|
|
if res.Status != nfs4.ErrOK || len(res.Ops) != 2 {
|
|
t.Fatalf("cb res: status %d ops %d", res.Status, len(res.Ops))
|
|
}
|
|
for i, op := range res.Ops {
|
|
if op.Status != nfs4.ErrOK {
|
|
t.Fatalf("cb op %d status %d", i, op.Status)
|
|
}
|
|
}
|
|
if len(bodies[1]) != 0 {
|
|
t.Fatalf("cb recall body: %x", bodies[1])
|
|
}
|
|
|
|
// A session created without a connection context has no back channel
|
|
// and sendCB refuses it.
|
|
otherID, _ := newSession(t, h)
|
|
if _, _, err := h.SendCB(otherID, "probe", probe); err == nil {
|
|
t.Fatal("a session without a back channel accepted a CB call")
|
|
}
|
|
}
|
|
|
|
func TestDestroyClientIDLocksAndSessions(t *testing.T) {
|
|
h := testTree(t)
|
|
h.LeasePeriod = time.Second
|
|
sid, _ := newSession(t, h)
|
|
clientid := sid.ClientIDOf()
|
|
|
|
// A second client on the same file: it survives the destroy and
|
|
// carries the proof that the state is really gone.
|
|
sid2, _ := newSession(t, h)
|
|
|
|
seq := uint32(1)
|
|
run := func(session nfs4.SessionID, ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
|
t.Helper()
|
|
all := append([][]byte{nfs4.AppendSequenceArgs(nil, session, seq, 0, defaultSlots-1, true)}, ops...)
|
|
seq++
|
|
payload := nfs4.AppendCompoundArgs(nil, "test", nfs4.MinorVersion, all)
|
|
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("garbage")
|
|
}
|
|
r, bodies, derr := nfs4.DecodeCompoundResBodies(body)
|
|
if derr != nil {
|
|
t.Fatalf("decode: %v", derr)
|
|
}
|
|
return r, bodies
|
|
}
|
|
|
|
// OPEN plus a lock: state the destroy has to take with the client.
|
|
res, bodies := run(sid, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, clientid, []byte("o"),
|
|
nfs4.ShareAccessBoth, 0, true, 0o644, "held.txt"),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("open: status %d", res.Status)
|
|
}
|
|
var openStateid nfs4.Stateid
|
|
copy(openStateid[:], bodies[2])
|
|
|
|
res, _ = run(sid, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "held.txt"),
|
|
nfs4.AppendLockArgsNew(nil, openStateid, clientid, []byte("lk"),
|
|
nfs4.LockTypeWrite, false, 0, 0),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("lock: status %d", res.Status)
|
|
}
|
|
|
|
// DESTROY_CLIENTID clears the sessions and the state.
|
|
t.Logf("pre-destroy deleg count=%d", len(h.delegs().byKey))
|
|
res, _ = run(sid, [][]byte{nfs4.AppendDestroyClientIDArgs(nil, clientid)})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("destroy: status %d", res.Status)
|
|
}
|
|
t.Logf("post-destroy deleg count=%d byID=%d", len(h.delegs().byKey), len(h.sessions().byID))
|
|
|
|
// SEQUENCE on the destroyed session is BADSESSION.
|
|
res, _ = run(sid, [][]byte{nfs4.AppendSequenceArgs(nil, sid, seq, 0, defaultSlots-1, true)})
|
|
wantStatus(t, "old session", res.Status, nfs4.ErrBadSession)
|
|
|
|
// A WRITE with the dead open stateid, carried by the surviving
|
|
// session, is BAD_STATEID: the state went with the client.
|
|
res, _ = run(sid2, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "held.txt"),
|
|
nfs4.AppendWriteArgs(nil, openStateid, 0, nfs4.StableFileSync, []byte("x")),
|
|
})
|
|
wantStatus(t, "write after destroy", res.Status, nfs4.ErrBadStateid)
|
|
}
|
|
|
|
func TestDelegationGrantAndRecall(t *testing.T) {
|
|
h := testTree(t)
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := &server.Server{Handle: h.HandleConn}
|
|
go srv.Serve(t.Context(), ln)
|
|
|
|
// Two clients: A opens read-write and is granted a write delegation.
|
|
ca, err := nfsclient.Dial(ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ca.Close()
|
|
if err := ca.Establish("client-a"); err != nil {
|
|
t.Fatalf("establish a: %v", err)
|
|
}
|
|
sidA, _ := ca.SessionID()
|
|
|
|
res, bodies, err := nfs4.DecodeCompoundResBodies(mustCompound(t, h, nfs4.AppendCompoundArgs(nil, "open-a",
|
|
nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendSequenceArgs(nil, sidA, 1, 0, defaultSlots-1, true),
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, 0xaaaa, []byte("a"),
|
|
nfs4.ShareAccessWrite, 0, true, 0o644, "deleg.txt"),
|
|
})))
|
|
if err != nil || res.Status != nfs4.ErrOK {
|
|
t.Fatalf("open a: status %d, %v", res.Status, err)
|
|
}
|
|
openSt, delegType, delegSt, err := nfs4.DecodeOpenResDeleg(bodies[2])
|
|
if err != nil {
|
|
t.Fatalf("deleg decode: %v", err)
|
|
}
|
|
if delegType != nfs4.OpenDelegWrite {
|
|
t.Fatalf("delegation %d, want write", delegType)
|
|
}
|
|
_ = openSt
|
|
|
|
// Client B opens for read-write: the recall of A's delegation travels
|
|
// over A's back channel on A's callback worker, and the conflicting
|
|
// open answers NFS4ERR_DELAY while it runs, RFC 8881 section 18.16.
|
|
// The retry after the recall completes proceeds.
|
|
cb, err := nfsclient.Dial(ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer cb.Close()
|
|
if err := cb.Establish("client-b"); err != nil {
|
|
t.Fatalf("establish b: %v", err)
|
|
}
|
|
sidB, _ := cb.SessionID()
|
|
|
|
openB := func(seq uint32) (nfs4.CompoundRes, error) {
|
|
body := mustCompound(t, h, nfs4.AppendCompoundArgs(nil, "open-b",
|
|
nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendSequenceArgs(nil, sidB, seq, 0, defaultSlots-1, true),
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, 0xbbbb, []byte("b"),
|
|
nfs4.ShareAccessWrite, 0, true, 0o644, "deleg.txt"),
|
|
}))
|
|
res, _, derr := nfs4.DecodeCompoundResBodies(body)
|
|
return res, derr
|
|
}
|
|
res, err = openB(1)
|
|
if err != nil || res.Status != nfs4.ErrDelay {
|
|
t.Fatalf("open b while the recall runs: status %d, want DELAY, %v", res.Status, err)
|
|
}
|
|
|
|
// A's back channel receives the recall of the delegation stateid, and
|
|
// B's retries proceed once it completed.
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for seq := uint32(2); time.Now().Before(deadline); seq++ {
|
|
if rec := ca.Recalled(); len(rec) != 1 || rec[0] != delegSt {
|
|
time.Sleep(time.Millisecond)
|
|
seq--
|
|
continue
|
|
}
|
|
res, err = openB(seq)
|
|
if err != nil {
|
|
t.Fatalf("open b after the recall: %v", err)
|
|
}
|
|
if res.Status == nfs4.ErrOK {
|
|
return
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
t.Fatal("the delegation recall never completed for client A")
|
|
}
|
|
|
|
func TestDestroyDropsLocksAndDelegations(t *testing.T) {
|
|
h := testTree(t)
|
|
sid, seq0 := newSession(t, h)
|
|
clientid := sid.ClientIDOf()
|
|
seq := seq0
|
|
|
|
run := func(ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
|
t.Helper()
|
|
all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, seq, 0, defaultSlots-1, true)}, ops...)
|
|
seq++
|
|
payload := nfs4.AppendCompoundArgs(nil, "test", nfs4.MinorVersion, all)
|
|
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("garbage")
|
|
}
|
|
r, bodies, derr := nfs4.DecodeCompoundResBodies(body)
|
|
if derr != nil {
|
|
t.Fatalf("decode: %v", derr)
|
|
}
|
|
return r, bodies
|
|
}
|
|
|
|
// OPEN with a write delegation plus a byte range lock.
|
|
res, bodies := run([][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, clientid, []byte("o"),
|
|
nfs4.ShareAccessWrite, 0, true, 0o644, "held.txt"),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("open: status %d", res.Status)
|
|
}
|
|
var openStateid nfs4.Stateid
|
|
copy(openStateid[:], bodies[2])
|
|
|
|
root, rerr := h.FS.Root()
|
|
if rerr != nil {
|
|
t.Fatal(rerr)
|
|
}
|
|
fileFH, _, lerr := h.FS.Lookup(root, "held.txt")
|
|
if lerr != nil {
|
|
t.Fatal(lerr)
|
|
}
|
|
if _, ok := h.delegs().holder(fileKey(fileFH)); !ok {
|
|
t.Fatal("the delegation was not granted")
|
|
}
|
|
|
|
res, _ = run([][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "held.txt"),
|
|
nfs4.AppendLockArgsNew(nil, openStateid, clientid, []byte("lk"),
|
|
nfs4.LockTypeWrite, false, 0, 0),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("lock: status %d", res.Status)
|
|
}
|
|
|
|
// DESTROY_CLIENTID drops the locks and the delegation with the client.
|
|
res, _ = run([][]byte{
|
|
nfs4.AppendDestroyClientIDArgs(nil, clientid),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("destroy: status %d", res.Status)
|
|
}
|
|
if h.locks().locksHeldOn(fileFH) {
|
|
t.Fatal("locks survived the destroy")
|
|
}
|
|
if _, ok := h.delegs().holder(fileKey(fileFH)); ok {
|
|
t.Fatal("the delegation survived the destroy")
|
|
}
|
|
}
|
|
|
|
func TestGraceWindowAndCompletion(t *testing.T) {
|
|
h := testTree(t)
|
|
h.GracePeriod = time.Hour
|
|
g := h.graced()
|
|
if !g.active(time.Now()) {
|
|
t.Fatal("a fresh grace window is not active")
|
|
}
|
|
|
|
// RECLAIM_COMPLETE is answered once per client within the window.
|
|
|
|
res, bodies := compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendReclaimCompleteArgs(nil, false),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("reclaim complete: status %d", res.Status)
|
|
}
|
|
_ = bodies
|
|
|
|
// A LOCK reclaim inside the window re-registers the lock (the server
|
|
// recovers no state, so the reclaim starts from scratch) and succeeds.
|
|
res, _ = compoundOps(t, h, nfs4.MinorVersion, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLockArgsNew(nil, nfs4.AllZero, 0x4242, []byte("lk"),
|
|
nfs4.LockTypeWrite, true, 0, 0),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("reclaim lock: status %d", res.Status)
|
|
}
|
|
}
|
|
|
|
func TestLockReclaimRefusedAfterGrace(t *testing.T) {
|
|
h := testTree(t)
|
|
h.GracePeriod = -time.Nanosecond
|
|
sid, _ := newSession(t, h)
|
|
seq := uint32(1)
|
|
run := func(ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
|
t.Helper()
|
|
all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, seq, 0, defaultSlots-1, true)}, ops...)
|
|
seq++
|
|
payload := nfs4.AppendCompoundArgs(nil, "test", nfs4.MinorVersion, all)
|
|
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("garbage")
|
|
}
|
|
r, bodies, derr := nfs4.DecodeCompoundResBodies(body)
|
|
if derr != nil {
|
|
t.Fatalf("decode: %v", derr)
|
|
}
|
|
return r, bodies
|
|
}
|
|
res, _ := run([][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendLockArgsNew(nil, nfs4.AllZero, 0x4242, []byte("lk"),
|
|
nfs4.LockTypeWrite, true, 0, 0),
|
|
})
|
|
wantStatus(t, "reclaim after grace", res.Status, nfs4.ErrNoGrace)
|
|
}
|
|
|
|
func TestDestroyUnderLoad(t *testing.T) {
|
|
h := testTree(t)
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := &server.Server{Handle: h.HandleConn}
|
|
go srv.Serve(t.Context(), ln)
|
|
|
|
const workers = 4
|
|
const rounds = 25
|
|
var wg sync.WaitGroup
|
|
errCh := make(chan error, workers)
|
|
|
|
for w := range workers {
|
|
wg.Add(1)
|
|
go func(worker int) {
|
|
defer wg.Done()
|
|
c, err := nfsclient.Dial(ln.Addr().String())
|
|
if err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
defer c.Close()
|
|
if err := c.Establish(fmt.Sprintf("load-%d", worker)); err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
for i := range rounds {
|
|
res, _, cerr := c.Compound("load", [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrSize)),
|
|
})
|
|
if cerr != nil || res.Status != nfs4.ErrOK {
|
|
errCh <- fmt.Errorf("worker %d round %d: status %d err %v", worker, i, res.Status, cerr)
|
|
return
|
|
}
|
|
}
|
|
}(w)
|
|
wg.Go(func() {
|
|
// A hammering destroyer: unknown and known client ids alike.
|
|
dc, derr := nfsclient.Dial(ln.Addr().String())
|
|
if derr != nil {
|
|
return
|
|
}
|
|
defer dc.Close()
|
|
if err := dc.Establish("destroyer"); err != nil {
|
|
return
|
|
}
|
|
for range rounds {
|
|
dc.Procedure(0) // keep the connection active
|
|
}
|
|
})
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errCh)
|
|
for err := range errCh {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestOpenDowngradeConflict(t *testing.T) {
|
|
h := testTree(t)
|
|
sid, seq0 := newSession(t, h)
|
|
seq := seq0
|
|
run := func(ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
|
t.Helper()
|
|
all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, seq, 0, defaultSlots-1, true)}, ops...)
|
|
seq++
|
|
payload := nfs4.AppendCompoundArgs(nil, "test", nfs4.MinorVersion, all)
|
|
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
|
if !ok {
|
|
t.Fatal("garbage")
|
|
}
|
|
r, bodies, derr := nfs4.DecodeCompoundResBodies(body)
|
|
if derr != nil {
|
|
t.Fatalf("decode: %v", derr)
|
|
}
|
|
return r, bodies
|
|
}
|
|
|
|
// A opens read-write with no deny, B opens read with a write deny.
|
|
res, bodies := run([][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, 0x1111, []byte("owner-a"),
|
|
nfs4.ShareAccessBoth, 0, true, 0o644, "down.txt"),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("open a: status %d", res.Status)
|
|
}
|
|
var stA nfs4.Stateid
|
|
copy(stA[:], bodies[2])
|
|
|
|
res, _ = run([][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, 0x2222, []byte("owner-b"),
|
|
nfs4.ShareAccessRead, 0, false, 0, "down.txt"),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("open b: status %d", res.Status)
|
|
}
|
|
|
|
// A narrows to write-only access with deny-write: that collides with
|
|
// B's read access, so the downgrade is refused.
|
|
// A narrows to write-only with deny-read: that collides with B's
|
|
// read access, so the downgrade is refused.
|
|
res, _ = run([][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "down.txt"),
|
|
nfs4.AppendOpenDowngradeArgs(nil, stA, nfs4.ShareAccessWrite, nfs4.ShareDenyRead),
|
|
})
|
|
wantStatus(t, "downgrade conflict", res.Status, nfs4.ErrShareDenied)
|
|
|
|
// Narrowing without a deny succeeds.
|
|
res, _ = run([][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "down.txt"),
|
|
nfs4.AppendOpenDowngradeArgs(nil, stA, nfs4.ShareAccessWrite, 0),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("downgrade without deny: status %d", res.Status)
|
|
}
|
|
}
|
|
|
|
func TestRestartRecovery(t *testing.T) {
|
|
dir := t.TempDir()
|
|
root := t.TempDir()
|
|
|
|
// First life: a server with its own backend and state dir.
|
|
backend1, err := nfsfs.NewLocal(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := backend1.LoadPersistedHandles(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h1 := &Handler{FS: backend1, StateDir: dir}
|
|
ln1, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv1 := &server.Server{Handle: h1.HandleConn}
|
|
go srv1.Serve(t.Context(), ln1)
|
|
|
|
c1, err := nfsclient.Dial(ln1.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c1.Establish("restart-a"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sidA, _ := c1.SessionID()
|
|
res, bodies, err := c1.Compound("open", [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, 0x1111, []byte("owner-a"),
|
|
nfs4.ShareAccessBoth, 0, true, 0o644, "kept.txt"),
|
|
})
|
|
if err != nil || res.Status != nfs4.ErrOK {
|
|
t.Fatalf("open: status %d, %v", res.Status, err)
|
|
}
|
|
var openSt nfs4.Stateid
|
|
copy(openSt[:], bodies[1])
|
|
|
|
// Restart: the connections drop and a fresh server reads the persisted
|
|
// state from the same directory.
|
|
c1.Close()
|
|
ln1.Close()
|
|
ln2, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backend2, err := nfsfs.NewLocal(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := backend2.LoadPersistedHandles(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h2 := &Handler{FS: backend2, StateDir: dir}
|
|
srv2 := &server.Server{Handle: h2.HandleConn}
|
|
go srv2.Serve(t.Context(), ln2)
|
|
|
|
c2, err := nfsclient.Dial(ln2.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer c2.Close()
|
|
if err := c2.Establish("restart-b"); err != nil {
|
|
t.Fatalf("establish: %v", err)
|
|
}
|
|
sidB, _ := c2.SessionID()
|
|
|
|
// The client reclaims its open with CLAIM_PREVIOUS over the old
|
|
// handle bytes; the recovered state answers.
|
|
res, bodies, err = c2.Compound("reclaim", [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "kept.txt"),
|
|
nfs4.AppendOpenArgsPrevious(nil, 0x1111, []byte("owner-a"),
|
|
nfs4.ShareAccessBoth, 0),
|
|
})
|
|
if err != nil || res.Status != nfs4.ErrOK {
|
|
t.Fatalf("reclaim open: status %d, %v", res.Status, err)
|
|
}
|
|
var newSt nfs4.Stateid
|
|
copy(newSt[:], bodies[2])
|
|
if newSt != openSt {
|
|
t.Fatalf("the recovered stateid %x differs from %x", newSt, openSt)
|
|
}
|
|
|
|
// A different client on the restarted server sees the file intact.
|
|
res, _, err = c2.Compound("read", [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "kept.txt"),
|
|
nfs4.AppendRead(nil, nfs4.AllZero, 0, 64),
|
|
})
|
|
if err != nil || res.Status != nfs4.ErrOK {
|
|
t.Fatalf("read: status %d, %v", res.Status, err)
|
|
}
|
|
_ = sidA
|
|
_ = sidB
|
|
}
|