microsoft/typescript-go · error · ErrClientError

invalid AST header offsets: expected strTable <= strData <=

Error message

invalid AST header offsets: expected strTable <= strData <= extData <= nodeOff (got %d, %d, %d, %d)

What it means

The four region offsets are individually in-bounds but violate the required layout strTable <= strData <= extData <= nodeOff. The format mandates contiguous ordered regions, so violation means the header bytes are effectively garbage rather than a decoder bug.

Source

Thrown at internal/api/encoder/decoder.go:76

	if version != ProtocolVersion {
		return nil, fmt.Errorf("unsupported protocol version %d (expected %d)", version, ProtocolVersion)
	}

	strTable := readLE32(data, HeaderOffsetStringOffsets)
	strData := readLE32(data, HeaderOffsetStringData)
	extData := readLE32(data, HeaderOffsetExtendedData)
	nodeOff := readLE32(data, HeaderOffsetNodes)

	dataLen := uint32(len(data))

	// Validate that all offsets are within the buffer.
	if strTable > dataLen || strData > dataLen || extData > dataLen || nodeOff > dataLen {
		return nil, fmt.Errorf("invalid AST header offsets: offsets exceed data length (%d)", dataLen)
	}

	// Validate monotonic non-decreasing order of regions.
	if !(strTable <= strData && strData <= extData && extData <= nodeOff) {
		return nil, fmt.Errorf("invalid AST header offsets: expected strTable <= strData <= extData <= nodeOff (got %d, %d, %d, %d)", strTable, strData, extData, nodeOff)
	}

	d := &astDecoder{
		raw:      data,
		strTable: strTable,
		strData:  strData,
		extData:  extData,
		nodeOff:  nodeOff,
		factory:  ast.NewNodeFactory(ast.NodeFactoryHooks{}),
	}

	d.nodeCount = (len(data) - int(d.nodeOff)) / NodeSize

	// Convert entire string data region to a single Go string upfront.
	// Substringing a Go string shares the backing array, so subsequent
	// getString calls produce substrings with zero allocations.
	d.allStringData = string(data[d.strData:])

View on GitHub (pinned to 1bcfa18d79)

Solutions

  1. Confirm you opened the artifact actually written by the encoder, not a same-named file of another format
  2. Regenerate the blob from the parsed source file
  3. If fuzzing, treat as invalid input and skip - both header checks exist to reject this safely
  4. Add a magic/version prefix to your cache entries so wrong-format files fail earlier

Example fix

// before
blob, _ := os.ReadFile(cachePath) // may be any file

// after
blob, _ := os.ReadFile(cachePath)
if len(blob) < 4 || string(blob[:4]) != "tsast" { return errors.New("not an AST blob") }
Defensive patterns

Strategy: validation

Validate before calling

o1 := binary.LittleEndian.Uint32(data[encoder.HeaderOffsetStringOffsets:])
o2 := binary.LittleEndian.Uint32(data[encoder.HeaderOffsetStringData:])
o3 := binary.LittleEndian.Uint32(data[encoder.HeaderOffsetExtendedData:])
o4 := binary.LittleEndian.Uint32(data[encoder.HeaderOffsetNodes:])
if !(o1 <= o2 && o2 <= o3 && o3 <= o4) { return errNotASTBlob }

Prevention

When it happens

Trigger: Random or text bytes that pass the length check; a blob whose header alone was overwritten; endianness mistakes in a non-reference producer writing big-endian offsets.

Common situations: Decoding a file that was never an encoded AST (config, log) due to a path mix-up; memory corruption in fuzzing harnesses; byte-swapped copies through tools that assume big-endian.

Related errors


AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16). Data as JSON: /api/errors/2942521d4a7a52c8. Report an issue: GitHub.