Skip to content

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

@BraisedChick

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.

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