fix(inputs): bound InputMessage end_tick lookahead (DoS)#1525
Merged
cBournhonesque merged 1 commit intoJun 24, 2026
Merged
Conversation
A malicious client can send an `InputMessage` with a far-future `end_tick` (e.g. `server_tick + 30_000`). `InputBuffer::set_raw` / `extend_to_range` then extend the internal `VecDeque` to fit that tick, allocating one entry per intermediate tick — ~30k entries from a single message, repeatable across messages and connections (memory-exhaustion DoS). Reject input messages whose `end_tick` falls outside `[server_tick - 256, server_tick + 64]` before any buffer write, via a new `is_input_within_lookahead` helper in the server receive path. Legitimate clients run at most a few ticks ahead (typical input-delay is 0–3 ticks), so the window is generous. The symmetric past bound also closes a wrap-around case: `end_tick = server_tick + 2^31` makes `Tick - Tick` wrap to `i32::MIN`, which a forward-only `delta <= MAX` check would accept. Tests: - unit tests for the bound (forward, past, far-future, i32::MIN wrap); - an end-to-end regression test sending a forged message via the public `MessageSender::send::<InputChannel>` API and asserting the server's InputBuffer stays bounded (29998 entries without the fix → <1000 with). Split out from cBournhonesque#1476 (the input-target authorization half follows in a separate PR). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner
|
This works, but rather than just providing specific DoS protections, do you have an idea for providing an API where the user would inspect/validate the inputs? |
This was referenced Jun 24, 2026
| /// | ||
| /// Past-direction messages are normally handled harmlessly by | ||
| /// [`InputBuffer::set_raw`]'s start-tick guard. The reason we still bound them: | ||
| /// `Tick - Tick` returns `i32` via signed wrapping arithmetic over `u32` tick |
Owner
There was a problem hiding this comment.
Let's keep this for now, but I don't think there is any point of making Tick wrapping now that it's a u32; I should just make it a normal u32.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Splits the DoS half out of #1476 into its own focused PR.
A malicious/modified client can send an
InputMessagewith a far-futureend_tick(e.g.server_tick + 30_000). On the server receive path,InputBuffer::set_raw/extend_to_rangeextend the internalVecDequeto fit that tick value, filling intermediate entries withAbsent/SameAsPrecedent— one allocation per intermediate tick. A single forged message allocates ~30k entries; repeated across messages and connections, the server is memory-exhausted.Fix
Reject input messages whose
end_tickis implausibly far from the server's current tick before any buffer write, via a newis_input_within_lookaheadhelper inlightyear_inputs::server::receive_input_message:MAX_INPUT_LOOKAHEAD_TICKS = 64(~1 s at 64 Hz). Legitimate clients run at most a few ticks ahead of the server (typicalInputDelayConfigis 0–3 ticks), so this is generous.MAX_INPUT_PAST_TICKS = 256. The symmetric past bound is what makes the check safe under wrap-around: sinceTickisu32andTick - Tickreturnsi32, anend_tick = server_tick + 2^31wraps the delta toi32::MIN, which a forward-onlydelta <= MAXcheck would accept. Rejecting both ends eliminates that.Tests
i32::MINwrap-around case.test_input_message_with_huge_end_tick_does_not_allocate_unbounded_buffer) that forges a message via the publicMessageSender::send::<InputChannel>API — modelling exactly what a modified client binary could do — and asserts the server'sInputBufferstays bounded.Verified red→green on current
main: without the bound the server allocates 29 998 buffer entries from the single forged message; with it the buffer stays< 1000.Notes
client_server::inputsuite passes).InputTarget::Entity) follows separately, since it carries a downstream behavior change (ControlledByenforcement) that warrants independent review.