{"record":{"id":"ec270604fefbaea0","repo":"microsoft/typescript-go","slug":"invalid-ast-header-offsets-offsets-exceed-data-le","errorCode":null,"errorMessage":"invalid AST header offsets: offsets exceed data length (%d)","messagePattern":"invalid AST header offsets: offsets exceed data length \\((.+?)\\)","errorType":"validation","errorClass":"ErrClientError","httpStatus":null,"severity":"error","filePath":"internal/api/encoder/decoder.go","lineNumber":71,"sourceCode":"func newASTDecoder(data []byte) (*astDecoder, error) {\n\tif len(data) < HeaderSize {\n\t\treturn nil, fmt.Errorf(\"data too short for header: %d bytes\", len(data))\n\t}\n\tversion := data[HeaderOffsetMetadata+3]\n\tif version != ProtocolVersion {\n\t\treturn nil, fmt.Errorf(\"unsupported protocol version %d (expected %d)\", version, ProtocolVersion)\n\t}\n\n\tstrTable := readLE32(data, HeaderOffsetStringOffsets)\n\tstrData := readLE32(data, HeaderOffsetStringData)\n\textData := readLE32(data, HeaderOffsetExtendedData)\n\tnodeOff := readLE32(data, HeaderOffsetNodes)\n\n\tdataLen := uint32(len(data))\n\n\t// Validate that all offsets are within the buffer.\n\tif strTable > dataLen || strData > dataLen || extData > dataLen || nodeOff > dataLen {\n\t\treturn nil, fmt.Errorf(\"invalid AST header offsets: offsets exceed data length (%d)\", dataLen)\n\t}\n\n\t// Validate monotonic non-decreasing order of regions.\n\tif !(strTable <= strData && strData <= extData && extData <= nodeOff) {\n\t\treturn nil, fmt.Errorf(\"invalid AST header offsets: expected strTable <= strData <= extData <= nodeOff (got %d, %d, %d, %d)\", strTable, strData, extData, nodeOff)\n\t}\n\n\td := &astDecoder{\n\t\traw:      data,\n\t\tstrTable: strTable,\n\t\tstrData:  strData,\n\t\textData:  extData,\n\t\tnodeOff:  nodeOff,\n\t\tfactory:  ast.NewNodeFactory(ast.NodeFactoryHooks{}),\n\t}\n\n\td.nodeCount = (len(data) - int(d.nodeOff)) / NodeSize\n","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/microsoft/typescript-go/blob/1bcfa18d79a3be41772223d5c05dfe4480e614ff/internal/api/encoder/decoder.go#L53-L89","documentation":"One of the four region offsets read from the header (string table, string data, extended data, nodes) points past the end of the buffer. The decoder refuses to slice beyond len(data), so decoding aborts before any node is materialized.","triggerScenarios":"Corrupted or tail-truncated blob; buffer assembled from mismatched pieces of two encodes; offsets damaged by an endian-confused custom producer.","commonSituations":"Files truncated by full disks or interrupted writes; caches partially overwritten; network transport dropping bytes when no length prefix is used.","solutions":["Re-encode and compare lengths - a shorter blob identifies the truncating writer","Validate the blob's checksum (the header reserves hash words) before decoding","Write blobs atomically (temp file + rename) to prevent partial writes","If corruption is persistent, re-parse from source to regenerate the AST"],"exampleFix":"// before\nos.WriteFile(path, blob, 0o644) // partial write possible\n\n// after\ntmp := path + \".tmp\"\nos.WriteFile(tmp, blob, 0o644)\nos.Rename(tmp, path) // atomic publish","handlingStrategy":"validation","validationCode":"if len(data) < encoder.HeaderSize { return errTooShort }\noff := func(i int) uint32 { return binary.LittleEndian.Uint32(data[i*4:]) }\nfor _, o := range []int{encoder.HeaderOffsetStringOffsets, encoder.HeaderOffsetStringData, encoder.HeaderOffsetExtendedData, encoder.HeaderOffsetNodes} {\n\tif int(off(o)) > len(data) { return errCorrupt }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Write encoded blobs atomically (temp + rename)","Checksum cached blobs and validate before decode","Never hand-edit or append to encoded files"],"tags":["go","corruption","truncation","binary","offsets","atomic-write"],"backgroundTag":null,"analyzedSha":"1bcfa18d79a3be41772223d5c05dfe4480e614ff","analyzedAt":"2026-08-16T02:12:00.115Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}