FiloSottile/age · error

offset out of range [0:%d]: %d

Error message

offset out of range [0:%d]: %d

What it means

DecryptReaderAt.ReadAt rejects offsets outside the valid plaintext range [0, size]. This mirrors io.ReaderAt conventions: off must be >= 0 and <= plaintext size (off == size is allowed and yields io.EOF for non-empty reads). The library returns a descriptive range error rather than panicking or wrapping negative indices.

Source

Thrown at internal/stream/stream.go:414

		return nil, fmt.Errorf("failed to read final chunk: %w", err)
	}
	nonce := nonceForChunk(finalChunkIndex)
	setLastChunkFlag(nonce)
	plaintext, err := aead.Open(finalChunk[:0], nonce[:], finalChunk, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to decrypt and authenticate final chunk: %w", err)
	}
	cache := &cachedChunk{off: finalChunkOff, data: plaintext}

	plaintextSize := size - chunks*chacha20poly1305.Overhead
	r := &DecryptReaderAt{a: aead, src: src, size: plaintextSize, chunks: chunks}
	r.cache.Store(cache)
	return r, nil
}

func (r *DecryptReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
	if off < 0 || off > r.size {
		return 0, fmt.Errorf("offset out of range [0:%d]: %d", r.size, off)
	}
	if len(p) == 0 {
		return 0, nil
	}
	var cacheUpdate *cachedChunk
	chunk := make([]byte, encChunkSize)
	for len(p) > 0 && off < r.size {
		chunkIndex := off / ChunkSize
		chunkOff := chunkIndex * encChunkSize
		encSize := r.size + r.chunks*chacha20poly1305.Overhead
		chunkSize := min(encSize-chunkOff, encChunkSize)

		cached := r.cache.Load()
		var plaintext []byte
		if cached != nil && cached.off == chunkOff {
			plaintext = cached.data
			cacheUpdate = nil
		} else {

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Clamp or validate off against the plaintext size returned by stream.PlaintextSize(encryptedSize) before calling ReadAt.
  2. Use io.NewSectionReader(r, 0, plaintextSize) to get automatic bounds enforcement and correct EOF semantics.
  3. Check you are not passing ciphertext offsets; ReadAt operates in plaintext coordinates.
  4. Fix loops so they terminate at off == size (which yields io.EOF) rather than advancing past it.

Example fix

// before
buf := make([]byte, 100)
n, err := dr.ReadAt(buf, -1)
// after
if off < 0 || off > size {
    return 0, fmt.Errorf("offset %d out of range", off)
}
n, err := dr.ReadAt(buf, off)
Defensive patterns

Strategy: validation

Validate before calling

plainSize, err := stream.PlaintextSize(encryptedSize)
if err != nil { return err }
if off < 0 || off > plainSize {
    return fmt.Errorf("offset %d out of [0:%d]", off, plainSize)
}

Try / catch

n, err := dr.ReadAt(p, off)
if err != nil && !errors.Is(err, io.EOF) {
    if strings.Contains(err.Error(), "offset out of range") {
        return fmt.Errorf("caller bug: bad offset %d", off)
    }
    return err
}

Prevention

When it happens

Trigger: Calling ReadAt(p, off) with off < 0; or with off > r.size (beyond the plaintext length). Note off == r.size is accepted but immediately hits the EOF path once len(p) > 0.

Common situations: io.SectionReader or offset math gone wrong; passing the ciphertext offset instead of the plaintext offset; off-by-one loops with stale size values; concurrent readers sharing a stale size after a re-encryption.

Related errors


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/c522ed0b25f5767a. Report an issue: GitHub.