Description: lowpan6: fix FRAG1 decompression failure and add unit test (CVE_2026_91018)
Author: 	Simon Goldschmidt <goldsimon@gmx.de>
Last-Update: 2026-10-05

Signed-off-by: Simon Goldschmidt <goldsimon@gmx.de>
---
 contrib/ports/win32/msvc/lwip_unittests.vcxproj    |   2 +
 .../win32/msvc/lwip_unittests.vcxproj.filters      |   9 ++
 src/netif/lowpan6.c                                |   3 +-
 test/unit/Filelists.cmake                          |   1 +
 test/unit/Filelists.mk                             |   1 +
 test/unit/lowpan6/test_lowpan6.c                   | 148 +++++++++++++++++++++
 test/unit/lowpan6/test_lowpan6.h                   |   8 ++
 test/unit/lwip_unittests.c                         |   2 +
 test/unit/lwipopts.h                               |   1 +
 9 files changed, 174 insertions(+), 1 deletion(-)
 create mode 100644 test/unit/lowpan6/test_lowpan6.c
 create mode 100644 test/unit/lowpan6/test_lowpan6.h

diff --git a/contrib/ports/win32/msvc/lwip_unittests.vcxproj b/contrib/ports/win32/msvc/lwip_unittests.vcxproj
index 270ba284..85e327d4 100644
--- a/contrib/ports/win32/msvc/lwip_unittests.vcxproj
+++ b/contrib/ports/win32/msvc/lwip_unittests.vcxproj
@@ -165,6 +165,7 @@
     <ClCompile Include="..\..\..\..\test\unit\core\test_timers.c" />
     <ClCompile Include="..\..\..\..\test\unit\ip4\test_ip4.c" />
     <ClCompile Include="..\..\..\..\test\unit\ip6\test_ip6.c" />
+    <ClCompile Include="..\..\..\..\test\unit\lowpan6\test_lowpan6.c" />
     <ClCompile Include="..\..\..\..\test\unit\mdns\test_mdns.c" />
     <ClCompile Include="..\..\..\..\test\unit\mqtt\test_mqtt.c" />
     <ClCompile Include="..\..\..\..\test\unit\ppp\test_pppos.c" />
@@ -188,6 +189,7 @@
     <ClInclude Include="..\..\..\..\test\unit\core\test_timers.h" />
     <ClInclude Include="..\..\..\..\test\unit\ip4\test_ip4.h" />
     <ClInclude Include="..\..\..\..\test\unit\ip6\test_ip6.h" />
+    <ClInclude Include="..\..\..\..\test\unit\lowpan6\test_lowpan6.h" />
     <ClInclude Include="..\..\..\..\test\unit\mdns\test_mdns.h" />
     <ClInclude Include="..\..\..\..\test\unit\mqtt\test_mqtt.h" />
     <ClInclude Include="..\..\..\..\test\unit\ppp\test_pppos.h" />
diff --git a/contrib/ports/win32/msvc/lwip_unittests.vcxproj.filters b/contrib/ports/win32/msvc/lwip_unittests.vcxproj.filters
index bbac4e7b..d4e20251 100644
--- a/contrib/ports/win32/msvc/lwip_unittests.vcxproj.filters
+++ b/contrib/ports/win32/msvc/lwip_unittests.vcxproj.filters
@@ -37,6 +37,9 @@
     <Filter Include="ppp">
       <UniqueIdentifier>{4d24c808-c024-4aba-a214-e5bc276e124d}</UniqueIdentifier>
     </Filter>
+    <Filter Include="lowpan6">
+      <UniqueIdentifier>{7b1a7667-b6c5-4f47-85ff-edfe74a7f7c7}</UniqueIdentifier>
+    </Filter>
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="..\..\..\..\test\unit\core\test_mem.c">
@@ -100,6 +103,9 @@
     <ClCompile Include="..\..\..\..\test\unit\tcp\test_tcp_state.c">
       <Filter>tcp</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\..\..\test\unit\lowpan6\test_lowpan6.c">
+      <Filter>lowpan6</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\..\..\test\unit\core\test_mem.h">
@@ -164,5 +170,8 @@
     <ClInclude Include="..\..\..\..\test\unit\tcp\test_tcp_state.h">
       <Filter>tcp</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\..\..\test\unit\lowpan6\test_lowpan6.h">
+      <Filter>lowpan6</Filter>
+    </ClInclude>
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/src/netif/lowpan6.c b/src/netif/lowpan6.c
index 8eb751c0..7f6849c2 100644
--- a/src/netif/lowpan6.c
+++ b/src/netif/lowpan6.c
@@ -726,7 +726,8 @@ lowpan6_input(struct pbuf *p, struct netif *netif)
       if (lrh->reass == NULL) {
         /* decompression failed */
         mem_free(lrh);
-        goto lowpan6_input_discard;
+        MIB2_STATS_NETIF_INC(netif, ifindiscards);
+        return ERR_OK;
       }
     }
     /* TODO: handle the case where we already have FRAGN received */
diff --git a/test/unit/Filelists.cmake b/test/unit/Filelists.cmake
index b3db893c..16469221 100644
--- a/test/unit/Filelists.cmake
+++ b/test/unit/Filelists.cmake
@@ -27,6 +27,7 @@ set(LWIP_TESTFILES
 	${LWIP_TESTDIR}/etharp/test_etharp.c
 	${LWIP_TESTDIR}/ip4/test_ip4.c
 	${LWIP_TESTDIR}/ip6/test_ip6.c
+	${LWIP_TESTDIR}/lowpan6/test_lowpan6.c
 	${LWIP_TESTDIR}/mdns/test_mdns.c
 	${LWIP_TESTDIR}/mqtt/test_mqtt.c
 	${LWIP_TESTDIR}/tcp/tcp_helper.c
diff --git a/test/unit/Filelists.mk b/test/unit/Filelists.mk
index c33e1b04..578cc697 100644
--- a/test/unit/Filelists.mk
+++ b/test/unit/Filelists.mk
@@ -43,6 +43,7 @@ TESTFILES=$(TESTDIR)/lwip_unittests.c \
 	$(TESTDIR)/etharp/test_etharp.c \
 	$(TESTDIR)/ip4/test_ip4.c \
 	$(TESTDIR)/ip6/test_ip6.c \
+	$(TESTDIR)/lowpan6/test_lowpan6.c \
 	$(TESTDIR)/mdns/test_mdns.c \
 	$(TESTDIR)/mqtt/test_mqtt.c \
 	$(TESTDIR)/tcp/tcp_helper.c \
diff --git a/test/unit/lowpan6/test_lowpan6.c b/test/unit/lowpan6/test_lowpan6.c
new file mode 100644
index 00000000..b5540721
--- /dev/null
+++ b/test/unit/lowpan6/test_lowpan6.c
@@ -0,0 +1,148 @@
+#include "test_lowpan6.h"
+
+#include "netif/lowpan6.h"
+
+#include "lwip/tcpip.h"
+
+#if LWIP_IPV6 /* allow to build the unit tests without IPv6 support */
+
+static struct netif test_netif_lowpan6;
+static int linkoutput_ctr;
+static int linkoutput_byte_ctr;
+
+/* Helper functions */
+static err_t
+default_netif_linkoutput(struct netif *netif, struct pbuf *p)
+{
+  fail_unless(netif == &test_netif_lowpan6);
+  fail_unless(p != NULL);
+  linkoutput_ctr++;
+  linkoutput_byte_ctr += p->tot_len;
+  return ERR_OK;
+}
+
+static err_t
+default_netif_init(struct netif *netif)
+{
+  fail_unless(netif == &test_netif_lowpan6);
+  netif->name[0] = 'L';
+  netif->name[1] = '6';
+  netif->output_ip6 = lowpan6_output;
+  netif->linkoutput = default_netif_linkoutput;
+  netif->mtu = IP6_MIN_MTU_LENGTH;
+  netif->hwaddr_len = 8;
+  netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_MLD6;
+  netif->hwaddr[0] = 0x00;
+  netif->hwaddr[1] = 0x23;
+  netif->hwaddr[2] = 0xC1;
+  netif->hwaddr[3] = 0xDE;
+  netif->hwaddr[4] = 0xD0;
+  netif->hwaddr[5] = 0x0D;
+#if NETIF_MAX_HWADDR_LEN < 8
+#error "6LowPAN needs NETIF_MAX_HWADDR_LEN == 8"
+#endif
+  netif->hwaddr[6] = 0x00;
+  netif->hwaddr[7] = 0x01;
+  netif_create_ip6_linklocal_address(netif, 1);
+  return ERR_OK;
+}
+
+static void
+default_netif_add(void)
+{
+  struct netif *n;
+  fail_unless(netif_default == NULL);
+  n = netif_add_noaddr(&test_netif_lowpan6, NULL, default_netif_init, lowpan6_input);
+  fail_unless(n == &test_netif_lowpan6);
+  netif_set_default(&test_netif_lowpan6);
+}
+
+static void
+default_netif_remove(void)
+{
+  fail_unless(netif_default == &test_netif_lowpan6);
+  netif_remove(&test_netif_lowpan6);
+}
+
+/* Setups/teardown functions */
+
+static void
+lowpan6_setup(void)
+{
+  default_netif_add();
+  lwip_check_ensure_no_alloc(SKIP_POOL(MEMP_SYS_TIMEOUT));
+}
+
+static void
+lowpan6_teardown(void)
+{
+  if (netif_list->loop_first != NULL) {
+    pbuf_free(netif_list->loop_first);
+    netif_list->loop_first = NULL;
+  }
+  netif_list->loop_last = NULL;
+  /* poll until all memory is released... */
+  tcpip_thread_poll_one();
+  default_netif_remove();
+  lwip_check_ensure_no_alloc(SKIP_POOL(MEMP_SYS_TIMEOUT));
+}
+
+START_TEST(test_6low_frag1_short)
+{
+  unsigned char frame[] = {
+     0x61, 0xC8,                                     /* Frame Control */
+     0x01,                                           /* Sequence Number */
+     0xCD, 0xAB,                                     /* Dst PAN ID (LE) */
+     0xFF, 0xFF,                                     /* Dst Short Addr (LE) */
+     0x01, 0x00, 0x0D, 0xD0, 0xDE, 0xC1, 0x23, 0x00, /* Src Ext Addr */
+     /* 6LoWPAN FRAG1 dispatch: b & 0xf8 == 0xc0 */
+     0xC0, 0x08, /* datagram_size=8, high bits */
+     0x00, 0x00, /* datagram_tag=0 */
+     /* IPHC dispatch byte (0x60) - triggers lowpan6_decompress which will
+      * fail because there's not enough data for the IPHC header */
+     0x60 };
+
+  struct pbuf* p;
+  err_t err;
+  LWIP_UNUSED_ARG(_i);
+
+  p = pbuf_alloc(PBUF_RAW, sizeof(frame), PBUF_POOL);
+  fail_unless(p != NULL);
+  if (p == NULL) {
+    return;
+  }
+  pbuf_take(p, frame, sizeof(frame));
+
+  err = lowpan6_input(p, &test_netif_lowpan6);
+  fail_unless(err == ERR_OK);
+}
+END_TEST
+
+/** Create the suite including all tests for this module */
+Suite *
+lowpan6_suite(void)
+{
+  testfunc tests[] = {
+    TESTFUNC(test_6low_frag1_short)
+  };
+  return create_suite("6LoWPAN", tests, sizeof(tests)/sizeof(testfunc), lowpan6_setup, lowpan6_teardown);
+}
+
+#else /* LWIP_IPV6 */
+
+/* allow to build the unit tests without IPv6 support */
+START_TEST(test_lowpan6_dummy)
+{
+  LWIP_UNUSED_ARG(_i);
+}
+END_TEST
+
+Suite *
+lowpan6_suite(void)
+{
+  testfunc tests[] = {
+    TESTFUNC(test_lowpan6_dummy),
+  };
+  return create_suite("6LoWPAN", tests, sizeof(tests)/sizeof(testfunc), NULL, NULL);
+}
+#endif /* LWIP_IPV6 */
diff --git a/test/unit/lowpan6/test_lowpan6.h b/test/unit/lowpan6/test_lowpan6.h
new file mode 100644
index 00000000..4635ba92
--- /dev/null
+++ b/test/unit/lowpan6/test_lowpan6.h
@@ -0,0 +1,8 @@
+#ifndef LWIP_HDR_TEST_LOWPAN6_H
+#define LWIP_HDR_TEST_LOWPAN6_H
+
+#include "../lwip_check.h"
+
+Suite* lowpan6_suite(void);
+
+#endif
diff --git a/test/unit/lwip_unittests.c b/test/unit/lwip_unittests.c
index d088a066..3dd9e9ac 100644
--- a/test/unit/lwip_unittests.c
+++ b/test/unit/lwip_unittests.c
@@ -18,6 +18,7 @@
 #include "mqtt/test_mqtt.h"
 #include "api/test_sockets.h"
 #include "ppp/test_pppos.h"
+#include "lowpan6//test_lowpan6.h"
 
 #include "lwip/init.h"
 #if !NO_SYS
@@ -83,6 +84,7 @@ int main(void)
   suite_getter_fn* suites[] = {
     ip4_suite,
     ip6_suite,
+    lowpan6_suite,
     udp_suite,
     tcp_suite,
     tcp_oos_suite,
diff --git a/test/unit/lwipopts.h b/test/unit/lwipopts.h
index 03579ee7..d42ff0bf 100644
--- a/test/unit/lwipopts.h
+++ b/test/unit/lwipopts.h
@@ -35,6 +35,7 @@
 #define LWIP_TESTMODE                   1
 
 #define LWIP_IPV6                       1
+#define NETIF_MAX_HWADDR_LEN            8U /* for 6LoWPAN tests */
 
 #define LWIP_CHECKSUM_ON_COPY           1
 #define TCP_CHECKSUM_ON_COPY_SANITY_CHECK 1
-- 
cgit v1.2.3

