{"record":{"id":"a5bc363d2ab4f8af","repo":"XTLS/Xray-core","slug":"prefix-length-d-splits-chunk-d","errorCode":null,"errorMessage":"prefix length %d splits chunk %d","messagePattern":"prefix length (.+?) splits chunk (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"transport/internet/finalmask/xmc/padding.go","lineNumber":294,"sourceCode":"\tif variantIndex < 0 || variantIndex >= len(turn.variants) {\n\t\treturn 0, nil, nil, fmt.Errorf(\"invalid send variant index: %d\", variantIndex)\n\t}\n\tvariant := turn.variants[variantIndex]\n\ttargetLength := paddingVariantLength(variant)\n\tchunks, delays, err := trimPaddingPrefix(variant, prefixLength)\n\tif err != nil {\n\t\treturn 0, nil, nil, err\n\t}\n\treturn targetLength, chunks, delays, nil\n}\n\nfunc trimPaddingPrefix(variant paddingVariant, prefixLength int) ([]int, []paddingDelayRange, error) {\n\tremainingPrefix := prefixLength\n\tfirstChunk := 0\n\tfor firstChunk < len(variant.chunks) && remainingPrefix > 0 {\n\t\tchunkLength := variant.chunks[firstChunk]\n\t\tif remainingPrefix < chunkLength {\n\t\t\treturn nil, nil, fmt.Errorf(\"prefix length %d splits chunk %d\", prefixLength, firstChunk)\n\t\t}\n\t\tremainingPrefix -= chunkLength\n\t\tfirstChunk++\n\t}\n\tif remainingPrefix != 0 || firstChunk == len(variant.chunks) {\n\t\treturn nil, nil, fmt.Errorf(\"prefix length %d leaves no padding record\", prefixLength)\n\t}\n\n\tchunks := append([]int(nil), variant.chunks[firstChunk:]...)\n\tdelays := make([]paddingDelayRange, len(chunks))\n\tif len(variant.delays) > 0 {\n\t\tcopy(delays, variant.delays[firstChunk:])\n\t}\n\treturn chunks, delays, nil\n}\n\nfunc defaultPaddingChunks(recordLength, writeChunkLength int) []int {\n\tchunks := make([]int, 0, (recordLength+writeChunkLength-1)/writeChunkLength)","sourceCodeStart":276,"sourceCodeEnd":312,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/transport/internet/finalmask/xmc/padding.go#L276-L312","documentation":"trimPaddingPrefix consumes the first turn's prefix by walking whole padding chunks of a variant; this error fires when the prefix length falls strictly inside a chunk instead of ending exactly on a chunk boundary. The prefix and the chunked padding records share one stream, so a misaligned prefix would desynchronize framing. It surfaces during schedule validation (validatePaddingSchedule -> selectPaddingVariant -> trimPaddingPrefix) and aborts connection setup.","triggerScenarios":"Setting a firstTurnPrefixLength (from the protocol handshake, e.g. the initial record/packet prefix) whose value is not an exact sum of leading chunk lengths of a chosen padding variant. Happens when custom padding_preset variants use chunk sizes that do not divide the negotiated prefix, or when a copied preset is paired with a different prefix length.","commonSituations":"Customizing the 1.26.12-style padding presets (padding_preset.go) by changing chunk sizes while keeping the original first-turn prefix; upgrading the transport so the server sends a different prefix length than the client preset expects; hand-writing a variants-based schedule without accounting for the prefix record.","solutions":["Align the prefix with chunk boundaries: make the leading chunks of every variant sum exactly to the firstTurnPrefixLength (e.g. one chunk equal to the whole prefix), or lower the prefix below the first chunk so it is fully consumed by chunk 0.","If you control both ends, use uniform chunk sizes that divide the prefix length evenly.","Revert to the built-in presets (newClientPaddingSchedule2612/newServerPaddingSchedule2612) which are validated against the negotiated prefix.","Write a unit test that calls validatePaddingSchedule(schedule, prefix) for your custom schedule to catch this before deploy."],"exampleFix":"// before: prefix 1000 splits the single 1500-byte chunk\nvariants: []paddingVariant{{chunks: []int{1500, 2000}}}\n// after: leading chunk exactly covers the prefix\nvariants: []paddingVariant{{chunks: []int{1000, 500, 2000}}}","handlingStrategy":"validation","validationCode":"func prefixAlignedWithChunks(prefix int, chunks []int) bool {\n    rem := prefix\n    for _, c := range chunks {\n        if rem < c {\n            return false\n        }\n        rem -= c\n    }\n    return rem == 0\n}\n// before building the schedule:\nfor _, v := range turn1.variants {\n    if !prefixAlignedWithChunks(firstTurnPrefixLength, v.chunks) {\n        return errors.New(\"prefix splits a variant chunk\")\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Run validatePaddingSchedule(schedule, firstTurnPrefixLength) in a unit test for every custom preset.","Keep leading chunks sized to exactly cover the negotiated prefix.","Never mix presets from different protocol versions."],"tags":["go","configuration","padding","protocol-framing"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}