// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package nfs4 import ( "errors" "fmt" "sourcedock.dev/petrbalvin/nfs/internal/xdr" ) // maxOps bounds the COMPOUND arrays on both encode and decode. The // protocol has no fixed limit; a request longer than this is not a client, // it is an attack. const maxOps = 1024 // maxTag bounds the COMPOUND tag. const maxTag = 128 // AppendCompoundArgs encodes the COMPOUND4args: the tag, the minor version // and the operation array, where each element is an encoded argop: its // operation number followed by its own arguments. func AppendCompoundArgs(b []byte, tag string, minor uint32, ops [][]byte) []byte { b = xdr.AppendString(b, tag) b = xdr.AppendUint32(b, minor) b = xdr.AppendUint32(b, uint32(len(ops))) for _, op := range ops { b = append(b, op...) } return b } // CompoundHeader is the head of a decoded COMPOUND4args: the tag, the minor // version and the operation count. The returned decoder is positioned at // the first argop, and the dispatcher reads each operation's own arguments // from it as it dispatches. type CompoundHeader struct { Tag string Minor uint32 OpCount uint32 } // DecodeCompoundArgs splits COMPOUND4args into its header and a decoder // over the argops. func DecodeCompoundArgs(payload []byte) (CompoundHeader, *xdr.Decoder, error) { d := xdr.NewDecoder(payload) var h CompoundHeader var err error if h.Tag, err = d.String(); err != nil { return h, nil, err } if len(h.Tag) > maxTag { return h, nil, fmt.Errorf("nfs4: tag of %d bytes exceeds %d", len(h.Tag), maxTag) } if h.Minor, err = d.Uint32(); err != nil { return h, nil, err } if h.OpCount, err = d.Uint32(); err != nil { return h, nil, err } if h.OpCount > maxOps { return h, nil, fmt.Errorf("nfs4: %d operations exceeds %d", h.OpCount, maxOps) } return h, d, nil } // AppendCompoundRes encodes the COMPOUND4res: the top level status, the // echoed tag and the result array. On success the results hold one entry // per executed operation; on failure they hold the results up to and // including the operation that failed, and the top level status carries its // status. func AppendCompoundRes(b []byte, status uint32, tag string, results [][]byte) []byte { b = xdr.AppendUint32(b, status) b = xdr.AppendString(b, tag) b = xdr.AppendUint32(b, uint32(len(results))) for _, op := range results { b = append(b, op...) } return b } // AppendOpHeader appends the head of one nfs_resop4: the operation number // and its status. A successful result appends its payload afterwards; a // failed one is complete as it stands. func AppendOpHeader(b []byte, op, status uint32) []byte { b = xdr.AppendUint32(b, op) return xdr.AppendUint32(b, status) } // A ResOp is one decoded result operation: its number, its status, and the // bytes that follow the status. Body is meaningful only while the status is // NFS4ERR_OK, and its shape depends on the operation. type ResOp struct { Op uint32 Status uint32 Body []byte } // A CompoundRes is the decoded COMPOUND4res. type CompoundRes struct { Status uint32 Tag string Ops []ResOp } // DecodeCompoundResBodies decodes a COMPOUND4res and, for every operation, // the bytes between its status and the next operation number. A body is // empty when the operation carried no payload or when it failed. The // bodies are not length prefixed on the wire: the length of each follows // from its operation number, so the decoder measures each body as it goes. func DecodeCompoundResBodies(payload []byte) (CompoundRes, [][]byte, error) { d := xdr.NewDecoder(payload) var res CompoundRes var err error if res.Status, err = d.Uint32(); err != nil { return res, nil, err } if res.Tag, err = d.String(); err != nil { return res, nil, err } count, err := d.Uint32() if err != nil { return res, nil, err } if count > maxOps { return res, nil, fmt.Errorf("nfs4: %d results exceeds %d", count, maxOps) } var ops []ResOp var bodies [][]byte for range count { var op ResOp if op.Op, err = d.Uint32(); err != nil { return res, nil, err } if op.Status, err = d.Uint32(); err != nil { return res, nil, err } start := len(payload) - d.Remaining() n, err := resBodyLen(op.Op, op.Status, payload[start:]) if err != nil { return res, nil, err } if n < 0 || start+n > len(payload) { return res, nil, fmt.Errorf("nfs4: result body of %d bytes overruns the payload", n) } op.Body = payload[start : start+n] if _, err := d.Raw(n); err != nil { return res, nil, err } ops = append(ops, op) bodies = append(bodies, op.Body) } if d.Remaining() > 0 { return res, nil, fmt.Errorf("nfs4: %d bytes trail the declared operations", d.Remaining()) } res.Ops = ops return res, bodies, nil } // resBodyLen measures how many bytes one result body occupies. A failed // operation carries no body. An operation this build cannot measure, with a // successful status, is a response the client has no business receiving. func resBodyLen(op, status uint32, body []byte) (int, error) { // Every failed operation carries an empty body, except LOCKT: its // DENIED answer names the conflicting lock. if status != ErrOK && op != OpLockt { return 0, nil } d := xdr.NewDecoder(body) switch op { case OpPutfh, OpPutRootfh, OpLookup, OpLookupp, OpSavefh, OpRestorefh: return 0, nil case OpGetfh: if _, err := d.VarOpaque(); err != nil { return 0, err } case OpGetattr: if _, err := ReadBitmap(d); err != nil { return 0, err } if _, err := d.VarOpaque(); err != nil { return 0, err } case OpAccess: if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } case OpCreate: // changeinfo4 (atomic, before, after) then the bitmap of the // attributes set, RFC 8881 section 18.4.2. if _, err := d.Bool(); err != nil { return 0, err } if _, err := d.Uint64(); err != nil { return 0, err } if _, err := d.Uint64(); err != nil { return 0, err } if _, err := ReadBitmap(d); err != nil { return 0, err } case OpWrite: if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.Raw(8); err != nil { return 0, err } case OpReadlink: // The link text, length prefixed. if _, err := d.VarOpaque(); err != nil { return 0, err } return len(body) - d.Remaining(), nil case OpCommit: // The write verifier. return 8, nil case OpSecinfo, OpSecinfoNoName: // An array of secinfo4 unions: the flavour, and the // rpcsec_gss_info body only on the RPCSEC_GSS branch. n, err := d.Uint32() if err != nil { return 0, err } if n > maxOps { return 0, ErrUnknownOp } for range n { flavor, ferr := d.Uint32() if ferr != nil { return 0, ferr } if flavor == SecFlavorGSS { if _, err := d.VarOpaque(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } } } return len(body) - d.Remaining(), nil case OpRemove, OpOpenDowngrade: // change_info4: an atomic flag and two 64 bit counters. return 20, nil case OpDestroyClientID, OpReclaimComplete, OpPutPubfh, OpReleaseLockOwner, OpDelegReturn, OpFreeStateid, OpBackchannelCtl, OpVerify, OpNverify: return 0, nil case OpBindConnToSession: // sessionid and the granted direction. return 20, nil case OpTestStateid: // One status per stateid the arguments named. n, err := d.Uint32() if err != nil { return 0, err } if n > maxOps { return 0, ErrUnknownOp } return 4 + 4*int(n), nil case OpRename: // Two change_info4 values, one per directory. return 40, nil case OpLink: // change_info4 of the target directory. return 20, nil case OpSequence: // sessionid and the five words of SEQUENCE4resok, RFC 7863. if _, err := d.Raw(16); err != nil { return 0, err } for range 5 { if _, err := d.Uint32(); err != nil { return 0, err } } return 36, nil case OpDestroySession: return 0, nil case OpExchangeID: // clientid, sequence, flags, state protect how, eir_server_owner // (uint64 minor id, opaque major id), eir_server_scope and the // impl_id array, RFC 8881 section 18.35. if _, err := d.Uint64(); err != nil { return 0, err } for range 3 { if _, err := d.Uint32(); err != nil { return 0, err } } if _, err := d.Uint64(); err != nil { return 0, err } for range 2 { // so_major_id then server_scope if _, err := d.VarOpaque(); err != nil { return 0, err } } n, err := d.Uint32() if err != nil { return 0, err } for range n { for range 2 { // domain and name if _, err := d.String(); err != nil { return 0, err } } if _, err := d.Raw(12); err != nil { // nfstime4 return 0, err } } return len(body) - d.Remaining(), nil case OpCreateSession: // sessionid, sequence, flags and two channel attrs. if _, err := d.Raw(16); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } for range 2 { if _, err := d.Uint32(); err != nil { return 0, err } for range 5 { if _, err := d.Uint32(); err != nil { return 0, err } } count, cerr := d.Uint32() if cerr != nil { return 0, cerr } if count != 0xffffffff { for range count { if _, err := d.Uint32(); err != nil { return 0, err } } } } return len(body) - d.Remaining(), nil case OpOpen: // stateid, change_info4, rflags, attrset and the delegation union. if _, err := d.Raw(16); err != nil { return 0, err } if _, err := d.Bool(); err != nil { return 0, err } if _, err := d.Uint64(); err != nil { return 0, err } if _, err := d.Uint64(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } if _, err := ReadBitmap(d); err != nil { return 0, err } delegType, derr := d.Uint32() if derr != nil { return 0, derr } switch delegType { case OpenDelegRead: // stateid, recall flag and the permissions ace. if _, err := d.Raw(16); err != nil { return 0, err } if _, err := d.Bool(); err != nil { return 0, err } if err := walkACE(d); err != nil { return 0, err } case OpenDelegWrite: // stateid, recall flag, the space limit union and the ace. if _, err := d.Raw(16); err != nil { return 0, err } if _, err := d.Bool(); err != nil { return 0, err } if err := walkSpaceLimit(d); err != nil { return 0, err } if err := walkACE(d); err != nil { return 0, err } } return len(body) - d.Remaining(), nil case OpClose: return 16, nil case OpLock, OpLocku: // The stateid. return 16, nil case OpLockt: return resBodyLenLockt(status, body) case OpSetattr: // attrsset: only a bitmap4. if _, err := ReadBitmap(d); err != nil { return 0, err } case OpRead: if _, err := d.Bool(); err != nil { return 0, err } if _, err := d.VarOpaque(); err != nil { return 0, err } case OpReaddir: if _, err := d.Raw(8); err != nil { return 0, err } for { more, err := d.Bool() if err != nil { return 0, err } if !more { break } if _, err := d.Uint64(); err != nil { return 0, err } if _, err := d.String(); err != nil { return 0, err } if _, err := ReadBitmap(d); err != nil { return 0, err } if _, err := d.VarOpaque(); err != nil { return 0, err } } if _, err := d.Bool(); err != nil { return 0, err } case OpLayoutGet: // The layout stateid, the return on close flag and the layout // array: offset, length, IO mode, type and body per segment. if _, err := d.Raw(16); err != nil { return 0, err } if _, err := d.Bool(); err != nil { return 0, err } n, err := d.Uint32() if err != nil { return 0, err } for range n { if _, err := d.Uint64(); err != nil { return 0, err } if _, err := d.Uint64(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.VarOpaque(); err != nil { return 0, err } } case OpLayoutCommit: // The newsize4 union: the changed flag and the size. if _, err := d.Bool(); err != nil { return 0, err } if _, err := d.Uint64(); err != nil { return 0, err } case OpLayoutReturn: // The layoutreturn_stateid4 union: the returned flag and the // stateid. if _, err := d.Bool(); err != nil { return 0, err } if _, err := d.Raw(16); err != nil { return 0, err } case OpSeek: // The end of file flag and the offset. if _, err := d.Bool(); err != nil { return 0, err } if _, err := d.Uint64(); err != nil { return 0, err } case OpAllocate, OpDeallocate, OpClone, OpLayoutError, OpLayoutStats, OpOffloadCancel: return 0, nil case OpIoAdvise: // The accepted hints bitmap4. if _, err := ReadBitmap(d); err != nil { return 0, err } case OpCopy: // write_response4 then the two requirement flags. n, err := d.Uint32() if err != nil { return 0, err } if n == 1 { if _, err := d.Raw(16); err != nil { return 0, err } } if _, err := d.Uint64(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.Raw(8); err != nil { return 0, err } if _, err := d.Bool(); err != nil { return 0, err } if _, err := d.Bool(); err != nil { return 0, err } case OpCopyNotify: // lease time, empty owner confirm, stateid, source list. if _, err := d.Int64(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.Raw(16); err != nil { return 0, err } n, err := d.Uint32() if err != nil { return 0, err } for range n { kind, kerr := d.Uint32() if kerr != nil { return 0, kerr } switch kind { case 1, 2: if _, err := d.String(); err != nil { return 0, err } default: if _, err := d.String(); err != nil { return 0, err } if _, err := d.String(); err != nil { return 0, err } } } case OpOffloadStatus: if _, err := d.Uint64(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } case OpReadPlus: // eof flag and the content array. if _, err := d.Bool(); err != nil { return 0, err } n, err := d.Uint32() if err != nil { return 0, err } for range n { kind, kerr := d.Uint32() if kerr != nil { return 0, kerr } if _, err := d.Uint64(); err != nil { return 0, err } if kind == uint32(ContentData) { if _, err := d.VarOpaque(); err != nil { return 0, err } } else if _, err := d.Uint64(); err != nil { return 0, err } } case OpWriteSame: // write_response4 without a callback arm. if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.Uint64(); err != nil { return 0, err } if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.Raw(8); err != nil { return 0, err } case OpGetDirDelegation: // The non fatal union: the status, then the resok body or the // will signal flag. gdd, err := d.Uint32() if err != nil { return 0, err } if gdd == GddOK { if _, err := d.Raw(24); err != nil { return 0, err } for range 3 { if _, err := ReadBitmap(d); err != nil { return 0, err } } } else if _, err := d.Bool(); err != nil { return 0, err } case OpOpenattr: return 0, nil case OpGetDeviceList: // The cookie, its verifier, the device id array and the eof flag. if _, err := d.Uint64(); err != nil { return 0, err } if _, err := d.Raw(8); err != nil { return 0, err } n, err := d.Uint32() if err != nil { return 0, err } if _, err := d.Raw(int(n) * 16); err != nil { return 0, err } if _, err := d.Bool(); err != nil { return 0, err } case OpGetxattr: // The single opaque value. if _, err := d.VarOpaque(); err != nil { return 0, err } case OpSetxattr, OpRemovexattr: return 0, nil case OpListxattr: // The cookie of the last name, the names and the end of list // flag. if _, err := d.Uint64(); err != nil { return 0, err } n, err := d.Uint32() if err != nil { return 0, err } for range n { if _, err := d.String(); err != nil { return 0, err } } if _, err := d.Bool(); err != nil { return 0, err } case OpGetDeviceInfo: // The address type, the address body and the notification bitmap. if _, err := d.Uint32(); err != nil { return 0, err } if _, err := d.VarOpaque(); err != nil { return 0, err } if _, err := ReadBitmap(d); err != nil { return 0, err } default: return 0, ErrUnknownOp } return len(body) - d.Remaining(), nil } // walkACE walks one nfsace4 of a result body. func walkACE(d *xdr.Decoder) error { for range 3 { if _, err := d.Uint32(); err != nil { return err } } _, err := d.VarOpaque() return err } // walkSpaceLimit walks one nfs_space_limit4 union of a result body. func walkSpaceLimit(d *xdr.Decoder) error { limitBy, err := d.Uint32() if err != nil { return err } switch limitBy { case LimitBySize: _, err = d.Uint64() return err case 2: // NFS_LIMIT_BLOCKS if _, err = d.Uint32(); err != nil { return err } _, err = d.Uint32() return err default: return ErrBadDeleg } } // ErrUnknownOp marks a successful result whose shape this build cannot // measure: the client and the server disagree on what was sent. var ErrUnknownOp = errors.New("nfs4: unknown operation result") // resBodyLenCB measures one CB result body. CB operation numbers share // values with the fore channel, so the two families are never mixed in // one measurement. func resBodyLenCB(op, status uint32, body []byte) (int, error) { if status != ErrOK { return 0, nil } d := xdr.NewDecoder(body) switch op { case OpCBSequence: // The session id and the four words of CB_SEQUENCE4resok, // RFC 7863. if _, err := d.Raw(16); err != nil { return 0, err } for range 4 { if _, err := d.Uint32(); err != nil { return 0, err } } return 32, nil case OpCBRecall, OpCBNotify, OpCBNotifyLock: return 0, nil default: return 0, nil } } // DecodeCompoundResBodiesCB decodes a CB_COMPOUND reply: the same walk as // the fore channel variant, with the callback operation shapes. func DecodeCompoundResBodiesCB(payload []byte) (CompoundRes, [][]byte, error) { d := xdr.NewDecoder(payload) var res CompoundRes var err error if res.Status, err = d.Uint32(); err != nil { return res, nil, err } if res.Tag, err = d.String(); err != nil { return res, nil, err } count, err := d.Uint32() if err != nil { return res, nil, err } if count > maxOps { return res, nil, fmt.Errorf("nfs4: %d results exceeds %d", count, maxOps) } var ops []ResOp var bodies [][]byte for range count { var op ResOp if op.Op, err = d.Uint32(); err != nil { return res, nil, err } if op.Status, err = d.Uint32(); err != nil { return res, nil, err } start := len(payload) - d.Remaining() n, err := resBodyLenCB(op.Op, op.Status, payload[start:]) if err != nil { return res, nil, err } if n < 0 || start+n > len(payload) { return res, nil, fmt.Errorf("nfs4: result body of %d bytes overruns the payload", n) } op.Body = payload[start : start+n] if _, err := d.Raw(n); err != nil { return res, nil, err } ops = append(ops, op) bodies = append(bodies, op.Body) } if d.Remaining() > 0 { return res, nil, fmt.Errorf("nfs4: %d bytes trail the declared operations", d.Remaining()) } res.Ops = ops return res, bodies, nil }