~/posts/binary/cve-2020-16898-rdnss-parser-boundaries.md

CVE-2020-16898: an eight-byte RDNSS boundary mismatch

Trace RDNSS length parity through two parser passes and the NdisGetDataBuffer copy path. Verify the eight-byte mismatch with a model, then separate fragment handling, stack corruption, and code-execution claims.

date[31:24]
read[23:16]
7 min
cat[15:8]
Binary
Contents
  1. 0x00RDNSS length must account for the whole structure
  2. 0x01The two passes assign different identities to the tail
  3. 0x02Route Information carries the unchecked length forward
  4. 0x03NdisGetDataBuffer changes behavior when it copies
  5. 0x04Fragmentation, reassembly, and receive rules
  6. 0x05Isolate the boundary error with an arithmetic model
  7. 0x06Restore the same invariants in fixes and regression tests
  8. 0x07References

CVE-2020-16898 affects Windows IPv6 Router Advertisement (RA) processing before the October 2020 security updates. A malformed RDNSS option makes two traversals disagree by eight bytes: a tail treated as data during validation becomes a new option header during consumption, eventually influencing a copy into kernel-stack storage.

Length parity is only the first boundary. The outcome also depends on whether the NET_BUFFER data is contiguous and on the stack-cookie check before return. Addresses and crash values below belong to this historical case; the exact Windows build and tcpip.sys hash have not been matched. The demonstrated chain ends at parser desynchronization and memory corruption, not a cross-version code-execution exploit.

RDNSS length must account for the whole structure

RDNSS has type 0x19. RFC 8106 Sections 5.1 and 5.3.1 define an eight-byte fixed header followed by at least one 16-byte IPv6 address. Length counts eight-byte units, must be at least 3, and must satisfy (Length - 1) % 2 == 0.

RDNSS layout with one addressBE
OffsetNameTypeSizeValue
0x00Typeuint810x19
0x01Lengthuint813
0x02Reserveduint162
0x04Lifetimeuint324
0x08AddressIPv60x10
sizeof(struct RDNSS) = 0x18 (24 bytes)

With N addresses, the byte length is 8 + 16*N and Length is 1 + 2*N. An even Length does not merely add padding: the RDNSS structure has no trailing field for the leftover half-address.

Take Length 4. An outer walker calculates 4*8, or 32 bytes. An inner parser using integer division obtains (4-1)//2 == 1 address and consumes only 8+16 == 24 bytes.

Two boundaries for the same option4 rows
Length Declared bytes Bytes consumed as complete addresses Difference
3 24 24 0
4 32 24 8
5 40 40 0
6 48 40 8

The two passes assign different identities to the tail

The interaction between tcpip!Ipv6pHandleRouterAdvertisement and Ipv6pUpdateRDNSS exposes the boundary disagreement. The first pass validates options using their declared lengths; the second consumes their typed contents. When those cursor rules diverge, validating a byte span does not ensure that every object eventually interpreted within it was validated.

The case records the following option-read positions. They establish a relationship between addresses, not a portable function offset.

Cursor and field-interpretation evidence4 rows
Observation Value Meaning
First option start ffff920766042650 Start of RDNSS
Subsequent option start ffff920766042668 Cursor advanced by 0x18
Declared span 0x20 Eight bytes beyond the actual increment
Tail marker XXXXYYYY The first two 0x58 bytes become Type and Length

Once the last eight bytes of the even-length RDNSS are read as a header, the second pass sees a different option sequence from the one checked by the first. The precise failure is not that an oversized option passed its own validation. Its bytes never received that option type's validation at all.

Route Information carries the unchecked length forward

Route Information has type 0x18. RFC 4191 Section 2.3 allows Length 1, 2, or 3, with additional constraints from Prefix Length. Its maximum protocol size is therefore 24 bytes.

An oversized Route Information option placed directly in the sequence is rejected by the early length check. Placing its header in the residual eight bytes of RDNSS changes the path: the first pass treats those bytes as RDNSS content, while the second identifies Route Information and forwards its Length to a later memory operation.

Three values make this class of multi-pass parser easier to audit:

  • The start address used by validation.
  • The start address used by consumption.
  • The span formula used by each pass.

Showing that the overall buffer is large enough is not sufficient. The consumed type and boundary must be the same ones that were checked.

NdisGetDataBuffer changes behavior when it copies

NdisGetDataBufferc
PVOID NdisGetDataBuffer(
    PNET_BUFFER NetBuffer,
    ULONG BytesNeeded,
    PVOID Storage,
    ULONG AlignMultiple,
    ULONG AlignOffset
);

Microsoft's API contract requires caller-supplied Storage to hold at least BytesNeeded bytes. With contiguous data and suitable access conditions, the function can return a pointer into the existing data. For noncontiguous data with Storage supplied, it uses that storage to assemble the requested bytes. Insufficient data or mapping resources can instead produce a null return.

In this case, the caller uses a fixed stack region as Storage but derives BytesNeeded from the reinterpreted Length. Length << 3 multiplies by eight. A Length of 0x22 requests 0x110, or 272 bytes, well beyond the normal 24-byte Route Information maximum.

Two facts must therefore be established independently: the input length reaches the copy parameter, and the actual call takes the path that uses the fallback storage. An oversized Length alone does not explain a stack overwrite.

Fragmentation, reassembly, and receive rules

Fragmented input in this case influences the reassembled data layout and reaches the noncontiguous-data copy path. The decisive state is the actual organization of NET_BUFFER data and its memory descriptor list (MDL) chain. A Fragment Header on the wire does not automatically mean that the relevant bytes cross an MDL boundary. Drivers, receive processing, and reassembly all affect the result.

RA processing also has link-scope and Hop Limit acceptance conditions. This case does not establish direct reachability from any arbitrary location on the Internet.

Actual data length is a separate constraint. Even when the requested length exceeds the stack storage capacity, enough subsequent bytes must exist in the network buffer for the relevant copy to proceed. An exaggerated Length with insufficient data may fail first. Reproduction must inspect the state after reception and reassembly rather than treat one fragment size as a universal constant.

Isolate the boundary error with an arithmetic model

The model below checks byte-consumption relationships only. It sends no traffic and does not emulate the Windows kernel. It covers all 253 eight-bit Length values from 3 through 255, checks a zero difference for odd values and an eight-byte difference for even values, and verifies the recorded pointer increment and copy length.

rdnss_boundaries.pypython
def rdnss_offsets(length_units):
    if not 3 <= length_units <= 255:
        raise ValueError("length outside the modeled range")
    declared = length_units * 8
    walked = 8 + ((length_units - 1) // 2) * 16
    return declared, walked

for length in range(3, 256):
    declared, walked = rdnss_offsets(length)
    assert declared - walked == (8 if length % 2 == 0 else 0)
assert rdnss_offsets(4) == (32, 24)
assert 0xffff920766042668 - 0xffff920766042650 == 0x18
assert 0x22 << 3 == 0x110 == 272
print("PASS: 253 lengths; even delta=8; odd delta=0; copy length=272")
Actual boundary-model output
$ python rdnss_boundaries.py
PASS: 253 lengths; even delta=8; odd delta=0; copy length=272

The case also records 0x4242424242424242 in the return-address region, followed by BugCheck 0x139 with parameter 1 set to 2 and a path through _report_gsfailure. Microsoft documents that parameter as a stack-buffer overrun detected by stack-cookie instrumentation.

Evaluate each evidence layer separately4 rows
Layer Evidence boundary in this case
RDNSS cursor desynchronization Pointer increment agrees with the tail interpretation; the separate arithmetic model passes
Option length lacking its type-specific check The two traversals assign different object identities to the tail
Stack memory corruption Copy path, fill value, and security-check failure support one another
Reliable code execution Not demonstrated; overwriting control data is not the same as successfully using it

Detection by stack protection does not negate the preceding out-of-bounds write. Conversely, the write does not prove that stack protection was bypassed. Keeping those judgments separate gives the result its proper scope.

Restore the same invariants in fixes and regression tests

For system maintenance, select the applicable security update from Microsoft's CVE-2020-16898 advisory. The following parser checks are deductions from the root cause, not a line-by-line description of an unmatched patch.

Boundary-check checklist4 steps
  1. 1

    Validate the structure first

    Require RDNSS Length of at least 3 and (Length - 1) % 2 == 0. The address count must account for the declared span exactly.

  2. 2

    Keep one interpretation of each option boundary

    Validation and consumption should share boundary information. The typed parser's consumed size must match the outer declaration.

  3. 3

    Constrain fallback storage at the copy site

    Compare BytesNeeded with the actual Storage capacity instead of relying only on an earlier protocol pass.

  4. 4

    Test both memory representations

    Exercise contiguous and noncontiguous buffers, insufficient data, malformed lengths, and early protocol-layer rejection.

The chain crosses three different boundaries: protocol length, parser position, and backing storage. Aligning all three is what turns a stack-corruption symptom into an explanation of the first failed invariant.

References

NORMAL~/posts/binary/cve-2020-16898-rdnss-parser-boundaries.md§--
0%en