fix: charge the decompression budget at the decoder, not per entry

Draining skipped entries still left a hole: a tar entry whose header doesn't read as a regular file
(a socket or directory per its mode bits, say) can carry a body, and the tar reader decompresses it
to reach the next header without anything being charged or ctx being checked.

- compressed tars are now read through a reader that charges the budget for every byte the decoder
  produces and checks ctx per read, so no skip path inside the tar reader gets around it
- entry content from that stream isn't charged a second time, and the per-entry drain is gone
- zip and plain tar are unchanged

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-10-21 11:45:34 -04:00
parent c4c0907d4e
commit 36d417d52a
4 changed files with 65 additions and 40 deletions
+17 -18
View File
@@ -44,7 +44,8 @@ func (r *Resolver) put(b *blob, content io.Reader) error {
for {
n, readErr := content.Read(r.chunk)
if n > 0 {
if !r.budget.take(int64(n)) {
// content read through a meteredReader was charged as it was decoded
if !r.metered && !r.budget.take(int64(n)) {
return errBudgetSpent
}
if err := r.write(b, r.chunk[:n]); err != nil {
@@ -62,25 +63,23 @@ func (r *Resolver) put(b *blob, content io.Reader) error {
return nil
}
// drain reads the rest of a streaming entry without keeping it, charging the decompression budget for
// every byte, so skipping an entry is not a way around the budget. A read error ends the drain quietly;
// the archive reader reports a broken stream at the next entry.
func (r *Resolver) drain(ctx context.Context, content io.Reader) error {
if r.chunk == nil {
r.chunk = make([]byte, copyChunkSize)
// meteredReader charges the decompression budget for every byte read through it and stops once ctx is
// done, so nothing a decoder produces gets around the budget.
type meteredReader struct {
ctx context.Context
reader io.Reader
budget *Budget
}
func (m *meteredReader) Read(p []byte) (int, error) {
if err := m.ctx.Err(); err != nil {
return 0, err
}
for {
if err := ctx.Err(); err != nil {
return err
}
n, err := content.Read(r.chunk)
if n > 0 && !r.budget.take(int64(n)) {
return errBudgetSpent
}
if err != nil {
return nil
}
n, err := m.reader.Read(p)
if n > 0 && !m.budget.take(int64(n)) {
return 0, errBudgetSpent
}
return n, err
}
// write appends p to b: into memory while the memory limit admits it, otherwise into the spill file