Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package libupnp for openSUSE:Factory checked in at 2026-09-30 16:22:44 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/libupnp (Old) and /work/SRC/openSUSE:Factory/.libupnp.new.1465845 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "libupnp" Wed Sep 30 16:22:44 2026 rev:53 rq:1381625 version:22.1.7 Changes: -------- --- /work/SRC/openSUSE:Factory/libupnp/libupnp.changes 2026-09-28 10:38:44.994591088 +0200 +++ /work/SRC/openSUSE:Factory/.libupnp.new.1465845/libupnp.changes 2026-09-30 16:23:54.800507146 +0200 @@ -1,0 +2,15 @@ +Tue Sep 29 16:53:50 UTC 2026 - Jan Engelhardt <[email protected]> + +- Update to release 22.1.7 + * Fix a memory leak when a GENA subscription is freed. + [GHSA-h9f5-9vwp-h89q] + +------------------------------------------------------------------- +Mon Sep 28 17:55:52 UTC 2026 - Jan Engelhardt <[email protected]> + +- Update to release 22.1.6 + * GHSA-mhhw-gm73-c57g: Fix a heap over-read when parsing the + Callback header of an incoming GENA SUBSCRIBE request. + [GHSA-mhhw-gm73-c57g] + +------------------------------------------------------------------- Old: ---- libupnp-22.1.5.tar.bz2 New: ---- libupnp-22.1.7.tar.bz2 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ libupnp.spec ++++++ --- /var/tmp/diff_new_pack.CFgCkX/_old 2026-09-30 16:23:55.473535171 +0200 +++ /var/tmp/diff_new_pack.CFgCkX/_new 2026-09-30 16:23:55.475535254 +0200 @@ -19,7 +19,7 @@ %define pnpver 22 Name: libupnp -Version: 22.1.5 +Version: 22.1.7 Release: 0 Summary: An implementation of Universal Plug and Play (UPnP) License: BSD-3-Clause ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.CFgCkX/_old 2026-09-30 16:23:55.516536961 +0200 +++ /var/tmp/diff_new_pack.CFgCkX/_new 2026-09-30 16:23:55.520537128 +0200 @@ -1,5 +1,5 @@ -mtime: 1790373040 -commit: 0884d08aaa81307576fcb1ac2a591954268214ee78fb89b8ae4a34c7eedfdd94 +mtime: 1790700851 +commit: 21ad2a57bf59f5cb03c24b14dbd868634cafc7b55f6806abe0e73b4a3d8b4e64 url: https://src.opensuse.org/jengelh/libupnp revision: master ++++++ build.specials.obscpio ++++++ ++++++ build.specials.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.gitignore new/.gitignore --- old/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/.gitignore 2026-09-29 18:54:11.000000000 +0200 @@ -0,0 +1 @@ +.osc ++++++ libupnp-22.1.5.tar.bz2 -> libupnp-22.1.7.tar.bz2 ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.5/CMakeLists.txt new/libupnp-22.1.7/CMakeLists.txt --- old/libupnp-22.1.5/CMakeLists.txt 2026-09-25 21:45:40.000000000 +0200 +++ new/libupnp-22.1.7/CMakeLists.txt 2026-09-29 17:27:28.000000000 +0200 @@ -7,7 +7,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) project(PUPNP - VERSION 22.1.5 + VERSION 22.1.7 LANGUAGES C) include(GNUInstallDirs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.5/ChangeLog new/libupnp-22.1.7/ChangeLog --- old/libupnp-22.1.5/ChangeLog 2026-09-25 21:45:40.000000000 +0200 +++ new/libupnp-22.1.7/ChangeLog 2026-09-29 17:27:28.000000000 +0200 @@ -1,4 +1,31 @@ ******************************************************************************* +Version 22.1.7 +******************************************************************************* + +- GHSA-h9f5-9vwp-h89q: Fix a memory leak when a GENA subscription is freed. + The subscription's event queue recycles its list nodes: ListDelNode() + keeps them on the list's own free list, and only ListDestroy() releases + them. freeSubscription() drained the queue but never called + ListDestroy(), so every subscription a device accepted leaked at least + one list node when it was torn down, on UNSUBSCRIBE, on expiry or when + the device unregistered (CWE-401). A remote peer could repeat + SUBSCRIBE/UNSUBSCRIBE without credentials and grow the device's memory + without bound. Found and fixed by Damien Plisson (@damien78). + + +******************************************************************************* +Version 22.1.6 +******************************************************************************* + +- GHSA-mhhw-gm73-c57g: Fix a heap over-read when parsing the Callback header + of an incoming GENA SUBSCRIBE request. is_mark() and is_reserved() used + strchr(), which also matches the terminating NUL, so URI parsing did not + stop at the end of the string, and create_url_list() passed parse_uri() a + length two bytes too long. A Callback URL without the closing '>' read one + byte past its buffer (CWE-125). Found and fixed by @las7 (#641). + + +******************************************************************************* Version 22.1.5 ******************************************************************************* diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.5/README.md new/libupnp-22.1.7/README.md --- old/libupnp-22.1.5/README.md 2026-09-25 21:45:40.000000000 +0200 +++ new/libupnp-22.1.7/README.md 2026-09-29 17:27:28.000000000 +0200 @@ -108,6 +108,8 @@ | Release Number | Date | History | | -------------- | ---------- | ---------------------------------------- | +| 22.1.6 | 2026-09-27 | [Portable UPnP SDK][Portable UPnP SDK] | +| 22.1.5 | 2026-09-25 | [Portable UPnP SDK][Portable UPnP SDK] | | 22.1.4 | 2026-09-24 | [Portable UPnP SDK][Portable UPnP SDK] | | 22.1.3 | 2026-09-24 | [Portable UPnP SDK][Portable UPnP SDK] | | 22.1.2 | 2026-09-23 | [Portable UPnP SDK][Portable UPnP SDK] | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.5/docs/Doxyfile new/libupnp-22.1.7/docs/Doxyfile --- old/libupnp-22.1.5/docs/Doxyfile 2026-09-25 21:45:40.000000000 +0200 +++ new/libupnp-22.1.7/docs/Doxyfile 2026-09-29 17:27:28.000000000 +0200 @@ -38,7 +38,7 @@ # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 22.1.5 +PROJECT_NUMBER = 22.1.7 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.5/gtest/CMakeLists.txt new/libupnp-22.1.7/gtest/CMakeLists.txt --- old/libupnp-22.1.5/gtest/CMakeLists.txt 2026-09-25 21:45:40.000000000 +0200 +++ new/libupnp-22.1.7/gtest/CMakeLists.txt 2026-09-29 17:27:28.000000000 +0200 @@ -62,6 +62,29 @@ ) endif() +# regression test — freeSubscription() must free the list nodes its event +# queue parked for reuse; freeSubscription() and copy_subscription() are +# internal symbols so only the static variant is built. +if (NOT WIN32 AND UPNP_BUILD_STATIC) + add_executable(test_service_table-static test_service_table.cpp) + target_link_libraries(test_service_table-static + PRIVATE upnp_static GTest::gtest) + target_include_directories(test_service_table-static PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../upnp/src/inc/ + ${CMAKE_CURRENT_SOURCE_DIR}/../upnp/src/threadutil/ + ) + if(HAVE_MACRO_PREFIX_MAP) + target_compile_options(test_service_table-static + PRIVATE -fmacro-prefix-map=${CMAKE_SOURCE_DIR}/= + ) + endif() + gtest_add_tests( + TARGET test_service_table-static + TEST_PREFIX test-upnp- + TEST_SUFFIX -static + ) +endif() + # regression test — Windows UpnpGetIfInfo() must not format an address # family that was not found on the interface (amule-org/amule#242, #301); # UpnpSetIfAddrStrings() is an internal symbol so only the static variant diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.5/gtest/test_service_table.cpp new/libupnp-22.1.7/gtest/test_service_table.cpp --- old/libupnp-22.1.5/gtest/test_service_table.cpp 1970-01-01 01:00:00.000000000 +0100 +++ new/libupnp-22.1.7/gtest/test_service_table.cpp 2026-09-29 17:27:28.000000000 +0200 @@ -0,0 +1,98 @@ +// regression: freeSubscription() must release the list nodes its event +// queue parked for reuse. +// +// The LinkedList recycles its nodes: ListDelNode() parks a node on the +// list's own free list, and only ListDestroy() frees the parked ones. +// freeSubscription() drained sub->outgoing with ListDelNode(), through +// freeSubscriptionQueuedEvents(), and never called ListDestroy(), so every +// subscription that had queued an event leaked at least one ListNode when +// it was torn down. +// +// These tests check the free list directly, so they fail without the fix +// on any build; under LeakSanitizer the lost nodes are also reported. + +#include "gtest/gtest.h" + +extern "C" { +#include "ThreadPool.h" +#include "service_table.h" +} + +#include <cstdlib> +#include <cstring> + +#if defined(INCLUDE_DEVICE_APIS) && EXCLUDE_GENA == 0 + +// A queued event as genaInitNotifyCommon() stores it: a heap-allocated +// ThreadPoolJob. freeSubscriptionQueuedEvents() frees the job itself, and +// looks at job->arg only for the entries after the first. +static ThreadPoolJob *new_queued_job() +{ + return static_cast<ThreadPoolJob *>(calloc(1, sizeof(ThreadPoolJob))); +} + +TEST(FreeSubscription, releases_the_nodes_its_event_queue_recycled) +{ + subscription sub{}; + ASSERT_EQ(ListInit(&sub.outgoing, nullptr, free), 0); + + // Two queued events, then the head sent and removed the way + // genaNotifyThread() does it: its node is parked, not freed. + ListNode *head = ListAddTail(&sub.outgoing, new_queued_job()); + ASSERT_NE(head, nullptr); + ASSERT_NE(ListAddTail(&sub.outgoing, new_queued_job()), nullptr); + ListDelNode(&sub.outgoing, head, 1); + ASSERT_EQ(sub.outgoing.freeNodeList.freeListLength, 1); + + freeSubscription(&sub); + + // Without the fix, both nodes are still parked here -- the sent one + // and the one freeSubscriptionQueuedEvents() removed -- and they are + // lost once the subscription itself is freed. + EXPECT_EQ(sub.outgoing.freeNodeList.freeListLength, 0); + EXPECT_EQ(sub.outgoing.freeNodeList.head, nullptr); +} + +TEST(FreeSubscription, is_safe_on_a_copy_made_by_copy_subscription) +{ + // genaNotifyThread() sends each event from a stack copy of the + // subscription, frees it with freeSubscription(), and + // copy_subscription() gives that copy an empty event queue of its own. + static const char callback[] = "<http://192.168.0.2:49152/cb>"; + subscription sub{}; + subscription copy{}; + + ASSERT_EQ(ListInit(&sub.outgoing, nullptr, free), 0); + sub.DeliveryURLs.URLs = strdup(callback); + sub.DeliveryURLs.parsedURLs = + static_cast<uri_type *>(calloc(1, sizeof(uri_type))); + ASSERT_NE(sub.DeliveryURLs.URLs, nullptr); + ASSERT_NE(sub.DeliveryURLs.parsedURLs, nullptr); + ASSERT_EQ(parse_uri(sub.DeliveryURLs.URLs + 1, + strlen(callback) - 2, + &sub.DeliveryURLs.parsedURLs[0]), + HTTP_SUCCESS); + sub.DeliveryURLs.size = 1; + ASSERT_NE(ListAddTail(&sub.outgoing, new_queued_job()), nullptr); + + ASSERT_EQ(copy_subscription(&sub, ©), HTTP_SUCCESS); + EXPECT_EQ(ListSize(©.outgoing), 0); + + freeSubscription(©); + EXPECT_EQ(copy.outgoing.freeNodeList.freeListLength, 0); + EXPECT_EQ(copy.outgoing.freeNodeList.head, nullptr); + + // The original is untouched by freeing the copy. + EXPECT_EQ(ListSize(&sub.outgoing), 1); + freeSubscription(&sub); + EXPECT_EQ(sub.outgoing.freeNodeList.freeListLength, 0); + EXPECT_EQ(sub.outgoing.freeNodeList.head, nullptr); +} + +#endif /* INCLUDE_DEVICE_APIS && EXCLUDE_GENA == 0 */ + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.5/upnp/src/gena/gena_device.c new/libupnp-22.1.7/upnp/src/gena/gena_device.c --- old/libupnp-22.1.5/upnp/src/gena/gena_device.c 2026-09-25 21:45:40.000000000 +0200 +++ new/libupnp-22.1.7/upnp/src/gena/gena_device.c 2026-09-29 17:27:28.000000000 +0200 @@ -1175,7 +1175,7 @@ for (i = 0; i < URLS->size; i++) { if ((URLS->buff[i] == '<') && (i + 1 < URLS->size)) { if (((return_code = parse_uri(&URLS->buff[i + 1], - URLS->size - i + 1, + URLS->size - i - 1, &temp)) == HTTP_SUCCESS) && (temp.hostport.text.size != 0) && (temp.hostport.IPaddress.ss_family != @@ -1204,7 +1204,7 @@ for (i = 0; i < URLS->size; i++) { if ((URLS->buff[i] == '<') && (i + 1 < URLS->size)) { if (((return_code = parse_uri(&out->URLs[i + 1], - URLS->size - i + 1, + URLS->size - i - 1, &out->parsedURLs[URLcount2])) == HTTP_SUCCESS) && (out->parsedURLs[URLcount2] @@ -1239,9 +1239,9 @@ } } } - out->size = URLcount; + out->size = URLcount2; - return (int)URLcount; + return (int)URLcount2; } /*! diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.5/upnp/src/genlib/net/uri/uri.c new/libupnp-22.1.7/upnp/src/genlib/net/uri/uri.c --- old/libupnp-22.1.5/upnp/src/genlib/net/uri/uri.c 2026-09-25 21:45:40.000000000 +0200 +++ new/libupnp-22.1.7/upnp/src/genlib/net/uri/uri.c 2026-09-29 17:27:28.000000000 +0200 @@ -76,7 +76,7 @@ /*! [in] Char to be matched for RESERVED characters. */ char in) { - if (strchr(RESERVED, (int)in)) { + if (in != '\0' && strchr(RESERVED, (int)in)) { return 1; } else { return 0; @@ -93,7 +93,7 @@ /*! [in] Char to be matched for MARKED characters. */ char in) { - if (strchr(MARK, (int)in)) { + if (in != '\0' && strchr(MARK, (int)in)) { return 1; } else { return 0; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.5/upnp/src/genlib/service_table/service_table.c new/libupnp-22.1.7/upnp/src/genlib/service_table/service_table.c --- old/libupnp-22.1.5/upnp/src/genlib/service_table/service_table.c 2026-09-25 21:45:40.000000000 +0200 +++ new/libupnp-22.1.7/upnp/src/genlib/service_table/service_table.c 2026-09-29 17:27:28.000000000 +0200 @@ -205,6 +205,10 @@ if (sub) { free_URL_list(&sub->DeliveryURLs); freeSubscriptionQueuedEvents(sub); + /* ListDelNode() only parks the list nodes on the list's own + * free list for reuse; ListDestroy() is what releases them. + * The items are already freed, so freeItem is 0. */ + ListDestroy(&sub->outgoing, 0); } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.5/upnp/test/poc_gh_325.c new/libupnp-22.1.7/upnp/test/poc_gh_325.c --- old/libupnp-22.1.5/upnp/test/poc_gh_325.c 2026-09-25 21:45:40.000000000 +0200 +++ new/libupnp-22.1.7/upnp/test/poc_gh_325.c 2026-09-29 17:27:28.000000000 +0200 @@ -29,6 +29,7 @@ #include <stddef.h> #include <string.h> #include <sys/socket.h> + #include <sys/time.h> #include <unistd.h> /* regression: issue #325 -- test hook exported from libupnp */ @@ -48,6 +49,12 @@ if (sock < 0) return; + /* Once UpnpFinish() has released the port, connecting to it can end + * in a TCP self-connect (the port is in the ephemeral range), and + * recv() would then wait forever for a peer that is this socket. */ + struct timeval tv = {1, 0}; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + struct sockaddr_in addr; memset(&addr, 0, sizeof addr); addr.sin_family = AF_INET; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/libupnp-22.1.5/upnp/test/test_parse_uri.c new/libupnp-22.1.7/upnp/test/test_parse_uri.c --- old/libupnp-22.1.5/upnp/test/test_parse_uri.c 2026-09-25 21:45:40.000000000 +0200 +++ new/libupnp-22.1.7/upnp/test/test_parse_uri.c 2026-09-29 17:27:28.000000000 +0200 @@ -198,6 +198,42 @@ return 0; } +/* + * Regression test for PR #641: is_mark() and is_reserved() used + * strchr(SET, in), which matches the terminating NUL, so parse_uric() treated + * '\0' as a URI character and scanned past the end of the string whenever + * the caller's length overshot it. create_url_list() did exactly that with + * an unterminated GENA Callback header ("<http://host/path" with no '>'), + * causing a heap over-read (CWE-125). + * + * The length passed here covers the NUL plus one non-uric byte, so the + * buffer is never overrun; the path must stop at the NUL ("/a", size 2). + */ +static int check_nul_terminates_uri(void) +{ + uri_type url; + static const char s[] = "http://192.0.2.1/a\0 "; + + if (parse_uri(s, sizeof(s) - 1, &url) != HTTP_SUCCESS) { + printf("%s:%d parse_uri('%s') failed to parse a valid URL\n", + __FILE__, + __LINE__, + s); + return 1; + } + if (url.pathquery.size != 2) { + printf("%s:%d parse_uri('%s') pathquery.size = %d, expected 2 " + "-- the NUL terminator must not be a URI character\n", + __FILE__, + __LINE__, + s, + (int)url.pathquery.size); + return 1; + } + + return 0; +} + int main(void) { int i; @@ -230,6 +266,7 @@ failures += check_no_eager_resolution(); failures += check_literal_ip_is_noop(); failures += check_resolve_hostport_localhost(); + failures += check_nul_terminates_uri(); #ifdef _WIN32 WSACleanup();
