XTLS/Xray-core · error

prefix length %d leaves no padding record

Error message

prefix length %d leaves no padding record

What it means

After the prefix-consumption loop, either some prefix bytes remain unconsumed (remainingPrefix != 0) or every chunk of the variant was eaten (firstChunk == len(variant.chunks)), leaving zero padding records for the turn. The reader expects at least one record after the prefix, so the schedule is rejected. This is the boundary case of the prefix/chunk alignment contract enforced by trimPaddingPrefix.

Source

Thrown at transport/internet/finalmask/xmc/padding.go:300

	if err != nil {
		return 0, nil, nil, err
	}
	return targetLength, chunks, delays, nil
}

func trimPaddingPrefix(variant paddingVariant, prefixLength int) ([]int, []paddingDelayRange, error) {
	remainingPrefix := prefixLength
	firstChunk := 0
	for firstChunk < len(variant.chunks) && remainingPrefix > 0 {
		chunkLength := variant.chunks[firstChunk]
		if remainingPrefix < chunkLength {
			return nil, nil, fmt.Errorf("prefix length %d splits chunk %d", prefixLength, firstChunk)
		}
		remainingPrefix -= chunkLength
		firstChunk++
	}
	if remainingPrefix != 0 || firstChunk == len(variant.chunks) {
		return nil, nil, fmt.Errorf("prefix length %d leaves no padding record", prefixLength)
	}

	chunks := append([]int(nil), variant.chunks[firstChunk:]...)
	delays := make([]paddingDelayRange, len(chunks))
	if len(variant.delays) > 0 {
		copy(delays, variant.delays[firstChunk:])
	}
	return chunks, delays, nil
}

func defaultPaddingChunks(recordLength, writeChunkLength int) []int {
	chunks := make([]int, 0, (recordLength+writeChunkLength-1)/writeChunkLength)
	for remaining := recordLength; remaining > 0; {
		chunkLength := min(remaining, writeChunkLength)
		chunks = append(chunks, chunkLength)
		remaining -= chunkLength
	}
	return chunks

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Add at least one extra chunk after the prefix boundary to every variant (increase total variant length beyond the prefix).
  2. Reduce firstTurnPrefixLength so at least one chunk survives after consumption.
  3. Use the shipped presets, which guarantee trailing records after the prefix.
  4. Assert in tests that selectPaddingVariant(variant, prefix) returns at least one chunk for every variant in turn one.

Example fix

// before: prefix 4096 consumes the only chunk, no record remains
chunks: []int{4096}
// after: one record survives after the prefix
chunks: []int{4096, 512}
Defensive patterns

Strategy: validation

Validate before calling

func variantSurvivesPrefix(prefix int, chunks []int) bool {
    sum := 0
    for _, c := range chunks {
        sum += c
    }
    return prefix < sum && prefix >= chunks[0] // at least one chunk or partial remainder left
}

Prevention

When it happens

Trigger: A firstTurnPrefixLength exactly equal to (or greater than) the total length of every variant in the turn, so the prefix consumes all chunks; or a prefix that is longer than the variant total leaving leftover bytes no chunk can absorb.

Common situations: Configuring a first turn whose variants total exactly the prefix size (e.g. prefix 4096 with a single 4096-byte chunk); mixing presets built for a smaller prefix with a peer that now sends a larger prefix after a version change.

Related errors


AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15). Data as JSON: /api/errors/b1fbc623a1d675ee. Report an issue: GitHub.