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
644 lines
18 KiB
Go
644 lines
18 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package nfs4server
|
|
|
|
import (
|
|
crand "crypto/rand"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
|
|
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
|
|
)
|
|
|
|
// randCounter draws a random starting value for a state counter, so the
|
|
// other field of a stateid this server mints is difficult to guess,
|
|
// RFC 8881 section 8.2.2. A counter that started at one would let a
|
|
// client walk another client's stateids by enumeration.
|
|
func randCounter() uint64 {
|
|
var b [8]byte
|
|
if _, err := crand.Read(b[:]); err != nil {
|
|
panic("nfs4server: the random source failed: " + err.Error())
|
|
}
|
|
return binary.BigEndian.Uint64(b[:])
|
|
}
|
|
|
|
var dbgMu sync.Mutex
|
|
|
|
// stateidSeq answers the sequence field of a stateid as the 32 bit
|
|
// big-endian word the wire carries, RFC 7863.
|
|
func stateidSeq(st nfs4.Stateid) uint32 {
|
|
return binary.BigEndian.Uint32(st[0:4])
|
|
}
|
|
|
|
// setStateidSeq writes the sequence field of a stateid as a big-endian
|
|
// word.
|
|
func setStateidSeq(st *nfs4.Stateid, seq uint32) {
|
|
binary.BigEndian.PutUint32(st[0:4], seq)
|
|
}
|
|
|
|
// shareConflict reports whether a new open with the given access and deny
|
|
// bits collides with an existing one on the same file. A requested access
|
|
// fights an existing deny of the same kind, and a requested deny fights an
|
|
// existing access.
|
|
func shareConflicts(access, deny uint32, existing *openEntry) bool {
|
|
if access&nfs4.ShareAccessRead != 0 && existing.deny&nfs4.ShareDenyRead != 0 {
|
|
return true
|
|
}
|
|
if access&nfs4.ShareAccessWrite != 0 && existing.deny&nfs4.ShareDenyWrite != 0 {
|
|
return true
|
|
}
|
|
if deny&nfs4.ShareDenyRead != 0 && existing.access&nfs4.ShareAccessRead != 0 {
|
|
return true
|
|
}
|
|
if deny&nfs4.ShareDenyWrite != 0 && existing.access&nfs4.ShareAccessWrite != 0 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// An openEntry is one live OPEN: the stateid the client holds, the share
|
|
// reservation it made, and the file it points at. A recovered open is
|
|
// one the store loaded back after a server restart: it still carries
|
|
// the client id of its past life, which a CLAIM_PREVIOUS in the grace
|
|
// window rebinds to the client that comes back for it.
|
|
type openEntry struct {
|
|
stateid nfs4.Stateid
|
|
clientID uint64
|
|
owner []byte
|
|
fh nfsfs.Handle
|
|
fileKey string
|
|
access uint32
|
|
deny uint32
|
|
delegSt *nfs4.Stateid
|
|
recovered bool
|
|
}
|
|
|
|
// stateStore keeps the OPEN state of the server: every live open, the
|
|
// share reservations grouped per file, and the tombstones of closed
|
|
// stateids, which turn a reuse of an old stateid into NFS4ERR_OLD_STATEID
|
|
// instead of the vaguer NFS4ERR_BAD_STATEID.
|
|
type stateStore struct {
|
|
mu sync.Mutex
|
|
next uint64
|
|
dir string // when set, live opens persist here across restarts
|
|
opens map[string]*openEntry
|
|
byFile map[string][]*openEntry
|
|
closed map[string]uint32
|
|
}
|
|
|
|
func newStateStore(dir string) *stateStore {
|
|
s := &stateStore{
|
|
next: randCounter(),
|
|
dir: dir,
|
|
opens: make(map[string]*openEntry),
|
|
byFile: make(map[string][]*openEntry),
|
|
closed: make(map[string]uint32),
|
|
}
|
|
if dir != "" {
|
|
s.load(dir)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// persist writes the live opens into dir, atomically.
|
|
func (s *stateStore) persist(dir string) {
|
|
if dir == "" {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
list := make([]persistedOpen, 0, len(s.opens))
|
|
for other, e := range s.opens {
|
|
list = append(list, persistedOpen{
|
|
Other: hex.EncodeToString([]byte(other)),
|
|
Seqid: stateidSeq(e.stateid),
|
|
ClientID: e.clientID,
|
|
FileKey: hex.EncodeToString([]byte(e.fileKey)),
|
|
Access: e.access,
|
|
Deny: e.deny,
|
|
})
|
|
}
|
|
s.mu.Unlock()
|
|
image, err := json.Marshal(map[string][]persistedOpen{"opens": list})
|
|
if err != nil {
|
|
return
|
|
}
|
|
tmp := filepath.Join(dir, "opens.json.tmp")
|
|
if err := os.WriteFile(tmp, image, 0o600); err != nil {
|
|
return
|
|
}
|
|
_ = os.Rename(tmp, filepath.Join(dir, "opens.json"))
|
|
}
|
|
|
|
// loadDir reads the persisted opens from dir into the store.
|
|
func (s *stateStore) load(dir string) {
|
|
data, err := os.ReadFile(filepath.Join(dir, "opens.json"))
|
|
if err != nil {
|
|
return
|
|
}
|
|
var image struct {
|
|
Opens []persistedOpen `json:"opens"`
|
|
}
|
|
if json.Unmarshal(data, &image) != nil {
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for _, p := range image.Opens {
|
|
other, derr := hex.DecodeString(p.Other)
|
|
if derr != nil || len(other) != 12 {
|
|
continue
|
|
}
|
|
fileKeyBytes, derr := hex.DecodeString(p.FileKey)
|
|
if derr != nil {
|
|
continue
|
|
}
|
|
var st nfs4.Stateid
|
|
setStateidSeq(&st, p.Seqid)
|
|
copy(st[4:], other)
|
|
e := &openEntry{
|
|
stateid: st,
|
|
clientID: p.ClientID,
|
|
fileKey: string(fileKeyBytes),
|
|
access: p.Access,
|
|
deny: p.Deny,
|
|
recovered: true,
|
|
}
|
|
s.opens[string(other)] = e
|
|
s.byFile[e.fileKey] = append(s.byFile[e.fileKey], e)
|
|
}
|
|
}
|
|
|
|
// fileKey names one file across all its handles: the backend handle bytes
|
|
// already encode the stable identity of the file.
|
|
func fileKey(fh nfsfs.Handle) string {
|
|
return string(fh)
|
|
}
|
|
|
|
// open registers a new OPEN of a file, enforcing the share reservations of
|
|
// the opens already live on it. It answers the stateid of the new open.
|
|
func (s *stateStore) open(fh nfsfs.Handle, clientid uint64, owner []byte, access, deny uint32) (nfs4.Stateid, uint32) {
|
|
key := fileKey(fh)
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for _, e := range s.byFile[key] {
|
|
if shareConflicts(access, deny, e) {
|
|
return nfs4.Stateid{}, nfs4.ErrShareDenied
|
|
}
|
|
}
|
|
s.next++
|
|
var other [12]byte
|
|
copy(other[:4], []byte("OPEN"))
|
|
otherUint := s.next
|
|
other[4] = byte(otherUint >> 56)
|
|
other[5] = byte(otherUint >> 48)
|
|
other[6] = byte(otherUint >> 40)
|
|
other[7] = byte(otherUint >> 32)
|
|
other[8] = byte(otherUint >> 24)
|
|
other[9] = byte(otherUint >> 16)
|
|
other[10] = byte(otherUint >> 8)
|
|
other[11] = byte(otherUint)
|
|
st := nfs4.Stateid{}
|
|
setStateidSeq(&st, 1) // the stateid of the first state change
|
|
copy(st[4:], other[:])
|
|
|
|
e := &openEntry{
|
|
stateid: st,
|
|
clientID: clientid,
|
|
owner: owner,
|
|
fh: fh,
|
|
fileKey: key,
|
|
access: access,
|
|
deny: deny,
|
|
}
|
|
s.opens[string(other[:])] = e
|
|
s.byFile[key] = append(s.byFile[key], e)
|
|
return st, nfs4.ErrOK
|
|
}
|
|
|
|
// close releases an OPEN by its stateid. Only the client the open
|
|
// belongs to may close it. A stateid older than the live one answers
|
|
// NFS4ERR_OLD_STATEID, an unknown one NFS4ERR_BAD_STATEID.
|
|
func (s *stateStore) close(st nfs4.Stateid, clientid uint64) (nfs4.Stateid, uint32) {
|
|
other := st[4:]
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
e, ok := s.opens[string(other)]
|
|
if !ok {
|
|
if last, was := s.closed[string(other)]; was && stateidSeq(st) <= last {
|
|
return nfs4.Stateid{}, nfs4.ErrOldStateid
|
|
}
|
|
return nfs4.Stateid{}, nfs4.ErrBadStateid
|
|
}
|
|
if e.clientID != clientid {
|
|
return nfs4.Stateid{}, nfs4.ErrBadStateid
|
|
}
|
|
switch {
|
|
case stateidSeq(st) == 0: // a cleared sequence names the current version
|
|
case stateidSeq(st) < stateidSeq(e.stateid):
|
|
return nfs4.Stateid{}, nfs4.ErrOldStateid
|
|
case stateidSeq(st) > stateidSeq(e.stateid):
|
|
return nfs4.Stateid{}, nfs4.ErrBadStateid
|
|
}
|
|
closed := e.stateid
|
|
setStateidSeq(&closed, stateidSeq(e.stateid)+1) // CLOSE answers a dead stateid
|
|
|
|
delete(s.opens, string(other))
|
|
s.closed[string(other)] = stateidSeq(closed)
|
|
list := s.byFile[e.fileKey]
|
|
for i, cand := range list {
|
|
if cand == e {
|
|
s.byFile[e.fileKey] = append(list[:i], list[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
return closed, nfs4.ErrOK
|
|
}
|
|
|
|
// currentStateid is the special CURRENT_STATEID of RFC 8881 section
|
|
// 8.2.3: sequence one and an empty other field name the most recent
|
|
// stateid the caller holds on the file.
|
|
func currentStateid() nfs4.Stateid {
|
|
var st nfs4.Stateid
|
|
setStateidSeq(&st, 1)
|
|
return st
|
|
}
|
|
|
|
// checkStateid resolves a stateid handed to READ, WRITE or SETATTR. The
|
|
// anonymous forms pass through with no entry; CURRENT_STATEID resolves
|
|
// to the caller's open of the file; anything else must name a live open
|
|
// of the same file that belongs to the asking client.
|
|
func (s *stateStore) checkStateid(st nfs4.Stateid, fh nfsfs.Handle, clientid uint64) (uint32, uint32) {
|
|
allOnes := nfs4.Stateid{}
|
|
for i := range allOnes {
|
|
allOnes[i] = 0xff
|
|
}
|
|
if st == allOnes || st == (nfs4.Stateid{}) {
|
|
return 0, nfs4.ErrOK
|
|
}
|
|
other := st[4:]
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
var e *openEntry
|
|
if st == currentStateid() {
|
|
// The current stateid names the caller's own open of this file.
|
|
for _, cand := range s.byFile[fileKey(fh)] {
|
|
if cand.clientID == clientid {
|
|
e = cand
|
|
break
|
|
}
|
|
}
|
|
if e == nil {
|
|
return 0, nfs4.ErrBadStateid
|
|
}
|
|
} else {
|
|
var ok bool
|
|
e, ok = s.opens[string(other)]
|
|
if !ok {
|
|
if last, was := s.closed[string(other)]; was && stateidSeq(st) <= last {
|
|
return 0, nfs4.ErrOldStateid
|
|
}
|
|
return 0, nfs4.ErrBadStateid
|
|
}
|
|
}
|
|
if e.clientID != clientid {
|
|
return 0, nfs4.ErrBadStateid
|
|
}
|
|
// A zero sequence names whatever version is current, RFC 8881
|
|
// section 8.2.2: conformant clients present stateids with the
|
|
// sequence field cleared, and the server honours them as the live
|
|
// version.
|
|
if st != currentStateid() && stateidSeq(st) != 0 {
|
|
if stateidSeq(st) < stateidSeq(e.stateid) {
|
|
return 0, nfs4.ErrOldStateid
|
|
}
|
|
if stateidSeq(st) > stateidSeq(e.stateid) {
|
|
return 0, nfs4.ErrBadStateid
|
|
}
|
|
}
|
|
if e.fileKey != fileKey(fh) {
|
|
return 0, nfs4.ErrBadStateid
|
|
}
|
|
return e.access, nfs4.ErrOK
|
|
}
|
|
|
|
// lookupOpen resolves a stateid to its live open entry, checking that
|
|
// the stateid names the file the caller says it does and belongs to the
|
|
// asking client. The anonymous forms never resolve here.
|
|
func (s *stateStore) lookupOpen(st nfs4.Stateid, fh nfsfs.Handle, clientid uint64) (*openEntry, uint32) {
|
|
other := st[4:]
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
e, ok := s.opens[string(other)]
|
|
if !ok {
|
|
return nil, nfs4.ErrBadStateid
|
|
}
|
|
if e.fileKey != fileKey(fh) || e.clientID != clientid {
|
|
return nil, nfs4.ErrBadStateid
|
|
}
|
|
return e, nfs4.ErrOK
|
|
}
|
|
|
|
// downgrade reduces the share access and deny bits of a live open. The
|
|
// stateid sequence moves one up; an older stateid is OLD_STATEID, an
|
|
// unknown one BAD_STATEID. Only the client the open belongs to may
|
|
// narrow it.
|
|
func (s *stateStore) downgrade(st nfs4.Stateid, clientid uint64, access, deny uint32) (nfs4.Stateid, uint32) {
|
|
other := st[4:]
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
e, ok := s.opens[string(other)]
|
|
if !ok {
|
|
if last, was := s.closed[string(other)]; was && stateidSeq(st) <= last {
|
|
return nfs4.Stateid{}, nfs4.ErrOldStateid
|
|
}
|
|
return nfs4.Stateid{}, nfs4.ErrBadStateid
|
|
}
|
|
if e.clientID != clientid {
|
|
return nfs4.Stateid{}, nfs4.ErrBadStateid
|
|
}
|
|
if stateidSeq(st) != stateidSeq(e.stateid) && stateidSeq(st) != 0 {
|
|
// A cleared sequence names the current version, RFC 8881
|
|
// section 8.2.2.
|
|
if stateidSeq(st) < stateidSeq(e.stateid) {
|
|
return nfs4.Stateid{}, nfs4.ErrOldStateid
|
|
}
|
|
return nfs4.Stateid{}, nfs4.ErrBadStateid
|
|
}
|
|
// The narrowed bits must not collide with the other opens of the file.
|
|
for _, cand := range s.byFile[e.fileKey] {
|
|
if cand == e {
|
|
continue
|
|
}
|
|
if shareConflicts(access, deny, cand) {
|
|
return nfs4.Stateid{}, nfs4.ErrShareDenied
|
|
}
|
|
}
|
|
e.access = access
|
|
e.deny = deny
|
|
setStateidSeq(&e.stateid, stateidSeq(e.stateid)+1)
|
|
return e.stateid, nfs4.ErrOK
|
|
}
|
|
|
|
// dropClient releases every OPEN of the client, which is what
|
|
// DESTROY_CLIENTID and lease expiry require.
|
|
func (s *stateStore) dropClient(clientid uint64) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for other, e := range s.opens {
|
|
if e.clientID == clientid {
|
|
delete(s.opens, other)
|
|
list := s.byFile[e.fileKey]
|
|
for i, cand := range list {
|
|
if cand == e {
|
|
s.byFile[e.fileKey] = append(list[:i], list[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A delegation is one granted OPEN delegation: the stateid the client
|
|
// holds, the session its recalls travel over, and the file it names.
|
|
type delegation struct {
|
|
stateid nfs4.Stateid
|
|
sessID nfs4.SessionID
|
|
fileKey string
|
|
clientID uint64
|
|
kind uint32 // nfs4.OpenDelegRead or nfs4.OpenDelegWrite
|
|
}
|
|
|
|
// delegStore tracks the live delegations. A file carries at most one; the
|
|
// store only answers whether a grant is possible and who holds what;
|
|
// the recall travels over the holder's back channel.
|
|
type delegStore struct {
|
|
mu sync.Mutex
|
|
next uint64
|
|
byKey map[string]*delegation // one per file key
|
|
}
|
|
|
|
func newDelegStore() *delegStore {
|
|
return &delegStore{next: randCounter(), byKey: make(map[string]*delegation)}
|
|
}
|
|
|
|
// grant registers a delegation of the file for the client and answers the
|
|
// delegation stateid. A file already delegated to somebody else is
|
|
// refused, which keeps the grants exclusive.
|
|
func (s *delegStore) grant(sessID nfs4.SessionID, clientid uint64, key string, kind uint32) (nfs4.Stateid, uint32) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if _, ok := s.byKey[key]; ok {
|
|
return nfs4.Stateid{}, nfs4.ErrDenied
|
|
}
|
|
s.next++
|
|
var other [12]byte
|
|
copy(other[:4], []byte("DELE"))
|
|
be := uint64(s.next)
|
|
for i := range 8 {
|
|
other[11-i] = byte(be >> (8 * i))
|
|
}
|
|
var st nfs4.Stateid
|
|
setStateidSeq(&st, 1)
|
|
copy(st[4:], other[:])
|
|
d := &delegation{stateid: st, sessID: sessID, fileKey: key, clientID: clientid, kind: kind}
|
|
s.byKey[key] = d
|
|
return st, nfs4.ErrOK
|
|
}
|
|
|
|
// holder returns the live delegation of a file, if any.
|
|
func (s *delegStore) holder(key string) (*delegation, bool) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
d, ok := s.byKey[key]
|
|
return d, ok
|
|
}
|
|
|
|
// revoke drops the delegation of a file.
|
|
func (s *delegStore) revoke(key string) {
|
|
s.mu.Lock()
|
|
delete(s.byKey, key)
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
// dropClient releases every delegation the client holds.
|
|
func (s *delegStore) dropClient(clientid uint64) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for key, d := range s.byKey {
|
|
if d.clientID == clientid {
|
|
delete(s.byKey, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// countOpens reports how many live opens the file carries.
|
|
func (s *stateStore) countOpens(fh nfsfs.Handle) int {
|
|
key := fileKey(fh)
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return len(s.byFile[key])
|
|
}
|
|
|
|
// bindDelegation records the delegation stateid on the open, so a CLOSE
|
|
// of the open revokes the delegation with it.
|
|
func (s *stateStore) bindDelegation(open nfs4.Stateid, deleg nfs4.Stateid) {
|
|
other := open[4:]
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if e, ok := s.opens[string(other)]; ok {
|
|
e.delegSt = &deleg
|
|
}
|
|
}
|
|
|
|
// delegOf reports the delegation bound to an open, if any.
|
|
func (s *stateStore) delegOf(open nfs4.Stateid) (nfs4.Stateid, bool) {
|
|
other := open[4:]
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
e, ok := s.opens[string(other)]
|
|
if !ok || e.delegSt == nil {
|
|
return nfs4.Stateid{}, false
|
|
}
|
|
return *e.delegSt, true
|
|
}
|
|
|
|
// A persistedOpen is the on disk image of one live OPEN: enough to
|
|
// re-register the state after a server restart, so the handles clients
|
|
// hold keep their state across the restart.
|
|
type persistedOpen struct {
|
|
Other string `json:"other"` // hex of the 12 byte other field
|
|
Seqid uint32 `json:"seqid"`
|
|
ClientID uint64 `json:"clientid"`
|
|
FileKey string `json:"file_key"` // hex of the backend file key
|
|
Access uint32 `json:"access"`
|
|
Deny uint32 `json:"deny"`
|
|
}
|
|
|
|
// reclaimOpen resolves a CLAIM_PREVIOUS open: the pre restart open of
|
|
// the file whose handle the client presented. The client's own open
|
|
// answers first; otherwise a recovered open of the file, one the store
|
|
// loaded back after the restart, is rebound to the claiming client,
|
|
// because client ids do not survive a restart and the grace window is
|
|
// the only gate. It answers the open's stateid by value, so the caller
|
|
// holds no pointer into the store. When nothing answers it returns
|
|
// BAD_STATEID.
|
|
func (s *stateStore) reclaimOpen(fh nfsfs.Handle, clientid uint64) (nfs4.Stateid, uint32) {
|
|
key := fileKey(fh)
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for _, e := range s.byFile[key] {
|
|
if e.clientID == clientid {
|
|
e.recovered = false
|
|
return e.stateid, nfs4.ErrOK
|
|
}
|
|
}
|
|
for _, e := range s.byFile[key] {
|
|
if e.recovered {
|
|
e.clientID = clientid
|
|
e.recovered = false
|
|
return e.stateid, nfs4.ErrOK
|
|
}
|
|
}
|
|
return nfs4.Stateid{}, nfs4.ErrBadStateid
|
|
}
|
|
|
|
// dropStateid removes the delegation the stateid names, when it belongs
|
|
// to the asking client, and reports whether there was one, which
|
|
// DELEGRETURN requires.
|
|
func (s *delegStore) dropStateid(st nfs4.Stateid, clientid uint64) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for key, d := range s.byKey {
|
|
if d.stateid == st {
|
|
if d.clientID != clientid {
|
|
return false
|
|
}
|
|
delete(s.byKey, key)
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// revokeIf drops the delegation of the file when it is still the one the
|
|
// recall named, so a recall that completes after a fresh grant never
|
|
// kills the new holder.
|
|
func (s *delegStore) revokeIf(key string, st nfs4.Stateid) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if d, ok := s.byKey[key]; ok && d.stateid == st {
|
|
delete(s.byKey, key)
|
|
}
|
|
}
|
|
|
|
// hasStateid reports whether the delegation stateid is live.
|
|
func (s *delegStore) hasStateid(st nfs4.Stateid) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for _, d := range s.byKey {
|
|
if d.stateid == st {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// checkDataStateid validates a stateid handed to a data operation
|
|
// against the delegation store: RFC 8881 sections 8.2.3 and 10.3 let a
|
|
// client present the delegation stateid of the file to READ, WRITE and
|
|
// their kin. The stateid must be live, name the asking client and name
|
|
// this very file.
|
|
func (s *delegStore) checkDataStateid(st nfs4.Stateid, fh nfsfs.Handle, clientid uint64) uint32 {
|
|
key := fileKey(fh)
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for _, d := range s.byKey {
|
|
// The presented sequence is irrelevant: a conformant client
|
|
// may present the stateid with the sequence field cleared,
|
|
// RFC 8881 section 8.2.2.
|
|
if string(d.stateid[4:]) == string(st[4:]) {
|
|
if d.clientID != clientid || d.fileKey != key {
|
|
return nfs4.ErrBadStateid
|
|
}
|
|
return nfs4.ErrOK
|
|
}
|
|
}
|
|
return nfs4.ErrBadStateid
|
|
}
|
|
|
|
// testStateid reports the status of one stateid against the open store:
|
|
// the answer TEST_STATEID hands back without touching any state.
|
|
func (s *stateStore) testStateid(st nfs4.Stateid) uint32 {
|
|
allOnes := nfs4.Stateid{}
|
|
for i := range allOnes {
|
|
allOnes[i] = 0xff
|
|
}
|
|
if st == allOnes || st == (nfs4.Stateid{}) {
|
|
return nfs4.ErrOK
|
|
}
|
|
other := st[4:]
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if e, ok := s.opens[string(other)]; ok {
|
|
if stateidSeq(st) < stateidSeq(e.stateid) {
|
|
return nfs4.ErrOldStateid
|
|
}
|
|
if stateidSeq(st) > stateidSeq(e.stateid) {
|
|
return nfs4.ErrBadStateid
|
|
}
|
|
return nfs4.ErrOK
|
|
}
|
|
if last, was := s.closed[string(other)]; was {
|
|
if stateidSeq(st) <= last {
|
|
return nfs4.ErrOldStateid
|
|
}
|
|
}
|
|
return nfs4.ErrBadStateid
|
|
}
|