Skip to content

Fix broadcastRPC sending an empty body after an out-RPC event handler reads the bitstream - #1270

Open
exc3pt1ongit wants to merge 1 commit into
openmultiplayer:masterfrom
exc3pt1ongit:fix/broadcastrpc-empty-body
Open

Fix broadcastRPC sending an empty body after an out-RPC event handler reads the bitstream#1270
exc3pt1ongit wants to merge 1 commit into
openmultiplayer:masterfrom
exc3pt1ongit:fix/broadcastrpc-empty-body

Conversation

@exc3pt1ongit

Copy link
Copy Markdown

Summary

RakNetLegacyNetwork::broadcastRPC transmits RPCs with an empty body whenever a
network out-event handler reads the outgoing bitstream. It passes
bs.GetNumberOfUnreadBits() as the payload length to rakNetServer.RPC(), but the
onSendRPC / per-RPC onSend handlers reset and consume the read pointer first, so the
"unread" count is 0 by the time the packet is sent.

Every other sender uses the amount actually written:

method length passed
sendRPC GetNumberOfBitsUsed()
sendPacket GetNumberOfBitsUsed()
broadcastPacket GetNumberOfBitsUsed()
broadcastRPC GetNumberOfUnreadBits() ← the odd one out

The fix is to use GetNumberOfBitsUsed() in broadcastRPC as well (both the
exceptPeer and broadcast-to-all paths).

Root cause

broadcastRPC builds a read/write bitstream over the caller's data, then runs the
out-event handlers:

NetworkBitStream bs(data.data(), bitsToBytes(data.size()), false);
bs.SetWriteOffset(data.size());
// ... onSendRPC / onSend handlers do bs.resetReadPointer() then read the body ...
return rakNetServer.RPC(id, (const char*)bs.GetData(),
    bs.GetNumberOfUnreadBits(), /* ... */);   // 0 once a handler has read to the end

GetNumberOfUnreadBits() is numberOfBitsUsed - readOffset. A handler that reads the
whole payload leaves readOffset == numberOfBitsUsed, so the RPC is sent with a
0-bit body. Handlers are not required to restore the read pointer, and the packet/RPC
siblings above already don't depend on it.

Impact

Any broadcast RPC observed by an out-event handler that reads the stream (e.g. a
Pawn.RakNet OnOutgoingRPC hook) is sent empty. This is silent for most modern
clients, but it crashes legacy 0.3.7 clients on RPCs that carry a fixed body:

  • SetWeather() → RPC 152 (SetWeather, 1 byte)
  • SetWorldTime() → RPC 94 (SetWorldTime, 1 byte)

On the client, RakPeer::HandleRPCPacket sets rpcParms.input = NULL when
numberOfBitsOfData == 0; the stock 0.3.7 RPC handler then wraps {NULL, 0} in a
no-copy BitStream and reads its first byte, dereferencing a null data pointer:

Unhandled exception at samp.dll +0x1bf0e
0xC0000005: Access violation reading location 0x00000000

Reproduced deterministically: a server that periodically broadcasts SetWeather /
SetWorldTime (with a Pawn.RakNet out-RPC hook loaded) crashes a stock 0.3.7 client
in-world ~a minute after spawn, every session. A wire capture shows the broadcast
SetWeather/SetWorldTime RPC leaving with a zero-length body; the per-player
SetPlayerWeather/SetPlayerTime equivalents (which go through sendRPC) carry their
byte correctly. With this change the broadcast RPCs carry their body and the crash no
longer occurs.

Change

Server/Components/LegacyNetwork/legacy_network_impl.hppbroadcastRPC: use
bs.GetNumberOfBitsUsed() instead of bs.GetNumberOfUnreadBits() for the transmitted
length (two call sites), matching sendRPC / sendPacket / broadcastPacket.

… reads the bitstream

RakNetLegacyNetwork::broadcastRPC passed bs.GetNumberOfUnreadBits() as the
payload length to rakNetServer.RPC(). The onSendRPC / per-RPC onSend out-event
handlers reset the read pointer and read from the bitstream, so by the time the
send happens the read pointer sits at the end and GetNumberOfUnreadBits() returns
0 - the RPC goes out with a zero-length body.

sendRPC, sendPacket and broadcastPacket all use GetNumberOfBitsUsed(); broadcastRPC
was the only sender using the unread-bit count. Switch it to GetNumberOfBitsUsed()
so the full written body is transmitted regardless of what out-event handlers do
with the read pointer.

Concrete impact: with any registered network out-event handler that reads the
stream (for example a Pawn.RakNet OnOutgoingRPC hook), a broadcast RPC is sent
empty. For a one-byte RPC broadcast such as SetWeather (152, via SetWeather) or
SetWorldTime (94, via SetWorldTime), a legacy 0.3.7 client then reads its single
byte from a null RPC parameter bitstream and crashes
(samp.dll +0x1bf0e, ACCESS_VIOLATION reading 0x00000000).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant