Skip to content

[dfs][nfs] bound READ copy length to request and data_len - #12012

Merged
Rbb666 merged 1 commit into
RT-Thread:masterfrom
cms19859230182-lang:fix/dfs-nfs-read-count-bounds
Nov 12, 2026
Merged

Rbb666 merged 1 commit into
RT-Thread:masterfrom
cms19859230182-lang:fix/dfs-nfs-read-count-bounds

Conversation

@cms19859230182-lang

@cms19859230182-lang cms19859230182-lang commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

中文

问题

NFS READ 曾信任服务器 count 做 memcpy,可越过调用方缓冲与已解码数据(#11917)。同函数里,部分读成功后再 RPC 失败会把 total 清零,调用方看到 0 字节却丢了 offset;另有过期 eof 占位注释。

改动

  • 拷贝长度收敛为 min(请求, 已解码 data_len, 服务器 count),xdr READ opaque 上限与请求侧共用 DFS_NFS_MAX_MTU
  • 按实际字节推进 buf / count / offset;WRITE 短写同样按实际 bytes 推进
  • RPC 失败时若已有成功字节则返回已读长度;清理过期 eof 注释

验证

静态核对长度收敛与失败路径返回值;未跑板、未做恶意 NFS PoC。

English

Problem

NFS READ trusted the server count for memcpy, which could overrun the caller buffer and decoded opaque (#11917). In the same path, a later RPC failure zeroed total after a partial success, and a stale eof placeholder comment remained.

Changes

  • Clamp the copy to min(request, decoded data_len, server count); share DFS_NFS_MAX_MTU with the xdr READ opaque cap
  • Advance buf / count / offset by actual bytes; same for short WRITE
  • On RPC failure after a partial read, return the bytes already copied; remove the stale eof comment

Test plan

Static check of the clamp and failure return path; no board run and no malicious NFS PoC.

Fixes #11917

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

@BernardXiong BernardXiong left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论 / Verdict: 修改方向正确,未发现新的阻塞问题。 / The fix is sound; no new blocking issue found.

已验证 / Verified

  • nfs_read() 将拷贝长度限制为 min(server count, args.count, data_len),memcpy 不会越过调用者缓冲区或实际解码的数据;buf / count / offset 按实际拷贝字节推进,短读不会再跳数据。 / Copy length is bounded by both the request and the decoded opaque length; pointers advance by actual bytes.
  • xdr_READ3resok() 把 READ opaque 上限收紧为 1024,超大 READ 回复会解码失败。 / XDR decode rejects READ payloads larger than 1024.
  • WRITE3resok.count 被 clamp 到提交长度,服务端无法把写偏移推进到未发送的位置。 / Write offset cannot advance past submitted length.
  • CI:104 success / 2 skipped。

建议(非阻塞) / Non-blocking suggestions

  1. nfs_xdr.c:21 的 NFS_READ_MAX_DATA 与 dfs_nfs.c:22 的 DFS_NFS_MAX_MTU 重复。建议抽到共享头文件复用,避免以后只改一处导致合法 READ 被解码拒绝。 / Duplicated 1024 limit; share one definition to avoid future divergence.
  2. dfs_nfs.c:677-678 在发送前按 args.count 推进 buf / count,而 NFS 允许 WRITE3resok.count < args.count。发生合法的短写时,未写出的尾部会被跳过,下一轮会把后续数据写到更小的 offset,造成数据错位。这是既有逻辑,但既然本 PR 已处理 WRITE3resok.count,建议一并按实际 bytes 推进并在非完整写入时重试剩余数据。 / On a legal short write, the unwritten tail is skipped and later data lands at the wrong offset. Pre-existing, but worth fixing together with the new clamp.

cms19859230182-lang added a commit to cms19859230182-lang/rt-thread that referenced this pull request Nov 10, 2026
Address BernardXiong's non-blocking review comments on RT-Thread#12012:

- Move DFS_NFS_MAX_MTU into dfs_nfs.h and use it both to cap WRITE/READ
  request sizes and as the opaque length limit in xdr_READ3resok(),
  replacing the duplicated NFS_READ_MAX_DATA definition. The request cap
  and reply decoder can no longer drift apart.
- In nfs_write(), stop advancing buf/count by args.count before the RPC.
  WRITE3resok.count may legitimately be smaller than the submitted count;
  now buf/count/offset/total advance by the clamped accepted byte count,
  so the unacknowledged tail is retried at the correct offset instead of
  being skipped. A zero-progress reply breaks the loop instead of spinning
  forever and returns the bytes accepted so far.
@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

📌 Code Review Assignment

🏷️ Tag: components

Reviewers: @Maihuanyi

Changed Files (Click to expand)
  • components/dfs/dfs_v1/filesystems/nfs/dfs_nfs.c
  • components/dfs/dfs_v1/filesystems/nfs/dfs_nfs.h
  • components/dfs/dfs_v1/filesystems/nfs/nfs_xdr.c

📊 Current Review Status (Last Updated: 2026-11-12 00:39 CST)


📝 Review Instructions

  1. 维护者可以通过单击此处来刷新审查状态: 🔄 刷新状态
    Maintainers can refresh the review status by clicking here: 🔄 Refresh Status

  2. 确认审核通过后评论 LGTM/lgtm
    Comment LGTM/lgtm after confirming approval

  3. PR合并前需至少一位维护者确认
    PR must be confirmed by at least one maintainer before merging

ℹ️ 刷新CI状态操作需要具备仓库写入权限。
ℹ️ Refresh CI status operation requires repository Write permission.

@BernardXiong BernardXiong left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conclusion: sound; no blocking issue found.

Verified on 30968ae8:

  • nfs_write() clamps WRITE3resok.count to the submitted length and advances buf / count only by the accepted bytes, so a short write retries the tail at the correct offset and a zero-count reply cannot spin.
  • READ and WRITE now share DFS_NFS_MAX_MTU from dfs_nfs.h; the include is valid, and xdr_READ3resok() caps the decoded opaque at 1024.
  • The new bytes == 0 break still reaches the single outer xdr_free() (the bundled xdr_bytes() clears the pointer in XDR_FREE mode), so there is no double free on this path.
  • CI: 107 pass / 3 skipping.

@Yunlk

Yunlk commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

看了一遍实现,核心修法是对的。下面是我逐条核过的结论,加两个观察(都不是本次改动引入的,但正好在这个函数里)。

核对通过的部分

1. 拷贝长度收敛正确

bytes = res.READ3res_u.resok.count;
if (bytes > args.count)                bytes = args.count;          /* 不超请求 */
if (bytes > res...data.data_len)       bytes = data.data_len;       /* 不超已解码 */

args.count = min(count, DFS_NFS_MAX_MTU),而 data_len 被 xdr 上限压到 1024,所以 bytes <= min(count, 1024),memcpy(buf, ..., bytes) 不可能越过调用方缓冲区。原 issue 里"服务器说 4096 就拷 4096"的路径堵住了。

2. xdr_READ3resok 的上限改动是自洽的

xdr_bytes(..., DFS_NFS_MAX_MTU)   /* 原来是 ~0 */

xdr.c:447 是 if ((nodesize > maxsize) && (xdrs->x_op != XDR_FREE))——上限只作用于 DECODE/ENCODE,不影响 FREE。而且 xdr_bytes 在 DECODE 时按 nodesize 分配,上限 1024 与请求侧 args.count <= 1024 一致,不会出现"合法回复被拒"。

3. 零进度保护

bytes == 0 时 break,避免了"服务器返回 count=0 但 eof=false"导致的空转。

4. buf / count 推进改对了

原来 buf += args.count(推进的是"请求"的量),现在 buf += bytes、count -= bytes(推进的是"实际拿到"的量)。写路径同样从"先扣减再发"改成"成功后按 bytes 推进",短写的重传偏移也正确。

两个观察(master 上已存在,非本 PR 引入)

a) 部分读成功后再出错,total 被清零

if (nfsproc3_read_3(...) != RPC_SUCCESS)
{
    total = 0;      /* 前几轮已经拷进调用方 buf、fd->offset 也推进了 */
    break;
}

假设第一轮读了 512 字节成功,第二轮 RPC 失败:buf 里已有 512 字节、fd->offset 已经 +512,但函数返回 0。调用方看到"读了 0 字节"却丢了文件位置。更合理的做法是保留 total(返回已读字节数),或者在 total == 0 时返回 -1。这属于语义层面的调整,可以放在单独的 PR,我只是正好看到。

b) if (res...eof) { /* something should probably be here */ } 这个注释过期了

下面紧跟着 fd->eof = TRUE; break;,已经是处理过的了。那句占位注释容易让人以为还没实现。

两点确认(我一开始怀疑过,查完发现不是问题)

  • 头文件保护宏不冲突:nfs.h 是 _NFS_H_RPCGEN,dfs_nfs.h 是 __NFS_H__,nfs_xdr.c 同时 include 两者没问题。
  • xdr_free 调了两次不会 double free:循环内一次(:636)、循环外一次(:640),但 xdr.c:472-475 的 FREE 分支会 *cpp = NULL,第二次是 no-op。这段结构是 master 就有的。

关于验证

PR 里写的是"组件级编译 + 静态检查,未运行 exploit"。这条 issue 的价值恰恰在于能实际打穿,所以我建议补一个能跑的证据:

  • qemu-vexpress-a9 有 lan9118,用户态网络可以起一个假 NFS 服务端
  • 最小验证:服务端对 READ 返回 count = 0x7FFFFFFF、data_len = 8,看修复前 memcpy 会不会读到 data_val 之外

如果你短期内做不了,我可以按类似思路补一个宿主侧的 PoC(参照我给 #12016 做的做法:构造畸形回复、对比修复前后),但需要先确认没人重复劳动。

我没有把这个 PR 的改动拿下来跑,只说静态结论。

Fix RT-Thread#11917

nfs_read() used the server-controlled READ3resok.count as the memcpy
length without checking it against the request or the decoded data.

Clamp each copy to min(server count, args.count, data.data_len), advance
buf and the remaining count by the bytes actually copied, and break out
when a reply makes no progress so the loop cannot spin.

Share DFS_NFS_MAX_MTU (1024) in dfs_nfs.h for both READ/WRITE request
caps and the xdr_READ3resok() opaque decode limit, replacing the
separate NFS_READ_MAX_DATA definition. In nfs_write(), clamp
WRITE3resok.count to the submitted length and advance buf/count/offset
only by the accepted bytes so short writes retry the tail correctly and
zero-count replies stop the loop.

Follow-up for Yunlk's review observations on nfs_read():
- When an RPC or NFS status failure occurs after earlier iterations
  already copied bytes and advanced fd->offset, keep total and return
  the bytes copied so far instead of resetting total to 0; only set
  total = -1 when total == 0.
- Remove the stale placeholder comment inside the eof handling branch
  where fd->eof = TRUE and break are already performed.
@cms19859230182-lang
cms19859230182-lang force-pushed the fix/dfs-nfs-read-count-bounds branch from 30968ae to e7375ad Compare November 11, 2026 15:14
@cms19859230182-lang

Copy link
Copy Markdown
Contributor Author

@Yunlk 已按两条观察压入单提交更新(e7375adf):

  • a) nfs_read() 中后继轮次出现 RPC 或 NFS3_OK 检查失败时,若 total > 0 则保留并返回已拷贝字节数,仅当 total == 0 时设为 -1;
  • b) 已移除 eof 分支内过期的占位注释。

恶意 NFS 服务端 PoC 本轮不做,上述长度边界静态结论仍然成立。

@github-actions

Copy link
Copy Markdown

👋 感谢您对 RT-Thread 的贡献!Thank you for your contribution to RT-Thread!

为确保代码符合 RT-Thread 的编码规范,请在你的仓库中执行以下步骤运行代码格式化工作流(如果格式化CI运行失败)。
To ensure your code complies with RT-Thread's coding style, please run the code formatting workflow by following the steps below (If the formatting of CI fails to run).


🛠 操作步骤 | Steps

  1. 前往 Actions 页面 | Go to the Actions page
    点击进入工作流 → | Click to open workflow →

  2. 点击 Run workflow | Click Run workflow

  • Use workflow from 保持默认分支(通常为 master)
    Keep the default branch (usually master) in Use workflow from
  • 在 branch 输入框填写 PR 分支 fix/dfs-nfs-read-count-bounds
    Enter PR branch fix/dfs-nfs-read-count-bounds in the branch field
  • 设置需排除的文件/目录(目录请以"/"结尾)
    Set files/directories to exclude (directories should end with "/")
  1. 等待工作流完成 | Wait for the workflow to complete
    格式化后的代码将作为独立提交推送至你的分支。
    The formatting changes will be pushed to your branch as a separate commit.

完成后,提交将自动更新至 fix/dfs-nfs-read-count-bounds 分支,关联的 Pull Request 也会同步更新。
Once completed, commits will be pushed to the fix/dfs-nfs-read-count-bounds branch automatically, and the related Pull Request will be updated.

如有问题欢迎联系我们,再次感谢您的贡献!💐
If you have any questions, feel free to reach out. Thanks again for your contribution!

@Rbb666
Rbb666 merged commit 2e2ec01 into RT-Thread:master Nov 12, 2026
112 of 113 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] dfs nfs: nfs_read uses the server READ count as memcpy length and overwrites the return address

4 participants