anacrolix / anacrolix/torrent

File.NewReader() returns bytes past the end of the file when the buffer exceeds what remains

未关闭 适合新手
#1,108 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
主要语言
Go
星标
6.1k
派生
690
平均合并
3 天 10 小时
30 天内合并 PR
12

描述

**Version:** v1.61.0 (newest tag) and master b682e476 (2026-09-07). Go 1.26, linux/amd64.

**What happens**

A reader from `File.NewReader()` on a multi-file torrent returns more bytes than the file contains when the caller's buffer is larger than the remaining length of the file. The extra bytes are the next file's data from the same piece.

With a torrent whose first file `a.txt` is 5 bytes and second file `b.txt` is 100,000 bytes (both in piece 0):

```
File.Length()=5, io.ReadAll(File.NewReader()) returned 512 bytes: "smallBBBBBBB"...
single Read into a 64-byte buffer at pos 0 returned n=64 (file is 5 bytes)
```

`io.ReadAll` gets 512 bytes because its first read uses a 512-byte buffer; the reader fills all of it, then reports EOF on the next call because `pos >= length`.

**Expected**

A reader scoped to a file should never return bytes beyond `File.Length()`: a 5-byte file should read back as exactly 5 bytes, and `Read` into a 64-byte buffer at position 0 should return `n=5`.

**Likely cause**

`reader.readAt` (reader.go) refuses to *start* a read at `pos >= r.length`, and `readContext` reports EOF once `r.pos >= r.length` after the read, but nothing clips `len(b)` to `r.length - pos` before `readOnceAt` reads from storage. Since storage is piece-addressed, the read runs into the following file within the piece.

This looks like the same gap diagnosed in #415 (2020), where the maintainer noted that "Reads on File Readers aren't limited to the end of the file, that logic is missing" and sketched a clip of `b` to `r.length-pos`. That issue was closed without the change landing; the reproduction below shows the behaviour on v1.61.0 and current master with the default file storage and no custom pieces.

**Minimal reproduction** (self-contained test; no network)

```go
package repro

import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"time"

"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/bencode"
"github.com/anacrolix/torrent/metainfo"
)

func TestFileReaderStopsAtFileEnd(t *testing.T) {
data := t.TempDir()
root := filepath.Join(data, "t")
_ = os.MkdirAll(root, 0o755)
_ = os.WriteFile(filepath.Join(root, "a.txt"), []byte("small"), 0o644)
_ = os.WriteFile(filepath.Join(root, "b.txt"), bytes.Repeat([]byte("B"), 100_000), 0o644)
var info metainfo.Info
if err := info.BuildFromFilePath(root); err != nil {
t.Fatal(err)
}
ib, _ := bencode.Marshal(info)
mi := &metainfo.MetaInfo{InfoBytes: ib}

cfg := torrent.NewDefaultClientConfig()
cfg.DataDir = data
cfg.NoDHT, cfg.DisableTrackers, cfg.DisableTCP, cfg.DisableUTP, cfg.NoDefaultPortForwarding = true, true, true, true, true
cfg.ListenPort = 0
cl, err := torrent.NewClient(cfg)
if err != nil {
t.Fatal(err)
}
defer cl.Close()
tor, err := cl.AddTorrent(mi)
if err != nil {
t.Fatal(err)
}
<-tor.GotInfo()
for deadline := time.Now().Add(10 * time.Second); tor.BytesMissing() > 0; time.Sleep(50 * time.Millisecond) {
if time.Now().After(deadline) {
t.Fatal("local data never verified")
}
}

f := tor.Files()[0] // a.txt, 5 bytes
r := f.NewReader()
defer r.Close()
got, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
if int64(len(got)) != f.Length() {
t.Fatalf("reader returned %d bytes for a %d-byte file: %q...", len(got), f.Length(), got[:12])
}
}
```

Output: `reader returned 512 bytes for a 5-byte file: "smallBBBBBBB"...`

**Suggested fix**

In `reader.readAt` (or `readContext`), clip the buffer before reading: `if rem := r.length - pos; int64(len(b)) > rem { b = b[:rem] }`. Callers can work around it today by reading exactly `f.Length()` bytes with `io.ReadFull`.

贡献指南

这个仓库没有索引到贡献指南

调研方向

Start in reader.go at File.NewReader, then trace readAt, readContext, and readOnceAt to see where the requested buffer is read. Reproduce the issue with the self-contained test from the report, covering both io.ReadAll and an oversized buffer. Done means reads stop at File.Length() and return exactly the file's remaining bytes without data from the next file.

由索引模型根据 Issue 内容生成。

评估

技术栈
go
领域
backend
Issue 类型
缺陷
难度
2/5
预计耗时
1-3 小时
活跃度
活跃
描述清晰度
描述清楚
新手友好度
88/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。