rmcast.sys PgmSendRData unbounded repair copy

PGM (Pragmatic General Multicast, RFC 3208) is a reliable one-to-many multicast transport. A sender pushes original data packets (ODATA) to a multicast group; receivers report losses back with negative acknowledgements (NAKs); the sender retransmits the missing data as repair packets (RDATA). The protocol is deliberately asymmetric: the sender keeps the authoritative state, and the receiver’s NAK tells the sender what to repair. That makes the repair path a packet-driven kernel routine: every field in a NAK is attacker input, processed by the sender kernel.

The repair path on the sender takes a NAK, converts the requested sequence number into a byte offset into its in-memory source buffer, and copies that many bytes into the RDATA packet it is about to multicast. Two pieces of arithmetic drive the copy: the offset computation, and the copy length. Both trusted the packet.

The repair copy that trusted the packet

PgmSendRData (vulnerable build) computed the offset and length like this:

/* PgmSendRData — vulnerable build, repair copy path              */
/* sender = arg1->Sender;  seq = NAK-requested sequence (var_a4_1) */

/* 1. byte offset into the source buffer: 32-bit product,         */
/*    zero-extended to 64-bit, then ONE subtract (not a modulo).   */
buffersize = sender->BufferSize;                              /* +0x58 */
offset = (uint64_t)((uint32_t)(seq - sender->BaseSeq)          /* +0xa0 */
                          * sender->BytesPerMessage)           /* +0x6c */
              + sender->WindowOffset;                          /* +0x88 */
if (offset >= buffersize)
    offset -= buffersize;

/* 2. source message pointer, somewhere in the pool-backed buffer. */
src = sender->BufferBase + offset;                            /* +0x50 */

/* 3. payload length: the first 16 bits at src, taken as-is.       */
len = src->PayloadLength;                                     /* +0x00 */

/* 4. unbounded copy from the slot's payload into the RDATA packet. */
memcpy(rdata, &src->Payload, len);                            /* +0x20 */

Two things are wrong, and either is sufficient on its own.

The first is the offset. (seq - BaseSeq) × BytesPerMessage is a 32-bit product; BytesPerMessage is part of the sender’s geometry and seq is supplied by the NAK. A NAK that picks a sequence number whose product with BytesPerMessage overflows 32 bits lands the product on a small, attacker-influenced value, and the single if (offset >= BufferSize) offset -= BufferSize cannot undo that — it is one subtraction, not a modulo, so a wrapped or oversized product leaves offset pointing somewhere it should not. BufferBase + offset then lands in the pool next to (or inside) the wrong message. This is the multiply-then-deref shape with no overflow check on the multiply and no bound on the resulting pointer.

The second is the copy length. len is read straight out of the slot at src, before anything confirms src is in bounds. With a wrapped offset, that read pulls a 16-bit length from wherever the wrapped pointer happens to land — a different sender message, or pool memory outside the source buffer entirely — and that arbitrary value then drives the memcpy. Even with a correct offset, the function has no clamp of len against the per-message slot size (BytesPerMessage), so an over-long length walks the copy past the end of the slot.

There is no bound on either side: the source may not contain [src->Payload, src->Payload + len) and nothing in the function checks that it does.

How it reads out of bounds

The destination rdata is the RDATA packet being assembled for multicast, which is sized for the on-wire payload. The source src is just BufferBase + offset, into the sender’s pool-backed source buffer. The copy reads kernel pool memory and writes it into a packet that goes back onto the wire to the multicast group — including the attacker. A long enough OOB walks neighbouring pool allocations; whatever kernel memory was parked next to the source buffer is leaked onto the wire as RDATA payload. The OOB is on the read side: information disclosure rather than pool corruption.

There is also a reliable DoS angle: a len or offset that points src at an unmapped page takes the sender kernel into a bugcheck on the copy.

The reason this is a remote primitive, not a local one, is PGM’s trust model. The sender runs the vulnerable code, but the sequence number that drives the offset comes from a NAK, and a NAK is just a packet from a receiver. Any host that can join the multicast group and emit a NAK is in the trust boundary for these arithmetic operations.

The patch

The offset moves to a full 64-bit multiply with explicit overflow detection, and four new bounds gates precede the copy: a signed sanity check on the requested sequence, an in-window fit check on the resulting pointer, a sequence-match check on the slot the pointer lands on, and a clamp of the payload length against the slot size.

/* PgmSendRData — patched                                        */

/* 1. reject requests for a sequence below the window's leading edge */
if ((int32_t)(seq - sender->BaseSeq) < 0)                     /* +0xa0 */
    return STATUS_INSUFFICIENT_RESOURCES;                      /* 0xc000009a */

/* 2. full-width multiply; high 32 bits must be zero (no overflow). */
product = (uint64_t)(uint32_t)(seq - sender->BaseSeq)
              * (uint64_t)sender->BytesPerMessage;             /* +0x6c */
if (product >> 32)
    return STATUS_INSUFFICIENT_RESOURCES;

/* 3. add the window offset; reject on 64-bit wrap.               */
ptr = sender->WindowOffset + product;                         /* +0x88 */
if (ptr < product)
    return STATUS_INSUFFICIENT_RESOURCES;

/* 4. reduce into the buffer, then confirm the access fits a slot. */
buffersize = sender->BufferSize;                              /* +0x58 */
offset = (ptr >= buffersize) ? (ptr - buffersize) : ptr;
if (offset + sender->BytesPerMessage > buffersize)            /* +0x6c */
    return STATUS_INSUFFICIENT_RESOURCES;

src = sender->BufferBase + offset;                            /* +0x50 */

/* 5. the slot we landed on must hold the sequence we asked for.    */
if (seq != bswap32(src->Sequence))                            /* +0x30 */
    return STATUS_UNSUCCESSFUL;                               /* 0xc0000001 */

/* 6. payload length is bounded against the slot before the copy.   */
len = src->PayloadLength;                                     /* +0x00 */
if (len + 0x20 > sender->BytesPerMessage)                     /* +0x6c */
    return STATUS_UNSUCCESSFUL;

memcpy(rdata, &src->Payload, len);                            /* +0x20 */

The principle on the offset is the same as anywhere a count and stride multiply into a pointer: compute in a width that cannot wrap, and reject on overflow rather than let it land where it will. The principle on the length is the load-bearing one for this function: never let the copy length come from memory the offset has not yet validated. The pointer src is untrusted until the offset has been bounded, the slot has been confirmed to fit, and the slot’s stored sequence has been confirmed to match the request; reading a length out of it before that point is what makes the copy unbounded.

Attack path

flowchart TD
    A["malicious PGM receiver joins the multicast group"] --> B["send a NAK whose (seq - BaseSeq) times BytesPerMessage overflows 32 bits"]
    B --> C["PgmSendRData computes a wrapped 32-bit offset; src = BufferBase + offset"]
    C --> D["len is read from src->PayloadLength, wherever src lands, possibly in neighbouring kernel pool"]
    D --> E["memcpy copies len bytes from src->Payload into the RDATA packet, multicast to the group"]

PGM is configured explicitly (it is not enabled by default), but once a sender is up, every group member is inside the trust boundary for the arithmetic that drives repairs.

A copy whose length is read out of the very memory the offset has just been pointed at is an unbounded copy. len = src->PayloadLength; memcpy(..., len) trusts whatever happens to be at src; the length has to be bounded against both the destination slot and the source slot, not against itself. PGM repairs are attacker-driven packet processing — every field that touches a length or an offset has to be treated as hostile at the boundary, even though the sender is the one running the code. The trust model inverts the usual “the sender is authoritative” intuition: the sender’s geometry is steered by the receivers.