// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package nfs4 import ( "testing" "sourcedock.dev/petrbalvin/nfs/internal/xdr" ) // FuzzDecodeCompound feeds arbitrary compound payloads through the // argument and result walkers: no input may panic, and every malformed // body must arrive as an error, never as a wrong split. func FuzzDecodeCompound(f *testing.F) { args := AppendCompoundArgs(nil, "seed", MinorVersion, [][]byte{ AppendSequenceArgs(nil, SessionID{1}, 1, 0, 0, true), AppendPutRootfh(nil), AppendLookup(nil, "a"), }) f.Add(args) res := AppendCompoundRes(nil, ErrOK, "seed", [][]byte{ append(AppendOpHeader(nil, OpSequence, ErrOK), AppendSequenceRes(nil, SessionID{1}, 1, 0, 0, 0)...), AppendOpHeader(nil, OpPutRootfh, ErrOK), }) f.Add(res) f.Add([]byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 75, 0, 0, 0, 0}) f.Add([]byte{0, 0, 0, 9, 0, 0, 0, 1, 0, 0, 0, 1}) f.Fuzz(func(t *testing.T, data []byte) { // The property under test is that none of this panics; a // malformed body is an ordinary error and a well formed one // decodes with bodies that stay inside the payload. _, d, err := DecodeCompoundArgs(data) if err == nil { for { op, oerr := d.Uint32() if oerr != nil || walkArgs(op, d) { break } } } _, _, _ = DecodeCompoundResBodies(data) _, _, _ = DecodeCompoundResBodiesCB(data) }) } // walkArgs walks one operation's arguments the way the dispatcher does, // so the fuzzed stream exercises the real decoders. The boolean reports // that the stream ended or was refused. func walkArgs(op uint32, d *xdr.Decoder) bool { switch op { case OpExchangeID: _, err := DecodeExchangeIDArgs(d) return err != nil case OpCreateSession: _, err := DecodeCreateSessionArgs(d) return err != nil case OpSequence: _, err := DecodeSequenceArgs(d) return err != nil case OpSecinfoNoName: _, err := DecodeSecinfoNoNameArgs(d) return err != nil } _, err := d.Uint32() return err != nil } // TestRegistryValues pins the registry numbers this package speaks // against the standards: an accidental renumbering of any of these // breaks interoperation with every conformant peer, so the values // themselves are the contract. func TestRegistryValues(t *testing.T) { pins := []struct { name string got uint32 want uint32 rfc string }{ {"session id size", uint32(len(SessionID{})), 16, "RFC 7863"}, {"FATTR4_TIME_ACCESS_SET", AttrTimeAccessSet, 48, "RFC 7863"}, {"FATTR4_TIME_MODIFY_SET", AttrTimeModifySet, 54, "RFC 7863"}, {"LAYOUT4_FLEX_FILES", LayoutTypeFlexfiles, 4, "RFC 8435"}, {"LAYOUT4_NFSV4_1_FILES", LayoutTypeFiles, 1, "RFC 7863"}, {"LAYOUT4_OSD2_OBJECTS", LayoutTypeObjects, 2, "RFC 7863"}, {"LAYOUT4_BLOCK_VOLUME", LayoutTypeBlock, 3, "RFC 7863"}, {"layoutiomode4 RW", IoModeRW, 2, "RFC 7863"}, {"layoutiomode4 ANY", IoModeAny, 3, "RFC 7863"}, {"SP4_MACH_CRED", StateProtectMachCred, 1, "RFC 8881"}, {"SP4_SSV", StateProtectSSV, 2, "RFC 8881"}, {"SECINFO_STYLE4_CURRENT_FH", StyleCurrentFH, 0, "RFC 8881"}, {"SECINFO_STYLE4_PARENT", StyleParent, 1, "RFC 8881"}, {"NFS4ERR_NXIO", ErrNXIO, 6, "RFC 8881"}, {"NFS4ERR_BADOWNER", ErrBadOwner, 10093, "RFC 8881"}, {"NFS4ERR_NOXATTR", ErrNoXattr, 10095, "RFC 8276"}, {"NFS4ERR_XATTR2BIG", ErrXattr2Big, 10096, "RFC 8276"}, } for _, p := range pins { if p.got != p.want { t.Errorf("%s: got %d, want %d per %s", p.name, p.got, p.want, p.rfc) } } }