anacrolix / anacrolix/torrent

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

Đang mở Phù hợp với người mới
#1,108 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
Go
Star
6.1k
Fork
690
Merge trung bình
3 ngày 10 giờ
Pull request đã merge (30 ngày)
12

Mô tả

**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`.

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
go
Lĩnh vực
backend
Loại issue
Lỗi
Độ khó
2/5
Thời gian dự kiến
1-3 giờ
Mức độ hoạt động
Sôi nổi
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
88/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.