Skip to content

Critical Bug: KeepAlive packet parsing fails for 8‑byte long long (affects MC 1.13 – 1.20.1) #352

Closed

Description

@BraisedChick

Description

ProtocolCraft incorrectly parses the Id field of the ClientboundKeepAlivePacket when it is defined as long long int (8‑byte big‑endian integer). This bug affects all Minecraft protocol versions that use an 8‑byte KeepAlive payload (protocol versions 393–763, corresponding to Minecraft 1.13 through 1.20.1).

When a server sends a valid 8‑byte KeepAlive ID, ProtocolCraft consistently returns a fixed wrong value – in our tests it always returns 33 (0x21), regardless of the actual payload. Manual byte‑by‑byte parsing produces the correct ID.

Because the client then replies with the wrong ID, the server immediately closes the connection with a “Timed out” error. This makes ProtocolCraft completely unusable for any Minecraft client targeting versions 1.13–1.20.1 without a manual workaround.

Environment

  • ProtocolCraft version: commit 402127c (latest as of 2026‑05‑18)
    (please specify the exact repository if different)
  • Minecraft protocol version: 758 (Minecraft 1.18.2)
  • Platform: Android ARM64
  • Compiler: NDK r28, Clang
  • Server: Vanilla Minecraft 1.18.2 (also reproduced on Paper 1.18.2)

Reproduction Steps

  1. Connect to any Minecraft 1.13+ server using ProtocolCraft.
  2. Wait for the server to send a KeepAlive packet (typically every 15–30 seconds).
  3. Parse the packet using ClientboundKeepAlivePacket::Read().
  4. Observe the parsed ID value.

Expected Behavior

The KeepAlive ID should be correctly parsed as an 8‑byte big‑endian long integer.

Example
Server sends the following 8 bytes payload:
00 00 00 00 0E EC 48 1A
Expected parsed value: 250365978 (0x000000000EEC481A).

Actual Behavior

ProtocolCraft parses the KeepAlive ID as 33 (0x0000000000000021), which is completely wrong.

Actual log output (from test run):
KeepAlive payload data (8 bytes):
[0] = 0x00
[1] = 0x00
[2] = 0x00
[3] = 0x00
[4] = 0x0E
[5] = 0xEC
[6] = 0x48
[7] = 0x1A

DEBUG: sizeof(long long) = 8
DEBUG: std::is_arithmetic = 1
DEBUG: Before Read, length = 8
DEBUG: After Read, length = 0, bytes consumed = 8

ProtocolCraft parsed KeepAlive ID=33 (0x0000000000000021) ❌ WRONG!
Manual parsed KeepAlive ID=250365978 (0x000000000EEC481A) ✓ CORRECT!

ERROR: ProtocolCraft and manual parsing give different results!

The library then sends a response packet containing the wrong ID (0x21), causing the server to immediately close the connection:
ProtocolCraft KeepAlive response: 9 bytes
[0] = 0x0F
[8] = 0x21
Sending raw (11 bytes): 0A 00 0F 00 00 00 00 00 00 00 21
Connection closed

Code Analysis

The packet is defined as:

// ClientboundKeepAlivePacket.hpp
SERIALIZED_FIELD(Id_, long long int);
According to BinaryReadWrite.hpp, arithmetic types should be read with proper endianness conversion (lines 57–96):
if constexpr (std::is_arithmetic_v<SerializationType>) { // reads sizeof(SerializationType) bytes // performs endianness conversion on little-endian systems }
The code logic appears correct, but the actual behavior shows:

The debug logs confirm that sizeof(long long) == 8 and that std::is_arithmetic is true.

The library attempts to read 8 bytes and reports bytes consumed = 8, but the resulting value is always 33 (or some fixed small integer) instead of the correctly computed value from the actual byte sequence.

This suggests a bug in either:

The byte‑order conversion (endianness) routine for long long on little‑endian ARM64, or

Memory alignment / memcpy behavior when casting bytes to long long on this platform.

Workaround
Manual parsing of the KeepAlive payload works correctly on the same platform:

// Correct way to parse KeepAlive ID (manual byte-by-byte)
long long keepAliveId = 0;
for (int i = 0; i < 8; i++) {
keepAliveId = (keepAliveId << 8) | data[i];
}

The code logic appears correct, but the actual behavior shows:

The debug logs confirm that sizeof(long long) == 8 and that std::is_arithmetic is true.

The library attempts to read 8 bytes and reports bytes consumed = 8, but the resulting value is always 33 (or some fixed small integer) instead of the correctly computed value from the actual byte sequence.

This suggests a bug in either:

The byte‑order conversion (endianness) routine for long long on little‑endian ARM64, or

Memory alignment / memcpy behavior when casting bytes to long long on this platform.

Workaround
Manual parsing of the KeepAlive payload works correctly on the same platform:

cpp
// Correct way to parse KeepAlive ID (manual byte-by-byte)
long long keepAliveId = 0;
for (int i = 0; i < 8; i++) {
keepAliveId = (keepAliveId << 8) | data[i];
}
For a temporary fix inside ProtocolCraft, one can replace the automatic SERIALIZED_FIELD for Id_ with manual read/write using the above method, or specialize BinaryReadWrite for long long on affected platforms.

Impact
Critical – Any Minecraft client using ProtocolCraft for versions 1.13–1.20.1 will be disconnected from every server as soon as the first KeepAlive packet is exchanged. This effectively blocks all gameplay for those versions.

Additional Context
The bug has been confirmed on Android ARM64 with NDK r28 and Clang.

Multiple test runs with different random KeepAlive IDs always produce the same wrong value 33 (though it might be a different fixed value under other circumstances).

The issue likely also affects other little‑endian platforms where long long is 8 bytes (e.g., x86_64, ARM64).

The problem does not occur when manually parsing the bytes, which indicates the raw data is correctly received and available.

Suggested Fix
Add explicit byte‑order handling for long long types in BinaryReadWrite, perhaps using platform‑independent functions like be64toh.

Add unit tests that verify long long serialization/deserialization with known byte patterns on all target platforms.

Consider using std::endian (C++20) or compiler‑specific macros to detect endianness at compile time.

Logs (full test run)
See attached log or the snippet above – full logs available upon request.

Activity

  1. adepierre commented on May 19, 2026

    @adepierre
    Owner

    Hello, thank you for the report. However, I'm not sure why that would not impact 1.12.2 and 1.20.2+ versions too. The packet structure is the same and the long long read/write code is the same. Could you expand on what you tested exactly?

    The issue likely also affects other little‑endian platforms where long long is 8 bytes (e.g., x86_64, ARM64).

    I have been running bots for years on these with no issue at all with the Keep Alive packet so if there is a bug it's probably platform/compiler specific. Any minimal reproducible case (i.e. just manually creating the packet bytes and calling read without all the server connection process) would help identify which platform exactly.

    The problem does not occur when manually parsing the bytes, which indicates the raw data is correctly received and available

    Could you detail what you did exactly to retrieve the bytes and parse them manually?

    If you are always getting the same value and it changes from version to version, it may just be because you are calling the base packet GetId that returns the packet ID in the protocol instead of GetId_ that returns the Id_ field. Could you confirm you are actually using GetId_ ?

  2. adepierre commented on Jun 6, 2026

    @adepierre
    Owner

    Closing for now as I assume the issue was you used GetId instead of GetId_. Feel free to reopen if it's still happening.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions