anacrolix / anacrolix/torrent

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

Aperta
#1,108 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
Lingua principale
Go
Stelle
6.1k
Fork
690
Merge medio
3g 10h
PR unite (30g)
12

Descrizione

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

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.