{"record":{"id":"ef3f2abe71e032a8","repo":"XTLS/Xray-core","slug":"empty-padding-schedule","errorCode":null,"errorMessage":"empty padding schedule","messagePattern":"empty padding schedule","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"transport/internet/finalmask/xmc/padding.go","lineNumber":75,"sourceCode":"\t\t}\n\n\t\tlocalSends := isClient == (turn.direction == paddingClientToServer)\n\t\tif localSends {\n\t\t\tif err := writePaddingTurnWithBuffer(writer, turn, prefixLength, time.Sleep, &writeBuffer); err != nil {\n\t\t\t\treturn fmt.Errorf(\"write padding turn %d: %w\", i, err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif err := readPaddingTurn(reader, turn, prefixLength); err != nil {\n\t\t\treturn fmt.Errorf(\"read padding turn %d: %w\", i, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validatePaddingSchedule(schedule []paddingTurn, firstTurnPrefixLength int) error {\n\tif len(schedule) == 0 {\n\t\treturn fmt.Errorf(\"empty padding schedule\")\n\t}\n\tif firstTurnPrefixLength < 0 {\n\t\treturn fmt.Errorf(\"negative first turn prefix length: %d\", firstTurnPrefixLength)\n\t}\n\tif firstTurnPrefixLength > 0 && schedule[0].direction != paddingClientToServer {\n\t\treturn fmt.Errorf(\"first prefixed padding turn is not client-to-server\")\n\t}\n\n\tfor i, turn := range schedule {\n\t\tif turn.direction != paddingClientToServer && turn.direction != paddingServerToClient {\n\t\t\treturn fmt.Errorf(\"padding turn %d has invalid direction: %d\", i, turn.direction)\n\t\t}\n\t\tif err := validatePaddingDelayRange(turn.startDelay); err != nil {\n\t\t\treturn fmt.Errorf(\"padding turn %d has an invalid start delay: %w\", i, err)\n\t\t}\n\t\tif err := validatePaddingDelayRange(turn.chunkDelay); err != nil {\n\t\t\treturn fmt.Errorf(\"padding turn %d has an invalid chunk delay: %w\", i, err)\n\t\t}","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/XTLS/Xray-core/blob/7d214f8b094f75322fa3990f8aadad1c912f24f5/transport/internet/finalmask/xmc/padding.go#L57-L93","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Ensure the schedule builder returns at least one turn for every supported path","Fail at construction time if schedule selection yields zero turns, before any bytes are written","Add a unit test asserting non-empty schedules for all builder inputs"],"exampleFix":"// before\nschedule := buildSchedule(v) // may return nil\nerr := runPaddingSchedule(r, w, true, prefix, schedule)\n\n// after\nschedule := buildSchedule(v)\nif len(schedule) == 0 {\n    return fmt.Errorf(\"no padding schedule for version %d\", v)\n}\nerr := runPaddingSchedule(r, w, true, prefix, schedule)","handlingStrategy":"validation","validationCode":"if len(schedule) == 0 {\n    return fmt.Errorf(\"refusing to run empty padding schedule\")\n}","typeGuard":"func hasPaddingTurns(s []paddingTurn) bool { return len(s) > 0 }","tryCatchPattern":null,"preventionTips":["Assert non-empty schedules at construction time","Unit-test every schedule builder output for non-emptiness"],"tags":["go","padding","validation","internal"],"backgroundTag":null,"analyzedSha":"7d214f8b094f75322fa3990f8aadad1c912f24f5","analyzedAt":"2026-08-15T14:26:24.325Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}