XTLS/Xray-core · error

empty padding schedule

Error message

empty padding schedule

What it means

validatePaddingSchedule rejected a padding schedule with zero turns. runPaddingSchedule needs at least one turn to shape the handshake; an empty slice means the schedule builder produced nothing. Since schedules are constructed in code (newClientPaddingSchedule2612), this is a programming/config-builder error, not a runtime condition.

Source

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

		}

		localSends := isClient == (turn.direction == paddingClientToServer)
		if localSends {
			if err := writePaddingTurnWithBuffer(writer, turn, prefixLength, time.Sleep, &writeBuffer); err != nil {
				return fmt.Errorf("write padding turn %d: %w", i, err)
			}
			continue
		}
		if err := readPaddingTurn(reader, turn, prefixLength); err != nil {
			return fmt.Errorf("read padding turn %d: %w", i, err)
		}
	}
	return nil
}

func validatePaddingSchedule(schedule []paddingTurn, firstTurnPrefixLength int) error {
	if len(schedule) == 0 {
		return fmt.Errorf("empty padding schedule")
	}
	if firstTurnPrefixLength < 0 {
		return fmt.Errorf("negative first turn prefix length: %d", firstTurnPrefixLength)
	}
	if firstTurnPrefixLength > 0 && schedule[0].direction != paddingClientToServer {
		return fmt.Errorf("first prefixed padding turn is not client-to-server")
	}

	for i, turn := range schedule {
		if turn.direction != paddingClientToServer && turn.direction != paddingServerToClient {
			return fmt.Errorf("padding turn %d has invalid direction: %d", i, turn.direction)
		}
		if err := validatePaddingDelayRange(turn.startDelay); err != nil {
			return fmt.Errorf("padding turn %d has an invalid start delay: %w", i, err)
		}
		if err := validatePaddingDelayRange(turn.chunkDelay); err != nil {
			return fmt.Errorf("padding turn %d has an invalid chunk delay: %w", i, err)
		}

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Ensure the schedule builder returns at least one turn for every supported path
  2. Fail at construction time if schedule selection yields zero turns, before any bytes are written
  3. Add a unit test asserting non-empty schedules for all builder inputs

Example fix

// before
schedule := buildSchedule(v) // may return nil
err := runPaddingSchedule(r, w, true, prefix, schedule)

// after
schedule := buildSchedule(v)
if len(schedule) == 0 {
    return fmt.Errorf("no padding schedule for version %d", v)
}
err := runPaddingSchedule(r, w, true, prefix, schedule)
Defensive patterns

Strategy: validation

Validate before calling

if len(schedule) == 0 {
    return fmt.Errorf("refusing to run empty padding schedule")
}

Type guard

func hasPaddingTurns(s []paddingTurn) bool { return len(s) > 0 }

Prevention

When it happens

Trigger: Calling runPaddingSchedule with a nil or empty schedule slice; a schedule builder that filters out all turns based on bad input (e.g. unsupported version profile).

Common situations: Forks adding conditional schedules that can return empty lists; tests invoking runPaddingSchedule directly with no turns; upstream changes where schedule selection fails open with an empty result.

Related errors


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