diff --git a/src/core/net/ip6.cpp b/src/core/net/ip6.cpp index 18c435c7c..31ab04292 100644 --- a/src/core/net/ip6.cpp +++ b/src/core/net/ip6.cpp @@ -626,6 +626,7 @@ Error Ip6::HandleFragment(Message &aMessage) Message *message = nullptr; uint16_t offset = 0; uint16_t payloadFragment = 0; + uint16_t unfragLength = 0; bool isFragmented = true; SuccessOrExit(error = aMessage.Read(0, header)); @@ -656,7 +657,13 @@ Error Ip6::HandleFragment(Message &aMessage) LogInfo("Fragment with id %lu received > %u bytes, offset %u", ToUlong(fragmentHeader.GetIdentification()), payloadFragment, offset); - if (offset + payloadFragment + aMessage.GetOffset() > kMaxAssembledDatagramLength) + // The reassembled datagram's unfragmentable part comes from the first + // fragment, and later fragments can carry a different-length extension + // header stack, so buffer sizing and payload write placement must use + // the first fragment's unfragmentable length. + unfragLength = (message != nullptr) ? (message->GetLength() - message->GetOffset()) : aMessage.GetOffset(); + + if (offset + payloadFragment + unfragLength > kMaxAssembledDatagramLength) { LogWarn("Packet too large for fragment buffer"); ExitNow(error = kErrorNoBufs); @@ -684,14 +691,14 @@ Error Ip6::HandleFragment(Message &aMessage) } // increase message buffer if necessary - if (message->GetLength() < offset + payloadFragment + aMessage.GetOffset()) + if (message->GetLength() < offset + payloadFragment + unfragLength) { - SuccessOrExit(error = message->SetLength(offset + payloadFragment + aMessage.GetOffset())); + SuccessOrExit(error = message->SetLength(offset + payloadFragment + unfragLength)); } // copy the fragment payload into the message buffer message->WriteBytesFromMessage( - /* aWriteOffset */ aMessage.GetOffset() + offset, aMessage, + /* aWriteOffset */ unfragLength + offset, aMessage, /* aReadOffset */ aMessage.GetOffset() + sizeof(fragmentHeader), /* aLength */ payloadFragment); message->SetOffset(offset + payloadFragment); @@ -700,7 +707,7 @@ Error Ip6::HandleFragment(Message &aMessage) if (!fragmentHeader.IsMoreFlagSet()) { // use the offset value for the whole ip message length - message->SetOffset(aMessage.GetOffset() + offset + payloadFragment); + message->SetOffset(unfragLength + offset + payloadFragment); // creates the header for the reassembled ipv6 package SuccessOrExit(error = aMessage.Read(0, header)); diff --git a/tests/nexus/CMakeLists.txt b/tests/nexus/CMakeLists.txt index 6f0ec821c..e674d6ece 100644 --- a/tests/nexus/CMakeLists.txt +++ b/tests/nexus/CMakeLists.txt @@ -418,6 +418,7 @@ ot_nexus_test(fed_rx_only_link_establishment "core;nexus") ot_nexus_test(form_join "core;nexus") ot_nexus_test(history_tracker "core;nexus") ot_nexus_test(inform_previous_parent_on_reattach "core;nexus") +ot_nexus_test(ip6_frag_reassembly "core;nexus") ot_nexus_test(ipv6_forward_ext_headers "core;nexus") ot_nexus_test(ipv6_fragmentation "core;nexus") ot_nexus_test(ipv6_recursion "core;nexus") diff --git a/tests/nexus/test_ip6_frag_reassembly.cpp b/tests/nexus/test_ip6_frag_reassembly.cpp new file mode 100644 index 000000000..2ce251bc4 --- /dev/null +++ b/tests/nexus/test_ip6_frag_reassembly.cpp @@ -0,0 +1,425 @@ +/* + * Copyright (c) 2026, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Verifies IPv6 fragment reassembly buffer sizing and payload placement when fragments + * of the same datagram carry different-length unfragmentable parts. + * + * `Ip6::HandleFragment()` tracks contiguity in fragment-payload space only. It must size + * the reassembly buffer and place fragment-payload writes using the FIRST fragment's + * unfragmentable-part length (the one actually copied into the reassembled datagram), + * so that no region of the reassembled datagram is left unwritten. This matters because + * the buffer growth path (`SetLength()` -> `MessagePool::NewBuffer()`) reuses pool + * buffers without clearing, so any unwritten region would hold stale bytes of previously + * freed messages. + * + * This test asserts: + * case A (positive control): a normally fragmented echo (consistent 40-byte headers, + * no length mismatch) reassembles and is answered; its data carries a + * deliberate run of marker bytes which must be reflected in the reply, + * proving the reply-content (marker run) inspection used in case B executes; + * case B: fragments whose unfragmentable-part lengths differ (the final fragment + * carries an extra Hop-by-Hop extension header), constructed so that any + * unwritten region would be exposed (with checksums precomputed against + * several possible region contents), must NOT produce an echo reply, since a + * reply only appears when the reassembled datagram incorporated bytes matching + * the checksum assumption. + * + * Gates: OPENTHREAD_CONFIG_IP6_FRAGMENTATION_ENABLE (default 0; 1 in this nexus config), + * default static message pool (heap builds zero via calloc). + */ + +#include +#include + +#include +#include +#include +#include + +#include "platform/nexus_core.hpp" +#include "platform/nexus_node.hpp" + +#include "net/ip6.hpp" + +namespace ot { +namespace Nexus { + +static constexpr uint32_t kFormNetworkTime = 13 * 1000; + +static constexpr uint16_t kPay1 = 200; // fragment 1 payload (echo header + data) +static constexpr uint16_t kPay2 = 8; // final fragment payload +static constexpr uint16_t kHole = 8; // hole size = HBH header length delta +static constexpr uint16_t kMarkerRunOffset = 16; // marker run position within control echo message +static constexpr uint8_t kPadOptionSize = 6; // PadN size filling the 8-byte HBH header +static constexpr uint8_t kHopLimit = 64; +static constexpr uint8_t kMarker = 0xC3; +static constexpr uint8_t kPayByte = 0xAA; +static constexpr uint8_t kPay2Byte = 0x55; + +static bool sGotReply; +static bool sReplyHasMarkerRun; +static uint16_t sReplyLength; + +static void HandleIcmp(void *, otMessage *aMessage, const otMessageInfo *, const otIcmp6Header *aHeader) +{ + if (aHeader->mType != OT_ICMP6_TYPE_ECHO_REPLY) + { + return; + } + + uint8_t buf[512]; + uint16_t offset = otMessageGetOffset(aMessage); + uint16_t len = otMessageGetLength(aMessage) - offset; + + len = (len > sizeof(buf)) ? sizeof(buf) : len; + otMessageRead(aMessage, offset, buf, len); + + sGotReply = true; + sReplyLength = len; + + // look for a run of kHole marker bytes (a reflected hole, or the control's seeded run) + for (uint16_t i = 0; i + kHole <= len; i++) + { + bool run = true; + + for (uint16_t j = 0; j < kHole; j++) + { + if (buf[i + j] != kMarker) + { + run = false; + break; + } + } + + if (run) + { + sReplyHasMarkerRun = true; + break; + } + } +} + +static uint32_t ChecksumAdd(uint32_t aSum, const uint8_t *aData, uint16_t aLength) +{ + for (uint16_t i = 0; i < aLength; i++) + { + aSum += (i & 1) ? aData[i] : (static_cast(aData[i]) << 8); + } + + return aSum; +} + +static uint16_t IcmpChecksum(const Ip6::Address &aSrc, + const Ip6::Address &aDst, + const uint8_t *aIcmp, + uint16_t aLen1, + const uint8_t *aHole, + uint16_t aHoleLen, + const uint8_t *aTail, + uint16_t aTailLen) +{ + uint32_t sum = 0; + uint16_t total = aLen1 + aHoleLen + aTailLen; + + sum = ChecksumAdd(sum, aSrc.GetBytes(), sizeof(otIp6Address)); + sum = ChecksumAdd(sum, aDst.GetBytes(), sizeof(otIp6Address)); + sum += total; + sum += Ip6::kProtoIcmp6; + sum = ChecksumAdd(sum, aIcmp, aLen1); + sum = ChecksumAdd(sum, aHole, aHoleLen); + sum = ChecksumAdd(sum, aTail, aTailLen); + + while (sum >> 16) + { + sum = (sum & 0xffff) + (sum >> 16); + } + + return static_cast(~sum); +} + +static Message *NewIp6Message(Node &aNode) +{ + Message *message = aNode.Get().Allocate(Message::kTypeIp6); + + VerifyOrQuit(message != nullptr); + SuccessOrQuit(message->SetPriority(Message::kPriorityNormal)); + + return message; +} + +static void SendPrimers(Node &aSender, const Ip6::Address &aDst, uint8_t aFill) +{ + for (int i = 0; i < 3; i++) + { + static uint8_t primer[600]; + + memset(primer, aFill, sizeof(primer)); + + otMessageInfo info; + otUdpSocket socket; + + memset(&info, 0, sizeof(info)); + memset(&socket, 0, sizeof(socket)); + info.mPeerAddr = aDst; + info.mPeerPort = 55555; // no listener on leader + + SuccessOrQuit(otUdpOpen(&aSender.GetInstance(), &socket, nullptr, nullptr)); + { + otMessage *m = otUdpNewMessage(&aSender.GetInstance(), nullptr); + VerifyOrQuit(m != nullptr); + SuccessOrQuit(otMessageAppend(m, primer, sizeof(primer))); + SuccessOrQuit(otUdpSend(&aSender.GetInstance(), &socket, m, &info)); + } + SuccessOrQuit(otUdpClose(&aSender.GetInstance(), &socket)); + } +} + +static void BuildAndSendFragments(Node &aSender, + Core &aNexus, + const Ip6::Address &aSrc, + const Ip6::Address &aDst, + uint32_t aFragId, + bool aWithHole, + uint8_t aGapAssume, + bool aPrimeBetween, + uint16_t aEchoSeq) +{ + uint8_t icmp[kPay1]; + uint8_t hole[kHole]; + uint8_t tail[kPay2]; + uint16_t csum; + Ip6::Icmp6Header echoHeader; + + // ICMP Echo Request message: header + patterned data. The control request + // (`aWithHole` false) carries a deliberate run of kMarker bytes so that the + // reply-content inspection in `HandleIcmp()` is exercised and can be asserted. + memset(icmp, kPayByte, sizeof(icmp)); + + if (!aWithHole) + { + memset(&icmp[kMarkerRunOffset], kMarker, kHole); + } + + echoHeader.Clear(); + echoHeader.SetType(Ip6::Icmp6Header::kTypeEchoRequest); + echoHeader.SetId(0x1234); + echoHeader.SetSequence(aEchoSeq); + memcpy(icmp, &echoHeader, sizeof(echoHeader)); + + memset(hole, aGapAssume, sizeof(hole)); + memset(tail, kPay2Byte, sizeof(tail)); + + if (aWithHole) + { + // checksum computed assuming the unwritten region would read back the assumed bytes + csum = IcmpChecksum(aSrc, aDst, icmp, kPay1, hole, kHole, tail, kPay2); + } + else + { + csum = IcmpChecksum(aSrc, aDst, icmp, kPay1, nullptr, 0, tail, kPay2); + } + + echoHeader.SetChecksum(csum); + memcpy(icmp, &echoHeader, sizeof(echoHeader)); + + // ---- fragment 1: base IPv6 header only (40-byte unfragmentable part), offset 0, + // M flag set, payload = icmp[kPay1] + + { + Message *message = NewIp6Message(aSender); + Ip6::Header ip6Header; + Ip6::FragmentHeader fragmentHeader; + + ip6Header.InitVersionTrafficClassFlow(); + ip6Header.SetPayloadLength(static_cast(sizeof(Ip6::FragmentHeader) + kPay1)); + ip6Header.SetNextHeader(Ip6::kProtoFragment); + ip6Header.SetHopLimit(kHopLimit); + ip6Header.SetSource(aSrc); + ip6Header.SetDestination(aDst); + SuccessOrQuit(message->Append(ip6Header)); + + fragmentHeader.Init(); + fragmentHeader.SetNextHeader(Ip6::kProtoIcmp6); + fragmentHeader.SetOffset(0); + fragmentHeader.SetMoreFlag(); + fragmentHeader.SetIdentification(aFragId); + SuccessOrQuit(message->Append(fragmentHeader)); + + SuccessOrQuit(message->AppendBytes(icmp, kPay1)); + + SuccessOrQuit(aSender.Get().SendRaw(OwnedPtr(message))); + } + + aNexus.AdvanceTime(400); + + if (aPrimeBetween) + { + // seed the message-pool free list with a known pattern right before the reassembly growth happens + SendPrimers(aSender, aDst, aGapAssume); + aNexus.AdvanceTime(400); + } + + // ---- fragment 2 (final): an optional Hop-by-Hop extension header widens the + // unfragmentable part + + { + Message *message = NewIp6Message(aSender); + Ip6::Header ip6Header; + Ip6::FragmentHeader fragmentHeader; + uint16_t extLength = aWithHole ? static_cast(sizeof(Ip6::HopByHopHeader) + kPadOptionSize) : 0; + + ip6Header.InitVersionTrafficClassFlow(); + ip6Header.SetPayloadLength(static_cast(extLength + sizeof(Ip6::FragmentHeader) + kPay2)); + ip6Header.SetNextHeader(aWithHole ? Ip6::kProtoHopOpts : Ip6::kProtoFragment); + ip6Header.SetHopLimit(kHopLimit); + ip6Header.SetSource(aSrc); + ip6Header.SetDestination(aDst); + SuccessOrQuit(message->Append(ip6Header)); + + if (aWithHole) + { + Ip6::HopByHopHeader hbhHeader; + Ip6::PadOption padOption; + + hbhHeader.SetNextHeader(Ip6::kProtoFragment); + hbhHeader.SetLength(0); // one 8-byte unit in total + SuccessOrQuit(message->Append(hbhHeader)); + + padOption.InitForPadSize(kPadOptionSize); + SuccessOrQuit(message->AppendBytes(&padOption, padOption.GetSize())); + } + + fragmentHeader.Init(); + fragmentHeader.SetNextHeader(Ip6::kProtoIcmp6); + fragmentHeader.SetOffset(Ip6::FragmentHeader::BytesToFragmentOffset(kPay1)); + fragmentHeader.ClearMoreFlag(); + fragmentHeader.SetIdentification(aFragId); + SuccessOrQuit(message->Append(fragmentHeader)); + + SuccessOrQuit(message->AppendBytes(tail, kPay2)); + + SuccessOrQuit(aSender.Get().SendRaw(OwnedPtr(message))); + } +} + +void TestFragmentReassemblyHole(void) +{ + Core nexus; + Node &leader = nexus.CreateNode(); + Node &child = nexus.CreateNode(); + + otIcmp6Handler handler; + + memset(&handler, 0, sizeof(handler)); + handler.mReceiveCallback = HandleIcmp; + + leader.Form(); + nexus.AdvanceTime(kFormNetworkTime); + VerifyOrQuit(leader.Get().IsLeader()); + + child.Join(leader); + nexus.AdvanceTime(20 * 1000); + VerifyOrQuit(child.Get().IsAttached()); + + SuccessOrQuit(otIcmp6RegisterHandler(&child.GetInstance(), &handler)); + + const Ip6::Address &src = child.Get().GetMeshLocalEid(); + const Ip6::Address &dst = leader.Get().GetMeshLocalEid(); + + Log("Case A (positive control): consistent-header fragmented echo must be answered"); + sGotReply = false; + sReplyHasMarkerRun = false; + BuildAndSendFragments(child, nexus, src, dst, 0x11111111, /* aWithHole */ false, 0, false, 1); + nexus.AdvanceTime(3 * 1000); + VerifyOrQuit(sGotReply); + + // the reply must reflect the seeded marker run, proving the reply-content inspection works + VerifyOrQuit(sReplyHasMarkerRun); + Log("control reply received (len %u, seeded marker run reflected)", sReplyLength); + + Log("Case B: final fragment carries an extra HBH header (longer unfragmentable part)."); + Log("A reply arrives only when the reassembled datagram content matches one of the"); + Log("checksum assumptions, i.e., only if an unwritten region was incorporated."); + + struct Attempt + { + uint8_t mGapAssume; + bool mPrimeBetween; + }; + + // (1) seed the free list with 0xC3 right before the growth; (2) assume the region + // reuses fragment 1's own freed payload bytes; (3) assume cleared/zero bytes. + Attempt attempts[] = {{kMarker, true}, {kPayByte, false}, {0x00, false}}; + bool replied = false; + uint8_t repliedVal = 0; + + for (uint8_t i = 0; i < 3; i++) + { + sGotReply = false; + sReplyHasMarkerRun = false; + BuildAndSendFragments(child, nexus, src, dst, 0x22220000u + i, /* aWithHole */ true, attempts[i].mGapAssume, + attempts[i].mPrimeBetween, static_cast(10 + i)); + nexus.AdvanceTime(3 * 1000); + + Log("attempt %u (gap assumption 0x%02x, prime-between %u): %s", i, attempts[i].mGapAssume, + attempts[i].mPrimeBetween, sGotReply ? "ECHO REPLY RECEIVED" : "no reply"); + + if (sGotReply) + { + replied = true; + repliedVal = attempts[i].mGapAssume; + break; + } + } + + if (replied) + { + Log("mismatched-length request answered; region bytes == 0x%02x pattern", repliedVal); + Log("(reply len %u, marker run %s; unwritten region content was incorporated and reflected)", sReplyLength, + sReplyHasMarkerRun ? "present" : "absent"); + } + + // Corrected behavior: no unwritten region exists in the reassembled datagram, so + // none of the region-assuming checksums can match and no reply is produced for any + // attempt. + VerifyOrQuit(!replied); + VerifyOrQuit(!sReplyHasMarkerRun); + + Log("TestFragmentReassemblyHole passed"); +} + +} // namespace Nexus +} // namespace ot + +int main(void) +{ + ot::Nexus::TestFragmentReassemblyHole(); + printf("All tests passed\n"); + return 0; +}