// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT // The handlers of the remaining NFSv4.1 operations: the parent lookup, // the attribute comparisons, the lock owner retirement and the session // binding family. package nfs4server import ( "errors" "sourcedock.dev/petrbalvin/nfs/internal/nfs4" "sourcedock.dev/petrbalvin/nfs/internal/nfsfs" "sourcedock.dev/petrbalvin/nfs/internal/xdr" ) // maxOps bounds the TEST_STATEID array, matching the COMPOUND bound of // the wire codecs. const maxOps = 1024 // lookuppOp serves LOOKUPP: the current handle becomes its parent // directory. func (h *Handler) lookuppOp(reg *fhreg) ([]byte, uint32, error) { if !reg.haveCur { return nil, nfs4.ErrNoFileHandle, nil } parent, _, err := h.FS.Parent(reg.cur) if err != nil { return nil, mapErr(err), nil } reg.cur, reg.haveCur = parent, true return nil, nfs4.ErrOK, nil } // verifyOp serves VERIFY and NVERIFY: the compound proceeds only when the // file carries, or with the negated form does not carry, every attribute // the arguments assert. A value this build cannot compare answers // ATTRNOTSUPP. func (h *Handler) verifyOp(d *xdr.Decoder, reg *fhreg, negate bool) ([]byte, uint32, error) { if !reg.haveCur { return nil, nfs4.ErrNoFileHandle, nil } request, err := nfs4.ReadBitmap(d) if err != nil { return nil, 0, err } blob, err := d.VarOpaque() if err != nil { return nil, 0, err } updates, err := nfs4.DecodeSetattrBlob(blob, request) if err != nil { if err == nfs4.ErrAttrNotSettable { return nil, nfs4.ErrAttrNotSupp, nil } return nil, 0, err } info, ferr := h.FS.Getattr(reg.cur) if ferr != nil { return nil, mapErr(ferr), nil } mismatch := false switch { case updates.HasMode && uint32(info.Mode.Perm()) != updates.Mode: mismatch = true case updates.HasSize && uint64(info.Size) != updates.Size: mismatch = true case updates.UID != nil && info.UID != *updates.UID: mismatch = true case updates.GID != nil && info.GID != *updates.GID: mismatch = true case updates.Atime != nil && !nfsTimeMatches(updates.Atime, info): mismatch = true case updates.Mtime != nil && !nfsTimeMatches(updates.Mtime, info): mismatch = true } if mismatch == negate { return nil, nfs4.ErrOK, nil } if negate { return nil, nfs4.ErrSame, nil } return nil, nfs4.ErrNotSame, nil } // nfsTimeMatches reports whether the asserted time is the file time the // backend reports. A server time assertion never matches a fixed value; // only the seconds and nanoseconds a client carries count. func nfsTimeMatches(set *nfs4.NfsTimeSet, info nfsfs.Info) bool { if set.Server { return false } got := info.ModTime return got.Unix() == set.Time.Seconds && int64(uint32(got.Nanosecond())) == int64(set.Time.Nseconds) } // errTooManyStateids marks a TEST_STATEID whose array exceeds the bound // every COMPOUND array of this build obeys. var errTooManyStateids = errors.New("nfs4server: too many stateids in one test") // releaseLockOwnerOp serves RELEASE_LOCKOWNER: every lock state the // owner holds dies with the owner. The owner must belong to the asking // client, RFC 8881 section 18.38. func (h *Handler) releaseLockOwnerOp(d *xdr.Decoder, sessionClientid uint64) ([]byte, uint32, error) { clientid, err := d.Uint64() if err != nil { return nil, 0, err } owner, err := d.VarOpaque() if err != nil { return nil, 0, err } if sessionClientid != 0 && clientid != sessionClientid { return nil, nfs4.ErrBadOwner, nil } h.locks().releaseOwner(clientid, owner) return nil, nfs4.ErrOK, nil } // delegReturnOp serves DELEGRETURN: the client gives its own delegation // back. A stateid the store holds is dropped; anything else is answered // OK just the same, because the release of a state the server does not // carry is already done and the reply only tells the client to forget // the stateid. func (h *Handler) delegReturnOp(d *xdr.Decoder, sessionClientid uint64) ([]byte, uint32, error) { var st nfs4.Stateid raw, err := d.Raw(16) if err != nil { return nil, 0, err } copy(st[:], raw) h.delegs().dropStateid(st, sessionClientid) return nil, nfs4.ErrOK, nil } // backchannelCtlOp serves BACKCHANNEL_CTL: the client names the program // its future back channels run. func (h *Handler) backchannelCtlOp(d *xdr.Decoder, ctx *connCB) ([]byte, uint32, error) { program, err := d.Uint32() if err != nil { return nil, 0, err } if ctx != nil { ctx.setProgram(program) } return nil, nfs4.ErrOK, nil } // bindConnToSessionOp serves BIND_CONN_TO_SESSION: the connection the // request rode in on joins the session as a fore channel, a back channel // or both. The session must belong to the asking client, so one client // cannot graft its connection onto another's callbacks, and the // callback program comes from CREATE_SESSION, which stores it with the // session. func (h *Handler) bindConnToSessionOp(d *xdr.Decoder, ctx *connCB, sessionClientid uint64) ([]byte, uint32, error) { var id nfs4.SessionID raw, err := d.Raw(16) if err != nil { return nil, 0, err } copy(id[:], raw) dir, err := d.Uint32() if err != nil { return nil, 0, err } if _, err = d.Bool(); err != nil { // use conn in RDMA mode return nil, 0, err } if _, status := h.sessions().lookupSession(id); status != nfs4.ErrOK { return nil, status, nil } // A compound that carries a session may bind only its own: one // client must not graft its connection onto another's callbacks. if sessionClientid != 0 && id.ClientIDOf() != sessionClientid { return nil, nfs4.ErrBadSession, nil } switch dir { case nfs4.Cdfc4Back, nfs4.Cdfc4BackOrBoth: if ctx == nil { return nil, nfs4.ErrConnNotBound, nil } h.sessions().attachCB(id, ctx) return nfs4.AppendBindConnToSessionRes(nil, id, nfs4.Cdfs4Both), nfs4.ErrOK, nil case nfs4.Cdfc4Fore, nfs4.Cdfc4ForeOrBoth: return nfs4.AppendBindConnToSessionRes(nil, id, nfs4.Cdfs4Fore), nfs4.ErrOK, nil default: return nil, nfs4.ErrInval, nil } } // freeStateidOp serves FREE_STATEID: the client retires a lock stateid // of its own whose owner holds no locks any more. func (h *Handler) freeStateidOp(d *xdr.Decoder, sessionClientid uint64) ([]byte, uint32, error) { var st nfs4.Stateid raw, err := d.Raw(16) if err != nil { return nil, 0, err } copy(st[:], raw) if status := h.locks().freeStateid(st, sessionClientid); status != nfs4.ErrOK { return nil, status, nil } return nil, nfs4.ErrOK, nil } // testStateidOp serves TEST_STATEID: one status per stateid, no state // touched. func (h *Handler) testStateidOp(d *xdr.Decoder) ([]byte, uint32, error) { n, err := d.Uint32() if err != nil { return nil, 0, err } if n > maxOps { return nil, 0, errTooManyStateids } var statuses []uint32 for range n { var st nfs4.Stateid raw, rerr := d.Raw(16) if rerr != nil { return nil, 0, rerr } copy(st[:], raw) status := h.openStates().testStateid(st) if status == nfs4.ErrBadStateid { switch { case h.locks().hasStateid(st): status = nfs4.ErrOK case h.delegs().hasStateid(st): status = nfs4.ErrOK } } statuses = append(statuses, status) } return nfs4.AppendTestStateidRes(nil, statuses), nfs4.ErrOK, nil }