From ceb310532b3dc16efcceb717c1653a03e96f17ea Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Mon, 20 Apr 2026 11:58:24 -0500 Subject: [PATCH 01/42] chore: update version to 1.5.1-rc1 and add changelog entry for security fixes --- CHANGELOG.md | 5 +++++ CMakeLists.txt | 2 +- src/bacnet/version.h | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71aec1f573..6f3fd20757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ The git repositories are hosted at the following sites: * * +## [1.5.1-rc1] - 2026-04-20 + +### Security +### Fixed + ## [1.5.0] - 2026-04-16 ### Security diff --git a/CMakeLists.txt b/CMakeLists.txt index 41c25272c2..ae6270d1c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project( bacnet-stack - VERSION 1.5.0 + VERSION 1.5.1-rc1 LANGUAGES C) # diff --git a/src/bacnet/version.h b/src/bacnet/version.h index 0f5a01e78e..a569252b98 100644 --- a/src/bacnet/version.h +++ b/src/bacnet/version.h @@ -15,8 +15,8 @@ #define BACNET_VERSION(x, y, z) (((x) << 16) + ((y) << 8) + (z)) #endif -#define BACNET_VERSION_TEXT "1.5.0" -#define BACNET_VERSION_CODE BACNET_VERSION(1, 5, 0) +#define BACNET_VERSION_TEXT "1.5.1-rc1" +#define BACNET_VERSION_CODE BACNET_VERSION(1, 5, 1) #define BACNET_VERSION_MAJOR ((BACNET_VERSION_CODE >> 16) & 0xFF) #define BACNET_VERSION_MINOR ((BACNET_VERSION_CODE >> 8) & 0xFF) #define BACNET_VERSION_MAINTENANCE (BACNET_VERSION_CODE & 0xFF) From e147aba3d6ec651612a027b2551cc2b67bae1d74 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Mon, 20 Apr 2026 11:52:44 -0500 Subject: [PATCH 02/42] Fix BBMD_Result handling to avoid false positive error message when no registration is requested. (#1305) --- src/bacnet/datalink/dlenv.c | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/bacnet/datalink/dlenv.c b/src/bacnet/datalink/dlenv.c index a49e04f336..d1b95bdad4 100644 --- a/src/bacnet/datalink/dlenv.c +++ b/src/bacnet/datalink/dlenv.c @@ -45,7 +45,12 @@ static uint16_t BBMD_TTL_Seconds = 60000; /* BBMD variables */ static BACNET_IP_ADDRESS BBMD_Address; static bool BBMD_Address_Valid; -static uint16_t BBMD_Result = 0; +/** BBMD Result: + * Positive number (of bytes sent) if registration was successful, + * 0 if no registration request was made, or + * -1 if registration attempt failed. + */ +static int BBMD_Result; #if defined(BACDL_BIP) && BBMD_ENABLED static BACNET_IP_BROADCAST_DISTRIBUTION_TABLE_ENTRY BBMD_Table_Entry; #endif @@ -130,7 +135,7 @@ int dlenv_bbmd_result(void) */ static int bbmd_register_as_foreign_device(void) { - int retval = -1; + int registration = 0; #if defined(BACDL_BIP) && BBMD_CLIENT_ENABLED char *pEnv = NULL; long long_value = 0; @@ -176,8 +181,8 @@ static int bbmd_register_as_foreign_device(void) (unsigned)BBMD_Address.address[3], (unsigned)BBMD_Address.port, (unsigned)BBMD_TTL_Seconds); } - retval = bvlc_register_with_bbmd(&BBMD_Address, BBMD_TTL_Seconds); - if (retval < 0) { + registration = bvlc_register_with_bbmd(&BBMD_Address, BBMD_TTL_Seconds); + if (registration < 0) { fprintf( stderr, "FAILED to Register with BBMD at %u.%u.%u.%u:%u\n", (unsigned)BBMD_Address.address[0], @@ -266,9 +271,9 @@ static int bbmd_register_as_foreign_device(void) } #endif #endif - BBMD_Result = retval; + BBMD_Result = registration; - return retval; + return registration; } /** Register as a Foreign Device with the designated BBMD. @@ -287,7 +292,7 @@ static int bbmd_register_as_foreign_device(void) */ static int bbmd6_register_as_foreign_device(void) { - int retval = -1; + int registration = 0; #if defined(BACDL_BIP6) && BBMD6_ENABLED char *pEnv = NULL; long long_value = 0; @@ -315,8 +320,8 @@ static int bbmd6_register_as_foreign_device(void) stderr, "Registering with BBMD6 at %s:0x%04x for %u seconds\n", pEnv, (unsigned)bip6_port, (unsigned)BBMD_TTL_Seconds); } - retval = bvlc6_register_with_bbmd(&bip6_addr, BBMD_TTL_Seconds); - if (retval < 0) { + registration = bvlc6_register_with_bbmd(&bip6_addr, BBMD_TTL_Seconds); + if (registration < 0) { fprintf( stderr, "FAILED to Register with BBMD6 at %s:%u\n", pEnv, (unsigned)BBMD_Address.port); @@ -324,9 +329,9 @@ static int bbmd6_register_as_foreign_device(void) BBMD_Timer_Seconds = BBMD_TTL_Seconds; } #endif - BBMD_Result = retval; + BBMD_Result = registration; - return retval; + return registration; } /** @@ -902,10 +907,10 @@ void dlenv_maintenance_timer(uint16_t elapsed_seconds) } if (BBMD_Timer_Seconds == 0) { if (Network_Port_Type(Network_Port_Instance) == PORT_TYPE_BIP) { - bbmd_register_as_foreign_device(); + (void)bbmd_register_as_foreign_device(); } else if ( Network_Port_Type(Network_Port_Instance) == PORT_TYPE_BIP6) { - bbmd6_register_as_foreign_device(); + (void)bbmd6_register_as_foreign_device(); } /* If that failed (negative), maybe just a network issue. * If nothing happened (0), may be un/misconfigured. From 5465aa6c427a0cec8976797194c4a65e8a73f29a Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Mon, 20 Apr 2026 12:00:09 -0500 Subject: [PATCH 03/42] docs: update CHANGELOG for 1.5.1-rc1 to include BBMD_Result fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f3fd20757..bb91a552c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,12 @@ The git repositories are hosted at the following sites: ## [1.5.1-rc1] - 2026-04-20 ### Security + ### Fixed +* Fix BBMD_Result handling to avoid false positive error message + when no registration is requested. (#1305) + ## [1.5.0] - 2026-04-16 ### Security From 638c3c25f03523e9dc0cb480965483374851a730 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 7 May 2026 22:44:51 -0500 Subject: [PATCH 04/42] Fix bounds checking in AtomicReadFile handler to prevent out-of-bounds writes --- CHANGELOG.md | 3 +++ src/bacnet/basic/service/h_arf.c | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb91a552c7..6697dc0f9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ The git repositories are hosted at the following sites: ### Security +* Secured AtomicReadFile handler by implementing bounds checks for + RecordCount stack based out-of-bounds write. (#1340) + ### Fixed * Fix BBMD_Result handling to avoid false positive error message diff --git a/src/bacnet/basic/service/h_arf.c b/src/bacnet/basic/service/h_arf.c index 8a5ba827cc..79f839c697 100644 --- a/src/bacnet/basic/service/h_arf.c +++ b/src/bacnet/basic/service/h_arf.c @@ -149,7 +149,12 @@ void handler_atomic_read_file( (int)octetstring_capacity(&data.fileData[0])); } } else if (data.access == FILE_RECORD_ACCESS) { - if (data.type.record.fileStartRecord >= + if (data.type.record.RecordCount > BACNET_READ_FILE_RECORD_COUNT) { + error_class = ERROR_CLASS_SERVICES; + error_code = ERROR_CODE_INCONSISTENT_PARAMETERS; + error = true; + } else if ( + data.type.record.fileStartRecord >= BACNET_READ_FILE_RECORD_COUNT) { error_class = ERROR_CLASS_SERVICES; error_code = ERROR_CODE_INVALID_FILE_START_POSITION; From 499cc09bdb18cf3b886cc21844283df6bd86b1e9 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Wed, 29 Apr 2026 07:27:19 -0500 Subject: [PATCH 05/42] bugfix: add null pointer check for value when resetting device identifier in bacdevobjpropref (#1321) * fix: add null pointer check for value when resetting device identifier in bacdevobjpropref * test: add regression test for bacnet_device_object_reference_decode with null value pointer --- src/bacnet/bacdevobjpropref.c | 6 ++++-- test/bacnet/bacdevobjpropref/src/main.c | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/bacnet/bacdevobjpropref.c b/src/bacnet/bacdevobjpropref.c index 76f1062fc4..e228945a42 100644 --- a/src/bacnet/bacdevobjpropref.c +++ b/src/bacnet/bacdevobjpropref.c @@ -489,8 +489,10 @@ int bacnet_device_object_reference_decode( return BACNET_STATUS_ERROR; } else { /* OPTIONAL - skip apdu_len increment */ - value->deviceIdentifier.type = BACNET_NO_DEV_TYPE; - value->deviceIdentifier.instance = BACNET_NO_DEV_ID; + if (value) { + value->deviceIdentifier.type = BACNET_NO_DEV_TYPE; + value->deviceIdentifier.instance = BACNET_NO_DEV_ID; + } } /* object-identifier [1] BACnetObjectIdentifier */ len = bacnet_object_id_context_decode( diff --git a/test/bacnet/bacdevobjpropref/src/main.c b/test/bacnet/bacdevobjpropref/src/main.c index 9a3ab60a1c..bf5bb719da 100644 --- a/test/bacnet/bacdevobjpropref/src/main.c +++ b/test/bacnet/bacdevobjpropref/src/main.c @@ -172,6 +172,14 @@ static void testDevIdRef(void) test_len = bacnet_device_object_reference_decode(NULL, sizeof(apdu), &test_data); zassert_true(test_len <= 0, NULL); + /* verify that NULL value pointer does not crash when the optional + device-identifier field is absent (regression test for the fix + that adds a null check before writing to value->deviceIdentifier) */ + data.deviceIdentifier.instance = 0; + data.deviceIdentifier.type = BACNET_NO_DEV_TYPE; + len = bacapp_encode_device_obj_ref(apdu, &data); + null_len = bacnet_device_object_reference_decode(apdu, len, NULL); + zassert_equal(null_len, len, "null_len=%d len=%d", null_len, len); } #if defined(CONFIG_ZTEST_NEW_API) From ff5660affe32d60b0518b6770af3bb593e211046 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 14:31:48 -0500 Subject: [PATCH 06/42] fix: correct memory allocation check in CheckArraySize and handle node creation failure in Keylist_Data_Add --- CHANGELOG.md | 6 +++++- src/bacnet/basic/sys/keylist.c | 20 +++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6697dc0f9b..b3cd687ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,13 +17,17 @@ The git repositories are hosted at the following sites: ### Security +* Secured WriteProperty to Structured View subordinate-list that caused a NULL + pointer dereference in bacnet_device_object_reference_decode(). (#1321) * Secured AtomicReadFile handler by implementing bounds checks for RecordCount stack based out-of-bounds write. (#1340) ### Fixed -* Fix BBMD_Result handling to avoid false positive error message +* Fixed BBMD_Result handling to avoid false positive error message when no registration is requested. (#1305) +* Fixed Keylist memory allocation check in CheckArraySize and handle + node creation failure in Keylist_Data_Add (#1295) ## [1.5.0] - 2026-04-16 diff --git a/src/bacnet/basic/sys/keylist.c b/src/bacnet/basic/sys/keylist.c index 96aba8f6a6..3dc602e5f1 100644 --- a/src/bacnet/basic/sys/keylist.c +++ b/src/bacnet/basic/sys/keylist.c @@ -69,7 +69,7 @@ static bool CheckArraySize(OS_Keylist list) /* See if we got the memory we wanted */ if (!new_array) { - return true; + return false; } /* copy the nodes from the old array to the new array */ @@ -171,6 +171,11 @@ int Keylist_Data_Add(OS_Keylist list, KEY key, void *data) int i; /* counts through the array */ if (list && CheckArraySize(list)) { + node = NodeCreate(); + if (!node) { + return -1; + } + /* figure out where to put the new node */ if (list->count) { (void)FindIndex(list, key, &index); @@ -190,14 +195,11 @@ int Keylist_Data_Add(OS_Keylist list, KEY key, void *data) index = 0; } - /* create and add the node */ - node = NodeCreate(); - if (node) { - list->count++; - node->key = key; - node->data = data; - list->array[index] = node; - } + /* add the node */ + list->count++; + node->key = key; + node->data = data; + list->array[index] = node; } return index; } From a61dd53f968ef7b38a43d18f7009f317aa42b92c Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 21 Apr 2026 22:31:51 -0500 Subject: [PATCH 07/42] Fixed EPICS values for recipient list, empty lists, and authentication factors (#1310) * Add BACnet authentication factor support with comparison functions and text strings and parsing * Fix MyReadPropertyAckHandler to allow zero length for empty property list * Fix rp_ack_fully_decode_service_request to handle empty recipient list * Enhance PrintReadPropertyArray to handle empty lists with improved output formatting * Refactor command line argument handling for target-address to accept dotted IP if offered. * This commit introduces the inclusion of `authentication_factor.c` and `authentication_factor_format.c` in the CMakeLists.txt files for multiple BACnet tests to fix the broken test builds. * Refactor bacapp.c for improved readability and documentation; streamline code formatting and enhance function comments. * Fix return value assignment in bacapp_snprintf_value for authentication format * Enhance help message to clarify target MAC or IP address format and improve readability * Implement memory management for ReadProperty ACK service requests and improve error handling in decoding --- CHANGELOG.md | 2 + apps/epics/main.c | 38 ++-- src/bacnet/authentication_factor.c | 22 +++ src/bacnet/authentication_factor.h | 6 +- src/bacnet/authentication_factor_format.c | 31 +++ src/bacnet/authentication_factor_format.h | 5 + src/bacnet/bacapp.c | 178 +++++++++++++++++- src/bacnet/bacapp.h | 6 + src/bacnet/bacenum.h | 10 +- src/bacnet/bactext.c | 63 +++++++ src/bacnet/bactext.h | 4 + src/bacnet/basic/service/h_rp_a.c | 65 ++++--- src/bacnet/config.h | 4 + test/bacnet/access_rule/CMakeLists.txt | 2 + .../CMakeLists.txt | 1 + test/bacnet/bacapp/CMakeLists.txt | 2 + test/bacnet/bacaudit/CMakeLists.txt | 2 + test/bacnet/bacdest/CMakeLists.txt | 2 + test/bacnet/bacdevobjpropref/CMakeLists.txt | 2 + test/bacnet/baclog/CMakeLists.txt | 2 + test/bacnet/bactimevalue/CMakeLists.txt | 2 + .../basic/binding/address/CMakeLists.txt | 2 + test/bacnet/basic/object/acc/CMakeLists.txt | 2 + .../object/access_credential/CMakeLists.txt | 1 + .../basic/object/access_door/CMakeLists.txt | 2 + .../basic/object/access_point/CMakeLists.txt | 2 + .../basic/object/access_rights/CMakeLists.txt | 2 + .../basic/object/access_user/CMakeLists.txt | 2 + .../basic/object/access_zone/CMakeLists.txt | 1 + test/bacnet/basic/object/ai/CMakeLists.txt | 2 + test/bacnet/basic/object/ao/CMakeLists.txt | 2 + .../basic/object/auditlog/CMakeLists.txt | 2 + test/bacnet/basic/object/av/CMakeLists.txt | 2 + .../basic/object/bacfile/CMakeLists.txt | 2 + test/bacnet/basic/object/bi/CMakeLists.txt | 2 + .../object/bitstring_value/CMakeLists.txt | 2 + test/bacnet/basic/object/blo/CMakeLists.txt | 2 + test/bacnet/basic/object/bo/CMakeLists.txt | 2 + test/bacnet/basic/object/bv/CMakeLists.txt | 2 + .../basic/object/calendar/CMakeLists.txt | 2 + .../basic/object/channel/CMakeLists.txt | 2 + .../basic/object/color_object/CMakeLists.txt | 2 + .../object/color_temperature/CMakeLists.txt | 2 + .../basic/object/command/CMakeLists.txt | 2 + test/bacnet/basic/object/csv/CMakeLists.txt | 2 + test/bacnet/basic/object/iv/CMakeLists.txt | 2 + test/bacnet/basic/object/lc/CMakeLists.txt | 2 + test/bacnet/basic/object/lo/CMakeLists.txt | 2 + test/bacnet/basic/object/loop/CMakeLists.txt | 2 + test/bacnet/basic/object/lsp/CMakeLists.txt | 2 + test/bacnet/basic/object/lsz/CMakeLists.txt | 2 + .../basic/object/ms-input/CMakeLists.txt | 2 + test/bacnet/basic/object/mso/CMakeLists.txt | 2 + test/bacnet/basic/object/msv/CMakeLists.txt | 2 + test/bacnet/basic/object/nc/CMakeLists.txt | 2 + .../basic/object/netport/CMakeLists.txt | 2 + test/bacnet/basic/object/osv/CMakeLists.txt | 2 + test/bacnet/basic/object/piv/CMakeLists.txt | 2 + .../basic/object/program/CMakeLists.txt | 2 + .../basic/object/schedule/CMakeLists.txt | 2 + .../object/structured_view/CMakeLists.txt | 2 + .../basic/object/time_value/CMakeLists.txt | 2 + test/bacnet/basic/object/timer/CMakeLists.txt | 2 + .../basic/object/trendlog/CMakeLists.txt | 2 + .../basic/server/bacnet_device/CMakeLists.txt | 2 + test/bacnet/cov/CMakeLists.txt | 2 + test/bacnet/create_object/CMakeLists.txt | 2 + .../datalink/bsc-datalink/CMakeLists.txt | 2 + test/bacnet/datalink/bsc-node/CMakeLists.txt | 2 + .../bacnet/datalink/bsc-socket/CMakeLists.txt | 2 + test/bacnet/datalink/hub-sc/CMakeLists.txt | 2 + test/bacnet/delete_object/CMakeLists.txt | 2 + test/bacnet/event/CMakeLists.txt | 2 + test/bacnet/getalarm/CMakeLists.txt | 2 + test/bacnet/getevent/CMakeLists.txt | 2 + test/bacnet/hostnport/CMakeLists.txt | 2 + test/bacnet/list_element/CMakeLists.txt | 2 + test/bacnet/lso/CMakeLists.txt | 2 + test/bacnet/ptransfer/CMakeLists.txt | 2 + test/bacnet/rpm/CMakeLists.txt | 2 + test/bacnet/secure_connect/CMakeLists.txt | 2 + test/bacnet/specialevent/CMakeLists.txt | 2 + test/bacnet/timesync/CMakeLists.txt | 2 + test/bacnet/weeklyschedule/CMakeLists.txt | 2 + test/bacnet/wp/CMakeLists.txt | 2 + test/bacnet/wpm/CMakeLists.txt | 2 + test/bacnet/write_group/CMakeLists.txt | 2 + 87 files changed, 522 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3cd687ad3..5cd4c8937b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ The git repositories are hosted at the following sites: ### Fixed +* Fixed EPICS values for recipient list, empty lists, and authentication + factors. Fixed EPICS app to allow target MAC or IP address format. (#1310) * Fixed BBMD_Result handling to avoid false positive error message when no registration is requested. (#1305) * Fixed Keylist memory allocation check in CheckArraySize and handle diff --git a/apps/epics/main.c b/apps/epics/main.c index 6737274179..1b512f897c 100644 --- a/apps/epics/main.c +++ b/apps/epics/main.c @@ -245,7 +245,7 @@ static void MyReadPropertyAckHandler( len = rp_ack_fully_decode_service_request( service_request, service_len, rp_data); } - if (len > 0) { + if (len >= 0) { memmove( &Read_Property_Multiple_Data.service_data, service_data, sizeof(BACNET_CONFIRMED_SERVICE_ACK_DATA)); @@ -472,7 +472,11 @@ static void PrintReadPropertyArray( if (Walked_List_Index == 1) { /* If the array is empty, make it VTS3-friendly */ if (value->tag == BACNET_APPLICATION_TAG_EMPTYLIST) { - fprintf(stdout, "?\n "); + if (ShowValues) { + fprintf(stdout, "{}\n"); + } else { + fprintf(stdout, "{?}\n"); + } return; } @@ -575,7 +579,7 @@ static void PrintReadPropertyData( * But are we showing Values? We (VTS3) want ? instead of {?,?} to show * up. */ switch (rpm_property->propertyIdentifier) { - /* Screen the Properties that can be arrays or Sequences */ + /* Screen the Properties that can be arrays or Sequences */ case PROP_PRESENT_VALUE: case PROP_PRIORITY_ARRAY: if (!ShowValues) { @@ -922,11 +926,12 @@ static void print_help(const char *filename) printf("-v: show values instead of '?' \n"); printf("-c: columns break for BACnetARRAY. Default is 0=always\n"); printf("-d: show only device object properties\n"); - printf("-p: Use sport for \"my\" port. 0xBAC0 is default.\n"); + printf("-p: Use sport for \"my\" port. 47808 is default.\n"); printf(" Allows you to communicate with a localhost target.\n"); - printf("-t: declare target's MAC instead of using Who-Is to bind to \n"); - printf(" device-instance. Format is \"C0:A8:00:18:BA:C0\"\n"); - printf(" Use \"7F:00:00:01:BA:C0\" for loopback testing \n"); + printf("-t: declare target's MAC or IP address instead of using Who-Is\n"); + printf(" to bind to device-instance.\n"); + printf(" Format is \"192.168.1.42:47808\" or \"C0:A8:01:2A:BA:C0\".\n"); + printf(" Use \"127.0.0.1:47808\" for loopback testing.\n"); printf("-n: specify target's DNET if not local BACnet network \n"); printf(" or on routed Virtual Network \n"); printf("\n"); @@ -938,6 +943,7 @@ static int CheckCommandLineArgs(int argc, char *argv[]) { int i; bool bFoundTarget = false; + BACNET_MAC_ADDRESS mac = { 0 }; int argi = 0; const char *filename = NULL; @@ -1002,22 +1008,8 @@ static int CheckCommandLineArgs(int argc, char *argv[]) break; case 't': if (++i < argc) { - /* decoded MAC addresses */ - unsigned mac[6]; - /* number of successful decodes */ - int count; - /* loop counter */ - unsigned j; - count = sscanf( - argv[i], "%2x:%2x:%2x:%2x:%2x:%2x", &mac[0], - &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]); - if (count == 6) { /* success */ - Target_Address.mac_len = count; - for (j = 0; j < 6; j++) { - Target_Address.mac[j] = (uint8_t)mac[j]; - } - Target_Address.net = 0; - Target_Address.len = 0; /* No src address */ + if (bacnet_address_mac_from_ascii(&mac, argv[i])) { + bacnet_address_init(&Target_Address, &mac, 0, NULL); Provided_Targ_MAC = true; break; } else { diff --git a/src/bacnet/authentication_factor.c b/src/bacnet/authentication_factor.c index d40d2e086b..1821b279e8 100644 --- a/src/bacnet/authentication_factor.c +++ b/src/bacnet/authentication_factor.c @@ -205,3 +205,25 @@ int bacapp_decode_context_authentication_factor( return bacnet_authentication_factor_context_decode(apdu, MAX_APDU, tag, af); } #endif + +/** + * @brief Compare two BACnetAuthenticationFactor values for equality + * @param value Pointer to the first value to compare + * @param test_value Pointer to the second value to compare + * @return true if the values are the same, false otherwise + */ +bool bacnet_authentication_factor_same( + const BACNET_AUTHENTICATION_FACTOR *value, + const BACNET_AUTHENTICATION_FACTOR *test_value) +{ + if (value == NULL || test_value == NULL) { + return false; + } + if (value->format_type != test_value->format_type) { + return false; + } + if (value->format_class != test_value->format_class) { + return false; + } + return octetstring_value_same(&value->value, &test_value->value); +} diff --git a/src/bacnet/authentication_factor.h b/src/bacnet/authentication_factor.h index cf3ac3435e..f19e297008 100644 --- a/src/bacnet/authentication_factor.h +++ b/src/bacnet/authentication_factor.h @@ -13,7 +13,7 @@ /* BACnet Stack defines - first */ #include "bacnet/bacdef.h" /* BACnet Stack API */ -#include "bacnet/bacapp.h" +#include "bacnet/bacstr.h" typedef struct BACnetAuthenticationFactor { BACNET_AUTHENTICATION_FACTOR_TYPE format_type; @@ -51,6 +51,10 @@ BACNET_STACK_DEPRECATED( BACNET_STACK_EXPORT int bacapp_decode_context_authentication_factor( const uint8_t *apdu, uint8_t tag, BACNET_AUTHENTICATION_FACTOR *af); +BACNET_STACK_EXPORT +bool bacnet_authentication_factor_same( + const BACNET_AUTHENTICATION_FACTOR *value, + const BACNET_AUTHENTICATION_FACTOR *test_value); #ifdef __cplusplus } diff --git a/src/bacnet/authentication_factor_format.c b/src/bacnet/authentication_factor_format.c index 3e15b1fdc6..2808f2f81a 100644 --- a/src/bacnet/authentication_factor_format.c +++ b/src/bacnet/authentication_factor_format.c @@ -223,3 +223,34 @@ int bacapp_decode_context_authentication_factor_format( apdu, MAX_APDU, tag, data); } #endif + +/** + * @brief Compare two BACNET_AUTHENTICATION_FACTOR_FORMAT structures + * @param data1 Pointer to the first structure to compare + * @param data2 Pointer to the second structure to compare + * @return true if the structures are the same, false otherwise + * @details The structures are considered the same if they have the same format + * type, and if the format type is CUSTOM, they must also have the same vendor + * ID and vendor format. + */ +bool bacnet_authentication_factor_format_same( + const BACNET_AUTHENTICATION_FACTOR_FORMAT *data1, + const BACNET_AUTHENTICATION_FACTOR_FORMAT *data2) +{ + if (!data1 || !data2) { + return false; + } + if (data1->format_type != data2->format_type) { + return false; + } + if (data1->format_type == AUTHENTICATION_FACTOR_CUSTOM) { + if (data1->vendor_id != data2->vendor_id) { + return false; + } + if (data1->vendor_format != data2->vendor_format) { + return false; + } + } + + return true; +} diff --git a/src/bacnet/authentication_factor_format.h b/src/bacnet/authentication_factor_format.h index 741d473d2b..4d64e9c9b4 100644 --- a/src/bacnet/authentication_factor_format.h +++ b/src/bacnet/authentication_factor_format.h @@ -55,6 +55,11 @@ int bacapp_decode_context_authentication_factor_format( uint8_t tag_number, BACNET_AUTHENTICATION_FACTOR_FORMAT *aff); +BACNET_STACK_EXPORT +bool bacnet_authentication_factor_format_same( + const BACNET_AUTHENTICATION_FACTOR_FORMAT *data1, + const BACNET_AUTHENTICATION_FACTOR_FORMAT *data2); + #ifdef __cplusplus } #endif /* __cplusplus */ diff --git a/src/bacnet/bacapp.c b/src/bacnet/bacapp.c index 6dd5885b67..7758950325 100644 --- a/src/bacnet/bacapp.c +++ b/src/bacnet/bacapp.c @@ -409,6 +409,18 @@ int bacapp_encode_application_data( apdu_len = bacnet_timer_value_no_value_encode(apdu); break; #endif +#if defined(BACAPP_AUTHENTICATION) + case BACNET_APPLICATION_TAG_AUTHENTICATION_FORMAT: + /* BACnetAuthenticationFactorFormat */ + apdu_len = bacapp_encode_authentication_factor_format( + apdu, &value->type.Authentication_Format); + break; + case BACNET_APPLICATION_TAG_AUTHENTICATION_FACTOR: + /* BACnetAuthenticationFactor */ + apdu_len = bacapp_encode_authentication_factor( + apdu, &value->type.Authentication_Factor); + break; +#endif #if defined(BACAPP_LOG_RECORD) case BACNET_APPLICATION_TAG_LOG_RECORD: /* BACnetLogRecord */ @@ -1138,6 +1150,9 @@ int bacapp_known_property_tag( } else if (object_type == OBJECT_CHANNEL) { /* Properties using BACnetChannelValue */ return BACNET_APPLICATION_TAG_CHANNEL_VALUE; + } else if (object_type == OBJECT_CREDENTIAL_DATA_INPUT) { + /* Properties using BACnetAuthenticationFactor */ + return BACNET_APPLICATION_TAG_AUTHENTICATION_FACTOR; } /* note: primitive application tagged present-values return '-1' */ return -1; @@ -1193,7 +1208,9 @@ int bacapp_known_property_tag( case PROP_SLAVE_ADDRESS_BINDING: /* BACnetAddressBinding */ return BACNET_APPLICATION_TAG_ADDRESS_BINDING; - + case PROP_SUPPORTED_FORMATS: + /* BACnetAuthenticationFactorFormat */ + return BACNET_APPLICATION_TAG_AUTHENTICATION_FORMAT; case PROP_LOG_BUFFER: /* BACnetLogRecord */ return BACNET_APPLICATION_TAG_LOG_RECORD; @@ -1573,6 +1590,19 @@ int bacapp_decode_application_tag_value( apdu, apdu_size, &value->type.Address_Binding); break; #endif +#if defined(BACAPP_AUTHENTICATION) + case BACNET_APPLICATION_TAG_AUTHENTICATION_FORMAT: + /* BACnetAuthenticationFactorFormat */ + apdu_len = bacnet_authentication_factor_format_decode( + apdu, apdu_size, &value->type.Authentication_Format); + break; + case BACNET_APPLICATION_TAG_AUTHENTICATION_FACTOR: + /* BACnetAuthenticationFactor */ + apdu_len = bacnet_authentication_factor_decode( + apdu, apdu_size, &value->type.Authentication_Factor); + break; + +#endif #if defined(BACAPP_LOG_RECORD) case BACNET_APPLICATION_TAG_LOG_RECORD: /* BACnetLogRecord */ @@ -1629,7 +1659,7 @@ int bacapp_decode_known_array_property( int apdu_len = 0; int tag; - if (bacnet_is_closing_tag(apdu, apdu_size)) { + if ((apdu_size == 0) || bacnet_is_closing_tag(apdu, apdu_size)) { if (value) { value->tag = BACNET_APPLICATION_TAG_EMPTYLIST; } @@ -3755,6 +3785,29 @@ static int bacapp_snprintf_action_command( } #endif +#if defined(BACAPP_AUTHENTICATION) +int bacapp_snprintf_authentication_factor( + char *str, size_t str_len, const BACNET_AUTHENTICATION_FACTOR *value) +{ + int slen; + int ret_val = 0; + + slen = bacapp_snprintf(str, str_len, "{"); + ret_val += bacapp_snprintf_shift(slen, &str, &str_len); + slen = bacapp_snprintf( + str, str_len, "%s,%lu,", + bactext_authentication_factor_type_name(value->format_type), + (unsigned long)value->format_class); + ret_val += bacapp_snprintf_shift(slen, &str, &str_len); + slen = bacapp_snprintf_octet_string(str, str_len, &value->value); + ret_val += bacapp_snprintf_shift(slen, &str, &str_len); + slen = bacapp_snprintf(str, str_len, "}"); + ret_val += bacapp_snprintf_shift(slen, &str, &str_len); + + return ret_val; +} +#endif + /** * @brief Extract the value into a text string * @param str - the buffer to store the extracted value, or NULL for length @@ -4053,6 +4106,22 @@ int bacapp_snprintf_value( ret_val = bacnet_timer_value_no_value_to_ascii(str, str_len); break; #endif +#if defined(BACAPP_AUTHENTICATION) + case BACNET_APPLICATION_TAG_AUTHENTICATION_FORMAT: + ret_val = bacapp_snprintf( + str, str_len, "{%s,%lu,%lu}", + bactext_authentication_factor_type_name( + value->type.Authentication_Format.format_type), + (unsigned long)value->type.Authentication_Format.vendor_id, + (unsigned long) + value->type.Authentication_Format.vendor_format); + break; + case BACNET_APPLICATION_TAG_AUTHENTICATION_FACTOR: + /* BACnetAuthenticationFactor */ + ret_val = bacapp_snprintf_authentication_factor( + str, str_len, &value->type.Authentication_Factor); + break; +#endif #if defined(BACAPP_LOG_RECORD) case BACNET_APPLICATION_TAG_LOG_RECORD: ret_val = bacapp_snprintf_log_record( @@ -4798,9 +4867,82 @@ special_event_from_ascii(BACNET_APPLICATION_DATA_VALUE *value, char *str) } #endif /* BACAPP_SPECIAL_EVENT */ -/* used to load the app data struct with the proper data - converted from a command line argument. - "argv" is not const to allow using strtok internally. It MAY be modified. */ +#if defined(BACAPP_AUTHENTICATION) +/** + * @brief Parse a string into a BACnetAuthenticationFactorFormat value + * @param value [out] The BACnetAuthenticationFactorFormat value + * @param argv [in] The string to parse + * @return True on success, else False + */ +static bool bacnet_authentication_format_from_ascii( + BACNET_AUTHENTICATION_FACTOR_FORMAT *value, const char *argv) +{ + bool status = false; + int count; + unsigned long format_type, vendor_id, vendor_format; + + if (!status) { + count = sscanf( + argv, "%lu,%lu,%lu", &format_type, &vendor_id, &vendor_format); + if (count == 3) { + /* optional fields are required when Format-Type + field has a value of CUSTOM. */ + value->format_type = (BACNET_AUTHENTICATION_FACTOR_TYPE)format_type; + value->vendor_id = (uint32_t)vendor_id; + value->vendor_format = (uint32_t)vendor_format; + status = true; + } else if (count == 1) { + value->format_type = (BACNET_AUTHENTICATION_FACTOR_TYPE)format_type; + value->vendor_id = 0; + value->vendor_format = 0; + status = true; + } + } + + return status; +} +/** + * @brief Parse a string into a BACnetAuthenticationFactor value + * @param value [out] The BACnetAuthenticationFactor value + * @param argv [in] The string to parse + * @return True on success, else False + */ +static bool bacnet_authentication_factor_from_ascii( + BACNET_AUTHENTICATION_FACTOR *value, const char *argv) +{ + bool status = false; + int count; + unsigned long format_type, format_class; + char factor_value[256] = { 0 }; + + count = sscanf( + argv, "%lu,%lu,%255s", &format_type, &format_class, factor_value); + if (count == 3) { + value->format_type = (BACNET_AUTHENTICATION_FACTOR_TYPE)format_type; + value->format_class = (uint32_t)format_class; + octetstring_init_ascii_epics(&value->value, factor_value); + status = true; + } + + return status; +} +#endif + +/** + * @brief Load the app data struct with the proper data converted from a command + * line argument. "argv" is not const to allow using strtok internally. It MAY + * be modified. + * @param tag_number The expected application tag number of the value to parse. + * This is used to determine how to parse the argv string. + * @param argv The string to parse into the value struct. This is typically a + * command line argument, and may be modified by this function. + * @param value [out] The BACNET_APPLICATION_DATA_VALUE struct to load with the + * parsed value. The tag field will be set to tag_number, and the type field + * will be set based on the tag. The value field will be set to the parsed value + * from argv. + * @return true if the data was successfully parsed and loaded into the value + * struct, else false + */ bool bacapp_parse_application_data( BACNET_APPLICATION_TAG tag_number, char *argv, @@ -5108,6 +5250,18 @@ bool bacapp_parse_application_data( bacnet_timer_value_no_value_from_ascii(&value->tag, argv); break; #endif +#if defined(BACAPP_AUTHENTICATION) + case BACNET_APPLICATION_TAG_AUTHENTICATION_FORMAT: + /* BACnetAuthenticationFactorFormat */ + status = bacnet_authentication_format_from_ascii( + &value->type.Authentication_Format, argv); + break; + case BACNET_APPLICATION_TAG_AUTHENTICATION_FACTOR: + /* BACnetAuthenticationFactor */ + status = bacnet_authentication_factor_from_ascii( + &value->type.Authentication_Factor, argv); + break; +#endif #if defined(BACAPP_LOG_RECORD) case BACNET_APPLICATION_TAG_LOG_RECORD: status = bacnet_log_record_datum_from_ascii( @@ -5876,6 +6030,20 @@ bool bacapp_same_value( } break; #endif +#if defined(BACAPP_AUTHENTICATION) + case BACNET_APPLICATION_TAG_AUTHENTICATION_FORMAT: + /* BACnetAuthenticationFactorFormat */ + status = bacnet_authentication_factor_format_same( + &value->type.Authentication_Format, + &test_value->type.Authentication_Format); + break; + case BACNET_APPLICATION_TAG_AUTHENTICATION_FACTOR: + /* BACnetAuthenticationFactor */ + status = bacnet_authentication_factor_same( + &value->type.Authentication_Factor, + &test_value->type.Authentication_Factor); + break; +#endif #if defined(BACAPP_LOG_RECORD) case BACNET_APPLICATION_TAG_LOG_RECORD: status = bacnet_log_record_same( diff --git a/src/bacnet/bacapp.h b/src/bacnet/bacapp.h index 7971739c7b..f6066f69e3 100644 --- a/src/bacnet/bacapp.h +++ b/src/bacnet/bacapp.h @@ -16,6 +16,8 @@ #include "bacnet/bacdef.h" /* BACnet Stack API */ #include "bacnet/access_rule.h" +#include "bacnet/authentication_factor.h" +#include "bacnet/authentication_factor_format.h" #include "bacnet/bacaction.h" #include "bacnet/bacaddr.h" #include "bacnet/bacdest.h" @@ -185,6 +187,10 @@ typedef struct BACnet_Application_Data_Value { BACNET_SC_HUB_FUNCTION_CONNECTION_STATUS SC_Hub_Function_Status; BACNET_SC_DIRECT_CONNECTION_STATUS SC_Direct_Status; BACNET_SC_HUB_CONNECTION_STATUS SC_Hub_Status; +#endif +#if defined(BACAPP_AUTHENTICATION) + BACNET_AUTHENTICATION_FACTOR_FORMAT Authentication_Format; + BACNET_AUTHENTICATION_FACTOR Authentication_Factor; #endif } type; /* simple linked list if needed */ diff --git a/src/bacnet/bacenum.h b/src/bacnet/bacenum.h index dc95f2d1f6..7e0432cadd 100644 --- a/src/bacnet/bacenum.h +++ b/src/bacnet/bacenum.h @@ -1705,6 +1705,10 @@ typedef enum { BACNET_APPLICATION_TAG_ADDRESS_BINDING, /* no-value - context tagged null */ BACNET_APPLICATION_TAG_NO_VALUE, + /* BACnetAuthenticationFactorFormat */ + BACNET_APPLICATION_TAG_AUTHENTICATION_FORMAT, + /* BACnetAuthenticationFactor */ + BACNET_APPLICATION_TAG_AUTHENTICATION_FACTOR, /* ABSTRACT-SYNTAX - constructed value */ BACNET_APPLICATION_TAG_ABSTRACT_SYNTAX, /* == mark the end of this list == */ @@ -2965,10 +2969,14 @@ typedef enum BACnetAuthenticationDisableReason { AUTHENTICATION_DISABLED_STOLEN = 3, AUTHENTICATION_DISABLED_DAMAGED = 4, AUTHENTICATION_DISABLED_DESTROYED = 5, - AUTHENTICATION_DISABLED_MAX = 6 + AUTHENTICATION_DISABLED_MAX = 6, + AUTHENTICATION_DISABLED_RESERVED_MIN = 6, + AUTHENTICATION_DISABLED_RESERVED_MAX = 63, /* Enumerated values 0-63 are reserved for definition by ASHRAE. Enumerated values 64-65535 may be used by others subject to the procedures and constraints described in Clause 23. */ + AUTHENTICATION_DISABLED_PROPRIETARY_MIN = 64, + AUTHENTICATION_DISABLED_PROPRIETARY_MAX = 65535 } BACNET_AUTHENTICATION_DISABLE_REASON; typedef enum BACnetAuthenticationFactorType { diff --git a/src/bacnet/bactext.c b/src/bacnet/bactext.c index 6918abdedc..22c9e04193 100644 --- a/src/bacnet/bactext.c +++ b/src/bacnet/bactext.c @@ -191,6 +191,10 @@ INDTEXT_DATA bacnet_application_tag_names[] = { { BACNET_APPLICATION_TAG_TIMER_VALUE, "BACnetTimerStateChangeValue" }, { BACNET_APPLICATION_TAG_ADDRESS_BINDING, "BACnetAddressBinding" }, { BACNET_APPLICATION_TAG_NO_VALUE, "BACnetNoValue" }, + { BACNET_APPLICATION_TAG_AUTHENTICATION_FORMAT, + "BACnetAuthenticationFormat" }, + { BACNET_APPLICATION_TAG_AUTHENTICATION_FACTOR, + "BACnetAuthenticationFactor" }, { BACNET_APPLICATION_TAG_ABSTRACT_SYNTAX, "ABSTRACT-SYNTAX" }, { 0, NULL } }; @@ -3379,6 +3383,65 @@ const char *bactext_authentication_status_name(uint32_t index) bactext_authentication_status_names, index, ASHRAE_Reserved_String); } +INDTEXT_DATA bactext_authentication_disable_reason_names[] = { + /* BACnetAuthenticationDisableReason enumerations */ + { AUTHENTICATION_NONE, "none" }, + { AUTHENTICATION_DISABLED, "disabled" }, + { AUTHENTICATION_DISABLED_LOST, "lost" }, + { AUTHENTICATION_DISABLED_STOLEN, "stolen" }, + { AUTHENTICATION_DISABLED_DAMAGED, "damaged" }, + { AUTHENTICATION_DISABLED_DESTROYED, "destroyed" }, + { 0, NULL } +}; + +const char *bactext_authentication_disable_reason_name(uint32_t index) +{ + /* Enumerated values 0-63 are reserved for definition by ASHRAE. + Enumerated values 64-65535 may be used by others subject to + the procedures and constraints described in Clause 23. */ + return indtext_by_index_split_default( + bactext_authentication_disable_reason_names, index, + AUTHENTICATION_DISABLED_PROPRIETARY_MIN, ASHRAE_Reserved_String, + Vendor_Proprietary_String); +} + +INDTEXT_DATA bactext_authentication_factor_type_names[] = { + /* BACnetAuthenticationFactorType enumerations */ + { AUTHENTICATION_FACTOR_UNDEFINED, "undefined" }, + { AUTHENTICATION_FACTOR_ERROR, "error" }, + { AUTHENTICATION_FACTOR_CUSTOM, "custom" }, + { AUTHENTICATION_FACTOR_SIMPLE_NUMBER16, "simple-number16" }, + { AUTHENTICATION_FACTOR_SIMPLE_NUMBER32, "simple-number32" }, + { AUTHENTICATION_FACTOR_SIMPLE_NUMBER56, "simple-number56" }, + { AUTHENTICATION_FACTOR_SIMPLE_ALPHA_NUMERIC, "simple-alpha-numeric" }, + { AUTHENTICATION_FACTOR_ABA_TRACK2, "aba-track2" }, + { AUTHENTICATION_FACTOR_WIEGAND26, "wiegand26" }, + { AUTHENTICATION_FACTOR_WIEGAND37, "wiegand37" }, + { AUTHENTICATION_FACTOR_WIEGAND37_FACILITY, "wiegand37-facility" }, + { AUTHENTICATION_FACTOR_FACILITY16_CARD32, "facility16-card32" }, + { AUTHENTICATION_FACTOR_FACILITY32_CARD32, "facility32-card32" }, + { AUTHENTICATION_FACTOR_FASC_N, "fasc-n" }, + { AUTHENTICATION_FACTOR_FASC_N_BCD, "fasc-n-bcd" }, + { AUTHENTICATION_FACTOR_FASC_N_LARGE, "fasc-n-large" }, + { AUTHENTICATION_FACTOR_FASC_N_LARGE_BCD, "fasc-n-large-bcd" }, + { AUTHENTICATION_FACTOR_GSA75, "gsa75" }, + { AUTHENTICATION_FACTOR_CHUID, "chuid" }, + { AUTHENTICATION_FACTOR_CHUID_FULL, "chuid-full" }, + { AUTHENTICATION_FACTOR_GUID, "guid" }, + { AUTHENTICATION_FACTOR_CBEFF_A, "cbeff-a" }, + { AUTHENTICATION_FACTOR_CBEFF_B, "cbeff-b" }, + { AUTHENTICATION_FACTOR_CBEFF_C, "cbeff-c" }, + { AUTHENTICATION_FACTOR_USER_PASSWORD, "user-password" }, + { 0, NULL } +}; + +const char *bactext_authentication_factor_type_name(uint32_t index) +{ + return indtext_by_index_default( + bactext_authentication_factor_type_names, index, + ASHRAE_Reserved_String); +} + INDTEXT_DATA bactext_authorization_mode_names[] = { /* BACnetAuthorizationMode enumerations */ { AUTHORIZATION_MODE_AUTHORIZE, "authorize" }, diff --git a/src/bacnet/bactext.h b/src/bacnet/bactext.h index db042f8e71..b49313fd89 100644 --- a/src/bacnet/bactext.h +++ b/src/bacnet/bactext.h @@ -385,6 +385,10 @@ bactext_access_event_name_default(uint32_t index, const char *default_string); BACNET_STACK_EXPORT const char *bactext_authentication_status_name(uint32_t index); +BACNET_STACK_EXPORT +const char *bactext_authentication_disable_reason_name(uint32_t index); +BACNET_STACK_EXPORT +const char *bactext_authentication_factor_type_name(uint32_t index); BACNET_STACK_EXPORT const char *bactext_authorization_mode_name(uint32_t index); diff --git a/src/bacnet/basic/service/h_rp_a.c b/src/bacnet/basic/service/h_rp_a.c index b5c2b1f3be..79f6e14617 100644 --- a/src/bacnet/basic/service/h_rp_a.c +++ b/src/bacnet/basic/service/h_rp_a.c @@ -131,6 +131,25 @@ void handler_read_property_ack( } } +/** + * @brief Free the memory allocated for a ReadProperty ACK service request. + * @param value [in] The head of the linked list of values to free. + * @param property [in] The property reference to free. + */ +static void rp_ack_service_request_free( + BACNET_APPLICATION_DATA_VALUE *value, BACNET_PROPERTY_REFERENCE *property) +{ + BACNET_APPLICATION_DATA_VALUE *old_value; + + while (value) { + /* free the linked list of values */ + old_value = value; + value = value->next; + free(old_value); + } + free(property); +} + /** Decode the received RP data into a linked list of the results, with the * same data structure used by RPM ACK replies. * This function is provided to provide common handling for RP and RPM data, @@ -149,7 +168,7 @@ int rp_ack_fully_decode_service_request( uint8_t *apdu, int apdu_len, BACNET_READ_ACCESS_DATA *read_access_data) { int decoded_len = 0; /* return value */ - BACNET_READ_PROPERTY_DATA rp1data; + BACNET_READ_PROPERTY_DATA rp1data = { 0 }; BACNET_PROPERTY_REFERENCE *rp1_property; /* single property */ BACNET_APPLICATION_DATA_VALUE *value, *old_value; uint8_t *vdata; @@ -170,33 +189,31 @@ int rp_ack_fully_decode_service_request( } rp1_property->propertyIdentifier = rp1data.object_property; rp1_property->propertyArrayIndex = rp1data.array_index; - /* Is there no Error case possible here, as there is when decoding RPM? - */ - /* rp1_property->error.error_class = ?? */ /* rp_ack_decode_service_request() processing already removed the - * Opening and Closing '3' Tags. - * note: if this is an array, there will be - more than one element to decode */ + * Opening and Closing '3' Tags. */ vdata = rp1data.application_data; vlen = rp1data.application_data_len; value = calloc(1, sizeof(BACNET_APPLICATION_DATA_VALUE)); + if (value == NULL) { + /* can't proceed if calloc failed. */ + rp_ack_service_request_free(NULL, rp1_property); + read_access_data->listOfProperties = NULL; + return BACNET_STATUS_ERROR; + } rp1_property->value = value; + /* check for empty list */ + if (rp1data.application_data_len == 0) { + bacapp_value_list_init(value, 1); + value->tag = BACNET_APPLICATION_TAG_EMPTYLIST; + return 0; + } while (value && vdata && (vlen > 0)) { - if (IS_CONTEXT_SPECIFIC(*vdata)) { - len = bacapp_decode_context_data( - vdata, vlen, value, rp1_property->propertyIdentifier); - } else { - len = bacapp_decode_application_data(vdata, vlen, value); - } + len = bacapp_decode_known_array_property( + vdata, (unsigned)vlen, value, rp1data.object_type, + rp1data.object_property, rp1data.array_index); if (len < 0) { /* unable to decode the data */ - while (value) { - /* free the linked list of values */ - old_value = value; - value = value->next; - free(old_value); - } - free(rp1_property); + rp_ack_service_request_free(value, rp1_property); read_access_data->listOfProperties = NULL; return len; } @@ -211,13 +228,7 @@ int rp_ack_fully_decode_service_request( } else { if (len == 0) { /* nothing decoded and no closing tag, so malformed */ - while (value) { - /* free the linked list of values */ - old_value = value; - value = value->next; - free(old_value); - } - free(rp1_property); + rp_ack_service_request_free(value, rp1_property); read_access_data->listOfProperties = NULL; return BACNET_STATUS_ERROR; } diff --git a/src/bacnet/config.h b/src/bacnet/config.h index 68c96e419f..b9f30b2408 100644 --- a/src/bacnet/config.h +++ b/src/bacnet/config.h @@ -241,6 +241,7 @@ defined(BACAPP_RECIPIENT) || \ defined(BACAPP_ADDRESS_BINDING) || \ defined(BACAPP_NO_VALUE) || \ + defined(BACAPP_AUTHENTICATION) || \ defined(BACAPP_LOG_RECORD) || \ defined(BACAPP_SECURE_CONNECT) || \ defined(BACAPP_TYPES_EXTRA)) @@ -335,6 +336,8 @@ #define BACAPP_ADDRESS_BINDING #undef BACAPP_NO_VALUE #define BACAPP_NO_VALUE +#undef BACAPP_AUTHENTICATION +#define BACAPP_AUTHENTICATION #undef BACAPP_LOG_RECORD #define BACAPP_LOG_RECORD #undef BACAPP_SECURE_CONNECT @@ -368,6 +371,7 @@ defined(BACAPP_RECIPIENT) || \ defined(BACAPP_ADDRESS_BINDING) || \ defined(BACAPP_NO_VALUE) || \ + defined(BACAPP_AUTHENTICATION) || \ defined(BACAPP_LOG_RECORD) #undef BACAPP_COMPLEX_TYPES #define BACAPP_COMPLEX_TYPES diff --git a/test/bacnet/access_rule/CMakeLists.txt b/test/bacnet/access_rule/CMakeLists.txt index 08b3a23ed7..eaed888fd3 100644 --- a/test/bacnet/access_rule/CMakeLists.txt +++ b/test/bacnet/access_rule/CMakeLists.txt @@ -39,6 +39,8 @@ add_executable(${PROJECT_NAME} # File(s) under test ${SRC_DIR}/bacnet/access_rule.c # Support files and stubs (pathname alphabetical) + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/authentication_factor_format/CMakeLists.txt b/test/bacnet/authentication_factor_format/CMakeLists.txt index dcac5885b7..9cec216fc8 100644 --- a/test/bacnet/authentication_factor_format/CMakeLists.txt +++ b/test/bacnet/authentication_factor_format/CMakeLists.txt @@ -34,6 +34,7 @@ include_directories( add_executable(${PROJECT_NAME} # File(s) under test + ${SRC_DIR}/bacnet/authentication_factor.c ${SRC_DIR}/bacnet/authentication_factor_format.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/bacaction.c diff --git a/test/bacnet/bacapp/CMakeLists.txt b/test/bacnet/bacapp/CMakeLists.txt index 06c6a32b75..f2294dfe8e 100644 --- a/test/bacnet/bacapp/CMakeLists.txt +++ b/test/bacnet/bacapp/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/bacapp.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacdest.c diff --git a/test/bacnet/bacaudit/CMakeLists.txt b/test/bacnet/bacaudit/CMakeLists.txt index 1de8dd1476..b06f6fad1d 100644 --- a/test/bacnet/bacaudit/CMakeLists.txt +++ b/test/bacnet/bacaudit/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/bacaudit.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/bacdest/CMakeLists.txt b/test/bacnet/bacdest/CMakeLists.txt index 49dd81e2fb..1c242bfa65 100644 --- a/test/bacnet/bacdest/CMakeLists.txt +++ b/test/bacnet/bacdest/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/bacdest.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/bacdevobjpropref/CMakeLists.txt b/test/bacnet/bacdevobjpropref/CMakeLists.txt index 970e61fa5a..a211b51f44 100644 --- a/test/bacnet/bacdevobjpropref/CMakeLists.txt +++ b/test/bacnet/bacdevobjpropref/CMakeLists.txt @@ -40,6 +40,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/bacdevobjpropref.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/baclog/CMakeLists.txt b/test/bacnet/baclog/CMakeLists.txt index 9ed3188e96..2876d3236f 100644 --- a/test/bacnet/baclog/CMakeLists.txt +++ b/test/bacnet/baclog/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/baclog.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/bactimevalue/CMakeLists.txt b/test/bacnet/bactimevalue/CMakeLists.txt index a587053aba..caa90f9bea 100644 --- a/test/bacnet/bactimevalue/CMakeLists.txt +++ b/test/bacnet/bactimevalue/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/bactimevalue.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/binding/address/CMakeLists.txt b/test/bacnet/basic/binding/address/CMakeLists.txt index b5ead17b6a..a0ab460861 100644 --- a/test/bacnet/basic/binding/address/CMakeLists.txt +++ b/test/bacnet/basic/binding/address/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/binding/address.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/acc/CMakeLists.txt b/test/bacnet/basic/object/acc/CMakeLists.txt index b525a3a564..7b0c15cb61 100644 --- a/test/bacnet/basic/object/acc/CMakeLists.txt +++ b/test/bacnet/basic/object/acc/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/acc.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/access_credential/CMakeLists.txt b/test/bacnet/basic/object/access_credential/CMakeLists.txt index 2d98d50b91..a291457e36 100644 --- a/test/bacnet/basic/object/access_credential/CMakeLists.txt +++ b/test/bacnet/basic/object/access_credential/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/access_rule.c ${SRC_DIR}/bacnet/assigned_access_rights.c ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/access_door/CMakeLists.txt b/test/bacnet/basic/object/access_door/CMakeLists.txt index fcdc5895ec..bba0e6e628 100644 --- a/test/bacnet/basic/object/access_door/CMakeLists.txt +++ b/test/bacnet/basic/object/access_door/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/access_door.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/access_point/CMakeLists.txt b/test/bacnet/basic/object/access_point/CMakeLists.txt index e3ede1f8cf..e4aa3d4bab 100644 --- a/test/bacnet/basic/object/access_point/CMakeLists.txt +++ b/test/bacnet/basic/object/access_point/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/access_point.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/access_rights/CMakeLists.txt b/test/bacnet/basic/object/access_rights/CMakeLists.txt index 3f8f47d143..0b67ceecc0 100644 --- a/test/bacnet/basic/object/access_rights/CMakeLists.txt +++ b/test/bacnet/basic/object/access_rights/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/access_rights.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/access_user/CMakeLists.txt b/test/bacnet/basic/object/access_user/CMakeLists.txt index d17bd469c3..cfb9672915 100644 --- a/test/bacnet/basic/object/access_user/CMakeLists.txt +++ b/test/bacnet/basic/object/access_user/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/access_user.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/access_zone/CMakeLists.txt b/test/bacnet/basic/object/access_zone/CMakeLists.txt index e7b34f8a51..2045565468 100644 --- a/test/bacnet/basic/object/access_zone/CMakeLists.txt +++ b/test/bacnet/basic/object/access_zone/CMakeLists.txt @@ -36,6 +36,7 @@ add_executable(${PROJECT_NAME} # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/assigned_access_rights.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c diff --git a/test/bacnet/basic/object/ai/CMakeLists.txt b/test/bacnet/basic/object/ai/CMakeLists.txt index e35d369fb0..c493dbb839 100644 --- a/test/bacnet/basic/object/ai/CMakeLists.txt +++ b/test/bacnet/basic/object/ai/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/ai.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacdcode.c diff --git a/test/bacnet/basic/object/ao/CMakeLists.txt b/test/bacnet/basic/object/ao/CMakeLists.txt index 2ca1ca805d..65cd5688d3 100644 --- a/test/bacnet/basic/object/ao/CMakeLists.txt +++ b/test/bacnet/basic/object/ao/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/ao.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/auditlog/CMakeLists.txt b/test/bacnet/basic/object/auditlog/CMakeLists.txt index ad4d416b65..5c322c0c72 100644 --- a/test/bacnet/basic/object/auditlog/CMakeLists.txt +++ b/test/bacnet/basic/object/auditlog/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(${PROJECT_NAME} # File(s) under test ${SRC_DIR}/bacnet/basic/object/auditlog.c # Support files and stubs (pathname alphabetical) + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacaudit.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/av/CMakeLists.txt b/test/bacnet/basic/object/av/CMakeLists.txt index 7381025685..b40d592c80 100644 --- a/test/bacnet/basic/object/av/CMakeLists.txt +++ b/test/bacnet/basic/object/av/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/av.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/bacfile/CMakeLists.txt b/test/bacnet/basic/object/bacfile/CMakeLists.txt index a0cb46fa8b..5756676333 100644 --- a/test/bacnet/basic/object/bacfile/CMakeLists.txt +++ b/test/bacnet/basic/object/bacfile/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/bacfile.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/arf.c ${SRC_DIR}/bacnet/awf.c ${SRC_DIR}/bacnet/bacaction.c diff --git a/test/bacnet/basic/object/bi/CMakeLists.txt b/test/bacnet/basic/object/bi/CMakeLists.txt index 8c68de541d..f3df5c1fe1 100644 --- a/test/bacnet/basic/object/bi/CMakeLists.txt +++ b/test/bacnet/basic/object/bi/CMakeLists.txt @@ -38,6 +38,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/bi.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/bitstring_value/CMakeLists.txt b/test/bacnet/basic/object/bitstring_value/CMakeLists.txt index b9ee0c6741..70d5ee5c35 100644 --- a/test/bacnet/basic/object/bitstring_value/CMakeLists.txt +++ b/test/bacnet/basic/object/bitstring_value/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/bitstring_value.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/blo/CMakeLists.txt b/test/bacnet/basic/object/blo/CMakeLists.txt index b61d5c60c6..9b94757a54 100644 --- a/test/bacnet/basic/object/blo/CMakeLists.txt +++ b/test/bacnet/basic/object/blo/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/blo.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/bo/CMakeLists.txt b/test/bacnet/basic/object/bo/CMakeLists.txt index aaef3f3d8a..e5766ace14 100644 --- a/test/bacnet/basic/object/bo/CMakeLists.txt +++ b/test/bacnet/basic/object/bo/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/bo.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/bv/CMakeLists.txt b/test/bacnet/basic/object/bv/CMakeLists.txt index d56237f292..92cce711f0 100644 --- a/test/bacnet/basic/object/bv/CMakeLists.txt +++ b/test/bacnet/basic/object/bv/CMakeLists.txt @@ -38,6 +38,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/bv.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/calendar/CMakeLists.txt b/test/bacnet/basic/object/calendar/CMakeLists.txt index 61bafdc2df..815ef1ebad 100644 --- a/test/bacnet/basic/object/calendar/CMakeLists.txt +++ b/test/bacnet/basic/object/calendar/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/calendar.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/channel/CMakeLists.txt b/test/bacnet/basic/object/channel/CMakeLists.txt index aab763414e..85d96d3ed2 100644 --- a/test/bacnet/basic/object/channel/CMakeLists.txt +++ b/test/bacnet/basic/object/channel/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/channel.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/color_object/CMakeLists.txt b/test/bacnet/basic/object/color_object/CMakeLists.txt index f193fbda4f..c6c002e3a5 100644 --- a/test/bacnet/basic/object/color_object/CMakeLists.txt +++ b/test/bacnet/basic/object/color_object/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/color_object.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/color_temperature/CMakeLists.txt b/test/bacnet/basic/object/color_temperature/CMakeLists.txt index 814ef64f34..3048f44095 100644 --- a/test/bacnet/basic/object/color_temperature/CMakeLists.txt +++ b/test/bacnet/basic/object/color_temperature/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/color_temperature.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/command/CMakeLists.txt b/test/bacnet/basic/object/command/CMakeLists.txt index 452f3b4f41..812d01217c 100644 --- a/test/bacnet/basic/object/command/CMakeLists.txt +++ b/test/bacnet/basic/object/command/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/command.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/csv/CMakeLists.txt b/test/bacnet/basic/object/csv/CMakeLists.txt index d846b85ec8..a8ffc65112 100644 --- a/test/bacnet/basic/object/csv/CMakeLists.txt +++ b/test/bacnet/basic/object/csv/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/csv.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/iv/CMakeLists.txt b/test/bacnet/basic/object/iv/CMakeLists.txt index 496dc26b71..721c748025 100644 --- a/test/bacnet/basic/object/iv/CMakeLists.txt +++ b/test/bacnet/basic/object/iv/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/iv.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/lc/CMakeLists.txt b/test/bacnet/basic/object/lc/CMakeLists.txt index 9ed34ebc3e..a3f71c3349 100644 --- a/test/bacnet/basic/object/lc/CMakeLists.txt +++ b/test/bacnet/basic/object/lc/CMakeLists.txt @@ -39,6 +39,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/lc.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/lo/CMakeLists.txt b/test/bacnet/basic/object/lo/CMakeLists.txt index 7eb0d44915..f24832682a 100644 --- a/test/bacnet/basic/object/lo/CMakeLists.txt +++ b/test/bacnet/basic/object/lo/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/lo.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/loop/CMakeLists.txt b/test/bacnet/basic/object/loop/CMakeLists.txt index 1f1a5ebb90..0559c7dbf1 100644 --- a/test/bacnet/basic/object/loop/CMakeLists.txt +++ b/test/bacnet/basic/object/loop/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/loop.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/lsp/CMakeLists.txt b/test/bacnet/basic/object/lsp/CMakeLists.txt index a73ed32cc4..4aff54475e 100644 --- a/test/bacnet/basic/object/lsp/CMakeLists.txt +++ b/test/bacnet/basic/object/lsp/CMakeLists.txt @@ -34,6 +34,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/lsp.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/lsz/CMakeLists.txt b/test/bacnet/basic/object/lsz/CMakeLists.txt index c5f6be9ce1..6a4e530ef7 100644 --- a/test/bacnet/basic/object/lsz/CMakeLists.txt +++ b/test/bacnet/basic/object/lsz/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/lsz.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/ms-input/CMakeLists.txt b/test/bacnet/basic/object/ms-input/CMakeLists.txt index e7f1c9b13d..04ab1f702b 100644 --- a/test/bacnet/basic/object/ms-input/CMakeLists.txt +++ b/test/bacnet/basic/object/ms-input/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/ms-input.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/mso/CMakeLists.txt b/test/bacnet/basic/object/mso/CMakeLists.txt index 7f3c4f1123..1e220b2d4f 100644 --- a/test/bacnet/basic/object/mso/CMakeLists.txt +++ b/test/bacnet/basic/object/mso/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/mso.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/msv/CMakeLists.txt b/test/bacnet/basic/object/msv/CMakeLists.txt index 2376b145ad..533f7421f8 100644 --- a/test/bacnet/basic/object/msv/CMakeLists.txt +++ b/test/bacnet/basic/object/msv/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/msv.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/nc/CMakeLists.txt b/test/bacnet/basic/object/nc/CMakeLists.txt index b23614b203..0a8a6c9381 100644 --- a/test/bacnet/basic/object/nc/CMakeLists.txt +++ b/test/bacnet/basic/object/nc/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/nc.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/netport/CMakeLists.txt b/test/bacnet/basic/object/netport/CMakeLists.txt index 966523cbda..46a8981588 100644 --- a/test/bacnet/basic/object/netport/CMakeLists.txt +++ b/test/bacnet/basic/object/netport/CMakeLists.txt @@ -52,6 +52,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/sc_netport.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/arf.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c diff --git a/test/bacnet/basic/object/osv/CMakeLists.txt b/test/bacnet/basic/object/osv/CMakeLists.txt index 0bf8750242..4c52d410f2 100644 --- a/test/bacnet/basic/object/osv/CMakeLists.txt +++ b/test/bacnet/basic/object/osv/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/osv.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/piv/CMakeLists.txt b/test/bacnet/basic/object/piv/CMakeLists.txt index 8883b236b6..5621cf810f 100644 --- a/test/bacnet/basic/object/piv/CMakeLists.txt +++ b/test/bacnet/basic/object/piv/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/piv.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/program/CMakeLists.txt b/test/bacnet/basic/object/program/CMakeLists.txt index 4a369f0981..a90ff65196 100644 --- a/test/bacnet/basic/object/program/CMakeLists.txt +++ b/test/bacnet/basic/object/program/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/program.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/schedule/CMakeLists.txt b/test/bacnet/basic/object/schedule/CMakeLists.txt index 55220daa93..10d0c04efc 100644 --- a/test/bacnet/basic/object/schedule/CMakeLists.txt +++ b/test/bacnet/basic/object/schedule/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/schedule.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/structured_view/CMakeLists.txt b/test/bacnet/basic/object/structured_view/CMakeLists.txt index 4475ab5b67..f220d4b449 100644 --- a/test/bacnet/basic/object/structured_view/CMakeLists.txt +++ b/test/bacnet/basic/object/structured_view/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/structured_view.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/time_value/CMakeLists.txt b/test/bacnet/basic/object/time_value/CMakeLists.txt index 5d85128a0a..05871ca7d8 100644 --- a/test/bacnet/basic/object/time_value/CMakeLists.txt +++ b/test/bacnet/basic/object/time_value/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/time_value.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/timer/CMakeLists.txt b/test/bacnet/basic/object/timer/CMakeLists.txt index 7b1a88e9b3..8a2161759a 100644 --- a/test/bacnet/basic/object/timer/CMakeLists.txt +++ b/test/bacnet/basic/object/timer/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/timer.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/object/trendlog/CMakeLists.txt b/test/bacnet/basic/object/trendlog/CMakeLists.txt index b194fee6d7..1ac6fd8798 100644 --- a/test/bacnet/basic/object/trendlog/CMakeLists.txt +++ b/test/bacnet/basic/object/trendlog/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/object/trendlog.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/basic/server/bacnet_device/CMakeLists.txt b/test/bacnet/basic/server/bacnet_device/CMakeLists.txt index 097ad1c66d..9222515b3e 100644 --- a/test/bacnet/basic/server/bacnet_device/CMakeLists.txt +++ b/test/bacnet/basic/server/bacnet_device/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(${PROJECT_NAME} # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/abort.c ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/arf.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c diff --git a/test/bacnet/cov/CMakeLists.txt b/test/bacnet/cov/CMakeLists.txt index 9502f3c3fc..717d2c888c 100644 --- a/test/bacnet/cov/CMakeLists.txt +++ b/test/bacnet/cov/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/cov.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/create_object/CMakeLists.txt b/test/bacnet/create_object/CMakeLists.txt index 74db0a3661..53eae23481 100644 --- a/test/bacnet/create_object/CMakeLists.txt +++ b/test/bacnet/create_object/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/create_object.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/datalink/bsc-datalink/CMakeLists.txt b/test/bacnet/datalink/bsc-datalink/CMakeLists.txt index 20734357f0..b0e8a7dcd6 100644 --- a/test/bacnet/datalink/bsc-datalink/CMakeLists.txt +++ b/test/bacnet/datalink/bsc-datalink/CMakeLists.txt @@ -171,6 +171,8 @@ target_sources(${PROJECT_NAME} PRIVATE ${SRC_DIR}/bacnet/basic/sys/keylist.c ${SRC_DIR}/bacnet/basic/sys/mstimer.c ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/arf.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/datalink/bsc-node/CMakeLists.txt b/test/bacnet/datalink/bsc-node/CMakeLists.txt index 7828dcc910..5d2d40de5f 100644 --- a/test/bacnet/datalink/bsc-node/CMakeLists.txt +++ b/test/bacnet/datalink/bsc-node/CMakeLists.txt @@ -160,6 +160,8 @@ target_sources(${PROJECT_NAME} PRIVATE ${SRC_DIR}/bacnet/basic/sys/keylist.c ${SRC_DIR}/bacnet/basic/sys/mstimer.c ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/arf.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/datalink/bsc-socket/CMakeLists.txt b/test/bacnet/datalink/bsc-socket/CMakeLists.txt index b14acf2c10..f9f7c3a521 100644 --- a/test/bacnet/datalink/bsc-socket/CMakeLists.txt +++ b/test/bacnet/datalink/bsc-socket/CMakeLists.txt @@ -158,6 +158,8 @@ target_sources(${PROJECT_NAME} PRIVATE ${SRC_DIR}/bacnet/basic/sys/keylist.c ${SRC_DIR}/bacnet/basic/sys/mstimer.c ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/arf.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/datalink/hub-sc/CMakeLists.txt b/test/bacnet/datalink/hub-sc/CMakeLists.txt index 162cd41168..c20ccd1e4a 100644 --- a/test/bacnet/datalink/hub-sc/CMakeLists.txt +++ b/test/bacnet/datalink/hub-sc/CMakeLists.txt @@ -163,6 +163,8 @@ target_sources(${PROJECT_NAME} PRIVATE ${SRC_DIR}/bacnet/basic/sys/keylist.c ${SRC_DIR}/bacnet/basic/sys/mstimer.c ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/arf.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/delete_object/CMakeLists.txt b/test/bacnet/delete_object/CMakeLists.txt index 6748043fad..7e298f3ec3 100644 --- a/test/bacnet/delete_object/CMakeLists.txt +++ b/test/bacnet/delete_object/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/delete_object.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/event/CMakeLists.txt b/test/bacnet/event/CMakeLists.txt index da50fac21a..1b402d2f65 100644 --- a/test/bacnet/event/CMakeLists.txt +++ b/test/bacnet/event/CMakeLists.txt @@ -60,6 +60,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/basic/sys/days.c # Dependencies of bacapp.c ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bactext.c ${SRC_DIR}/bacnet/indtext.c diff --git a/test/bacnet/getalarm/CMakeLists.txt b/test/bacnet/getalarm/CMakeLists.txt index 45fe4a84e5..666ce6b67f 100644 --- a/test/bacnet/getalarm/CMakeLists.txt +++ b/test/bacnet/getalarm/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/get_alarm_sum.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/getevent/CMakeLists.txt b/test/bacnet/getevent/CMakeLists.txt index 820eb21e13..6992c40192 100644 --- a/test/bacnet/getevent/CMakeLists.txt +++ b/test/bacnet/getevent/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/getevent.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/hostnport/CMakeLists.txt b/test/bacnet/hostnport/CMakeLists.txt index 3b8ad1a044..e9dd887a51 100644 --- a/test/bacnet/hostnport/CMakeLists.txt +++ b/test/bacnet/hostnport/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/hostnport.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/list_element/CMakeLists.txt b/test/bacnet/list_element/CMakeLists.txt index 7cb9d445a2..b8450a4497 100644 --- a/test/bacnet/list_element/CMakeLists.txt +++ b/test/bacnet/list_element/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/list_element.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/lso/CMakeLists.txt b/test/bacnet/lso/CMakeLists.txt index a189709c5c..bc26bd66fa 100644 --- a/test/bacnet/lso/CMakeLists.txt +++ b/test/bacnet/lso/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/lso.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/ptransfer/CMakeLists.txt b/test/bacnet/ptransfer/CMakeLists.txt index 754d772727..901016ca83 100644 --- a/test/bacnet/ptransfer/CMakeLists.txt +++ b/test/bacnet/ptransfer/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/ptransfer.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/rpm/CMakeLists.txt b/test/bacnet/rpm/CMakeLists.txt index 67d5dbde95..ce8786cf6f 100644 --- a/test/bacnet/rpm/CMakeLists.txt +++ b/test/bacnet/rpm/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/rpm.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/secure_connect/CMakeLists.txt b/test/bacnet/secure_connect/CMakeLists.txt index e90b4acc74..af055d7f74 100644 --- a/test/bacnet/secure_connect/CMakeLists.txt +++ b/test/bacnet/secure_connect/CMakeLists.txt @@ -34,6 +34,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/secure_connect.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/specialevent/CMakeLists.txt b/test/bacnet/specialevent/CMakeLists.txt index 8197178164..e8106ceba8 100644 --- a/test/bacnet/specialevent/CMakeLists.txt +++ b/test/bacnet/specialevent/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/special_event.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/timesync/CMakeLists.txt b/test/bacnet/timesync/CMakeLists.txt index 7d937c6094..3ca09bb5b1 100644 --- a/test/bacnet/timesync/CMakeLists.txt +++ b/test/bacnet/timesync/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/timesync.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/weeklyschedule/CMakeLists.txt b/test/bacnet/weeklyschedule/CMakeLists.txt index 46a62567e6..aa36fbf1bd 100644 --- a/test/bacnet/weeklyschedule/CMakeLists.txt +++ b/test/bacnet/weeklyschedule/CMakeLists.txt @@ -38,6 +38,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/dailyschedule.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/wp/CMakeLists.txt b/test/bacnet/wp/CMakeLists.txt index 753edfb61f..76421ef081 100644 --- a/test/bacnet/wp/CMakeLists.txt +++ b/test/bacnet/wp/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/wp.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/wpm/CMakeLists.txt b/test/bacnet/wpm/CMakeLists.txt index 8e8afa9908..6f4de920a6 100644 --- a/test/bacnet/wpm/CMakeLists.txt +++ b/test/bacnet/wpm/CMakeLists.txt @@ -36,6 +36,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/wpm.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c diff --git a/test/bacnet/write_group/CMakeLists.txt b/test/bacnet/write_group/CMakeLists.txt index adf94ba8f2..7447b8357e 100644 --- a/test/bacnet/write_group/CMakeLists.txt +++ b/test/bacnet/write_group/CMakeLists.txt @@ -35,6 +35,8 @@ add_executable(${PROJECT_NAME} ${SRC_DIR}/bacnet/write_group.c # Support files and stubs (pathname alphabetical) ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c ${SRC_DIR}/bacnet/bacaction.c ${SRC_DIR}/bacnet/bacaddr.c ${SRC_DIR}/bacnet/bacapp.c From 5e3a7641c63d11d072c124f916ec222f387c081f Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Sat, 25 Apr 2026 10:10:30 -0500 Subject: [PATCH 08/42] * Fixed lighting command update notifications to use scaled physical values using min/max actual value. (#1315) * Fixed lighting command off to off behavior. (#1314) * Fixed lighting command refresh logic in trim set functions. (#1313) * Fixed coupling between lighting output object and lighting command structure by using locking callbacks in lighting command that are engaged when accessing any of the lighting command structure data. (#1306) --- CHANGELOG.md | 7 + src/bacnet/basic/object/lo.c | 356 +++-- src/bacnet/basic/object/lo.h | 15 + src/bacnet/basic/sys/lighting_command.c | 1178 ++++++++++++----- src/bacnet/basic/sys/lighting_command.h | 112 +- test/bacnet/basic/object/lo/src/main.c | 135 +- .../basic/sys/lighting_command/src/main.c | 26 + 7 files changed, 1426 insertions(+), 403 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cd4c8937b..ed35dcc6b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,8 +24,15 @@ The git repositories are hosted at the following sites: ### Fixed +* Fixed lighting command update notifications to use scaled physical + values using min/max actual value. (#1315) +* Fixed lighting command off to off behavior. (#1314) +* Fixed lighting command refresh logic in trim set functions. (#1313) * Fixed EPICS values for recipient list, empty lists, and authentication factors. Fixed EPICS app to allow target MAC or IP address format. (#1310) +* Fixed coupling between lighting output object and lighting command structure + by using locking callbacks in lighting command that are engaged when accessing + any of the lighting command structure data. (#1306) * Fixed BBMD_Result handling to avoid false positive error message when no registration is requested. (#1305) * Fixed Keylist memory allocation check in CheckArraySize and handle diff --git a/src/bacnet/basic/object/lo.c b/src/bacnet/basic/object/lo.c index f22df6223b..488e27256f 100644 --- a/src/bacnet/basic/object/lo.c +++ b/src/bacnet/basic/object/lo.c @@ -106,6 +106,8 @@ static const int32_t Properties_Optional[] = { /* unordered list of optional properties */ PROP_DESCRIPTION, PROP_TRANSITION, + PROP_MIN_ACTUAL_VALUE, + PROP_MAX_ACTUAL_VALUE, #if (BACNET_PROTOCOL_REVISION >= 24) PROP_COLOR_OVERRIDE, PROP_COLOR_REFERENCE, @@ -139,7 +141,8 @@ static const int32_t Writable_Properties[] = { PROP_TRIM_FADE_TIME, PROP_BLINK_WARN_ENABLE, PROP_EGRESS_TIME, PROP_LIGHTING_COMMAND_DEFAULT_PRIORITY, PROP_FEEDBACK_VALUE, PROP_POWER, - PROP_INSTANTANEOUS_POWER, -1 + PROP_INSTANTANEOUS_POWER, PROP_MIN_ACTUAL_VALUE, + PROP_MAX_ACTUAL_VALUE, -1 }; /** @@ -507,29 +510,6 @@ unsigned Lighting_Output_Present_Value_Priority(uint32_t object_instance) return priority; } -/** - * @brief Determine if fade, ramp, or warn command is currently executing - * @param pObject [in] object to apply the trim values to - * @param priority [in] priority of the command - */ -static bool Lighting_Command_In_Progress(struct object_data *pObject) -{ - bool in_progress = false; - - if (!pObject) { - return in_progress; - } - if ((pObject->Lighting_Command.In_Progress == - BACNET_LIGHTING_FADE_ACTIVE) || - (pObject->Lighting_Command.In_Progress == - BACNET_LIGHTING_RAMP_ACTIVE) || - (pObject->Lighting_Command.Blink.Duration > 0)) { - in_progress = true; - } - - return in_progress; -} - /** * @brief Configure the lighting command to apply low or high trim * to the tracking value based on the priority of the command @@ -547,14 +527,12 @@ Lighting_Command_Trim_Apply(struct object_data *pObject, unsigned priority) the Tracking_Value shall not be clamped. */ if ((priority == 1) || (priority == 2)) { /* remove any high or low trim */ - pObject->Lighting_Command.High_Trim_Value = 100.0f; - pObject->Lighting_Command.Low_Trim_Value = 1.0f; - pObject->Lighting_Command.Trim_Fade_Time = 0; + lighting_command_trim_set(&pObject->Lighting_Command, 100.0f, 1.0f, 0); } else { /* apply high and low trim */ - pObject->Lighting_Command.High_Trim_Value = pObject->High_End_Trim; - pObject->Lighting_Command.Low_Trim_Value = pObject->Low_End_Trim; - pObject->Lighting_Command.Trim_Fade_Time = pObject->Trim_Fade_Time; + lighting_command_trim_set( + &pObject->Lighting_Command, pObject->High_End_Trim, + pObject->Low_End_Trim, pObject->Trim_Fade_Time); } } @@ -653,6 +631,7 @@ static void Lighting_Command_Warn(struct object_data *pObject, unsigned priority) { unsigned current_priority; + BACNET_LIGHTING_COMMAND_WARN_DATA blink = { 0 }; if (!pObject) { return; @@ -669,9 +648,9 @@ Lighting_Command_Warn(struct object_data *pObject, unsigned priority) active priority, or (b) The value at the specified priority is 0.0%, or (c) Blink_Warn_Enable is FALSE. */ + lighting_command_blink_copy(&pObject->Lighting_Command, &blink); lighting_command_blink_warn( - &pObject->Lighting_Command, BACNET_LIGHTS_WARN, - &pObject->Lighting_Command.Blink); + &pObject->Lighting_Command, BACNET_LIGHTS_WARN, &blink); } } @@ -720,7 +699,7 @@ static void Lighting_Command_Warn_Off(struct object_data *pObject, unsigned priority) { unsigned current_priority; - BACNET_LIGHTING_COMMAND_WARN_DATA blink; + BACNET_LIGHTING_COMMAND_WARN_DATA blink = { 0 }; if (!pObject) { return; @@ -740,9 +719,7 @@ Lighting_Command_Warn_Off(struct object_data *pObject, unsigned priority) active priority, or (b) The Present_Value is 0.0%, or (c) Blink_Warn_Enable is FALSE. */ - memmove( - &blink, &pObject->Lighting_Command.Blink, - sizeof(BACNET_LIGHTING_COMMAND_WARN_DATA)); + lighting_command_blink_copy(&pObject->Lighting_Command, &blink); blink.Duration = pObject->Egress_Time_Seconds * 1000UL; blink.Priority = priority; blink.Callback = Lighting_Command_Blink_Stop; @@ -813,9 +790,7 @@ Lighting_Command_Warn_Relinquish(struct object_data *pObject, unsigned priority) (b) The Present_Value is 0.0%, or (c) The Present_Value would not evaluate to 0.0% after the priority slot is relinquished. */ - memmove( - &blink, &pObject->Lighting_Command.Blink, - sizeof(BACNET_LIGHTING_COMMAND_WARN_DATA)); + lighting_command_blink_copy(&pObject->Lighting_Command, &blink); blink.Duration = pObject->Egress_Time_Seconds * 1000UL; blink.Priority = priority; blink.Callback = Lighting_Command_Blink_Stop; @@ -860,7 +835,7 @@ static void Lighting_Command_Step_Up_On( if (!pObject) { return; } - value = pObject->Lighting_Command.Tracking_Value; + value = lighting_command_tracking_value_get(&pObject->Lighting_Command); if (operation == BACNET_LIGHTS_STEP_UP) { if (is_float_equal(value, 0.0)) { /* If the starting level of Tracking_Value is 0.0%, @@ -914,7 +889,7 @@ static void Lighting_Command_Step_Down_Off( if (!pObject) { return; } - value = pObject->Lighting_Command.Tracking_Value; + value = lighting_command_tracking_value_get(&pObject->Lighting_Command); if (is_float_equal(value, 0.0)) { /* If the starting level of Tracking_Value is 0.0%, then this operation is ignored. */ @@ -959,7 +934,7 @@ Lighting_Command_Restore_On(struct object_data *pObject, unsigned priority) if (!pObject) { return; } - value = pObject->Lighting_Command.Last_On_Value; + value = lighting_command_last_on_value_get(&pObject->Lighting_Command); Lighting_Command_Transition_Default(pObject, priority, value); } @@ -978,7 +953,7 @@ Lighting_Command_Default_On(struct object_data *pObject, unsigned priority) if (!pObject) { return; } - value = pObject->Lighting_Command.Default_On_Value; + value = lighting_command_default_on_value_get(&pObject->Lighting_Command); Lighting_Command_Transition_Default(pObject, priority, value); } @@ -1001,7 +976,8 @@ Lighting_Command_Toggle_Restore(struct object_data *pObject, unsigned priority) /* Prior to the execution of this command, if Present_Value is 0.0%, write the Last_On_Value to the specified slot in the priority array. */ - toggle_value = pObject->Lighting_Command.Last_On_Value; + toggle_value = + lighting_command_last_on_value_get(&pObject->Lighting_Command); } else { /* Prior to the execution of this command, if Present_Value is not 0.0%, write 0.0% to the specified slot in the priority array. */ @@ -1029,7 +1005,8 @@ Lighting_Command_Toggle_Default(struct object_data *pObject, unsigned priority) /* Prior to the execution of this command, if Present_Value is 0.0%, write the Default_On_Value to the specified slot in the priority array. */ - toggle_value = pObject->Lighting_Command.Default_On_Value; + toggle_value = + lighting_command_default_on_value_get(&pObject->Lighting_Command); } else { /* Prior to the execution of this command, if Present_Value is not 0.0%, write 0.0% to the specified slot in the priority array. */ @@ -1467,10 +1444,11 @@ Lighting_Command_Stop(struct object_data *pObject, unsigned priority) } current_priority = Present_Value_Priority(pObject); if (priority == current_priority) { - if (Lighting_Command_In_Progress(pObject)) { + if (lighting_command_active(&pObject->Lighting_Command)) { /* fade, ramp, or warn command is currently executing at the specified priority */ - value = pObject->Lighting_Command.Tracking_Value; + value = + lighting_command_tracking_value_get(&pObject->Lighting_Command); Present_Value_Set(pObject, value, priority); /* configure the Lighting Command */ lighting_command_stop(&pObject->Lighting_Command); @@ -1762,7 +1740,7 @@ Lighting_Output_In_Progress(uint32_t object_instance) pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - value = pObject->Lighting_Command.In_Progress; + value = lighting_command_in_progress_get(&pObject->Lighting_Command); } return value; @@ -1785,7 +1763,8 @@ bool Lighting_Output_In_Progress_Set( pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - pObject->Lighting_Command.In_Progress = in_progress; + lighting_command_in_progress_set( + &pObject->Lighting_Command, in_progress); } return status; @@ -1805,7 +1784,7 @@ float Lighting_Output_Tracking_Value(uint32_t object_instance) pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - value = pObject->Lighting_Command.Tracking_Value; + value = lighting_command_tracking_value_get(&pObject->Lighting_Command); } return value; @@ -1827,7 +1806,7 @@ bool Lighting_Output_Tracking_Value_Set(uint32_t object_instance, float value) pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - pObject->Lighting_Command.Tracking_Value = value; + lighting_command_tracking_value_set(&pObject->Lighting_Command, value); status = true; } @@ -1916,9 +1895,8 @@ bool Lighting_Output_Blink_Warn_Feature_Set( } else if (isgreater(off_value, 100.0)) { off_value = 100.0f; } - pObject->Lighting_Command.Blink.Off_Value = off_value; - pObject->Lighting_Command.Blink.Interval = interval; - pObject->Lighting_Command.Blink.Count = count; + lighting_command_blink_warn_feature_set( + &pObject->Lighting_Command, off_value, interval, count); status = true; } @@ -2023,9 +2001,8 @@ bool Lighting_Output_Egress_Active(uint32_t object_instance) pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - if (pObject->Lighting_Command.Blink.Duration > 0) { - value = true; - } + value = + lighting_command_blink_egress_active(&pObject->Lighting_Command); } return value; @@ -2315,6 +2292,176 @@ unsigned Lighting_Output_Default_Priority(uint32_t object_instance) return value; } +/** + * @brief For a given object instance-number, gets the minimum-actual-value + * property value + * @param object_instance - object-instance number of the object + * @return the minimum-actual-value property value of this object + */ +float Lighting_Output_Min_Actual_Value(uint32_t object_instance) +{ + float value = 0.0; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = + lighting_command_min_actual_value_get(&pObject->Lighting_Command); + } + + return value; +} + +/** + * @brief For a given object instance-number, sets the minimum-actual-value + * property value + * @param object_instance - object-instance number of the object + * @param value - the minimum-actual-value property value to be set + * @return true if the minimum-actual-value property value was set + */ +bool Lighting_Output_Min_Actual_Value_Set(uint32_t object_instance, float value) +{ + bool status = false; + struct object_data *pObject; + float max_actual_value; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if (isgreaterequal(value, 1.0f) && islessequal(value, 100.0f)) { + /* Min_Actual_Value shall always be a positive number + in the range 1.0% to 100.0%.*/ + max_actual_value = lighting_command_max_actual_value_get( + &pObject->Lighting_Command); + if (value > max_actual_value) { + /* Changing Min_Actual_Value to a value greater than + Max_Actual_Value shall force Max_Actual_Value + to become equal to Min_Actual_Value. */ + value = max_actual_value; + } + lighting_command_min_actual_value_set( + &pObject->Lighting_Command, value); + status = true; + } + } + + return status; +} + +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Lighting_Output_Min_Actual_Value_Write( + uint32_t object_instance, + float value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + (void)priority; + status = Lighting_Output_Min_Actual_Value_Set(object_instance, value); + if (!status) { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + + return status; +} + +/** + * @brief For a given object instance-number, gets the maximum-actual-value + * property value + * @param object_instance - object-instance number of the object + * @return the maximum-actual-value property value of this object + */ +float Lighting_Output_Max_Actual_Value(uint32_t object_instance) +{ + float value = 0.0; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + value = + lighting_command_max_actual_value_get(&pObject->Lighting_Command); + } + + return value; +} + +/** + * @brief For a given object instance-number, sets the maximum-actual-value + * property value + * @param object_instance - object-instance number of the object + * @param value - the maximum-actual-value property value to be set + * @return true if the maximum-actual-value property value was set + */ +bool Lighting_Output_Max_Actual_Value_Set(uint32_t object_instance, float value) +{ + bool status = false; + struct object_data *pObject; + float min_actual_value; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + if (isgreaterequal(value, 1.0f) && islessequal(value, 100.0f)) { + /* Max_Actual_Value shall always be a positive number + in the range 1.0% to 100.0%.*/ + min_actual_value = lighting_command_min_actual_value_get( + &pObject->Lighting_Command); + if (value < min_actual_value) { + /* Changing Max_Actual_Value to a value less than + Min_Actual_Value shall force Min_Actual_Value + to become equal to Max_Actual_Value. */ + lighting_command_min_actual_value_set( + &pObject->Lighting_Command, value); + } + lighting_command_max_actual_value_set( + &pObject->Lighting_Command, value); + status = true; + } + } + + return status; +} + +/** + * Handle a WriteProperty to a specific property. + * + * @param object_instance - object-instance number of the object + * @param value - property value to be written + * @param priority - priority-array index value 1..16 + * @param error_class - the BACnet error class + * @param error_code - BACnet Error code + * + * @return true if values are within range and present-value is set. + */ +static bool Lighting_Output_Max_Actual_Value_Write( + uint32_t object_instance, + float value, + uint8_t priority, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + bool status = false; + + (void)priority; + status = Lighting_Output_Max_Actual_Value_Set(object_instance, value); + if (!status) { + *error_class = ERROR_CLASS_PROPERTY; + *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } + + return status; +} + /** * For a given object instance-number, sets the * lighting-command-default-priority property value of the object. @@ -2357,7 +2504,7 @@ bool Lighting_Output_Out_Of_Service(uint32_t object_instance) pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - value = pObject->Lighting_Command.Out_Of_Service; + value = lighting_command_out_of_service_get(&pObject->Lighting_Command); } return value; @@ -2377,7 +2524,7 @@ void Lighting_Output_Out_Of_Service_Set(uint32_t object_instance, bool value) pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - pObject->Lighting_Command.Out_Of_Service = value; + lighting_command_out_of_service_set(&pObject->Lighting_Command, value); } } @@ -2477,7 +2624,7 @@ float Lighting_Output_Last_On_Value(uint32_t object_instance) pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - value = pObject->Lighting_Command.Last_On_Value; + value = lighting_command_last_on_value_get(&pObject->Lighting_Command); } return value; @@ -2498,7 +2645,8 @@ bool Lighting_Output_Last_On_Value_Set(uint32_t object_instance, float value) pObject = Keylist_Data(Object_List, object_instance); if (pObject) { if (isgreaterequal(value, 1.0) && islessequal(value, 100.0)) { - pObject->Lighting_Command.Last_On_Value = value; + lighting_command_last_on_value_set( + &pObject->Lighting_Command, value); status = true; } } @@ -2547,7 +2695,8 @@ float Lighting_Output_Default_On_Value(uint32_t object_instance) pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - value = pObject->Lighting_Command.Default_On_Value; + value = + lighting_command_default_on_value_get(&pObject->Lighting_Command); } return value; @@ -2568,7 +2717,8 @@ bool Lighting_Output_Default_On_Value_Set(uint32_t object_instance, float value) pObject = Keylist_Data(Object_List, object_instance); if (pObject) { if (isgreaterequal(value, 1.0) && islessequal(value, 100.0)) { - pObject->Lighting_Command.Default_On_Value = value; + lighting_command_default_on_value_set( + &pObject->Lighting_Command, value); status = true; } } @@ -2643,9 +2793,7 @@ bool Lighting_Output_High_End_Trim_Set(uint32_t object_instance, float value) pObject->High_End_Trim = value; Lighting_Command_Trim_Apply( pObject, Present_Value_Priority(pObject)); - if (!Lighting_Command_In_Progress(pObject)) { - lighting_command_refresh(&pObject->Lighting_Command); - } + lighting_command_refresh(&pObject->Lighting_Command); status = true; } } @@ -2720,9 +2868,7 @@ bool Lighting_Output_Low_End_Trim_Set(uint32_t object_instance, float value) pObject->Low_End_Trim = value; Lighting_Command_Trim_Apply( pObject, Present_Value_Priority(pObject)); - if (!Lighting_Command_In_Progress(pObject)) { - lighting_command_refresh(&pObject->Lighting_Command); - } + lighting_command_refresh(&pObject->Lighting_Command); status = true; } } @@ -2798,9 +2944,7 @@ bool Lighting_Output_Trim_Fade_Time_Set( pObject->Trim_Fade_Time = value; Lighting_Command_Trim_Apply( pObject, Present_Value_Priority(pObject)); - if (!Lighting_Command_In_Progress(pObject)) { - lighting_command_refresh(&pObject->Lighting_Command); - } + lighting_command_refresh(&pObject->Lighting_Command); status = true; } } @@ -2927,8 +3071,7 @@ bool Lighting_Output_Overridden_Status(uint32_t object_instance) pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - status = pObject->Lighting_Command.Overridden || - pObject->Lighting_Command.Overridden_Momentary; + status = lighting_command_overridden_status(&pObject->Lighting_Command); } return status; @@ -3417,6 +3560,16 @@ int Lighting_Output_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) apdu_len = encode_application_enumerated( apdu, Lighting_Output_Transition(rpdata->object_instance)); break; + case PROP_MIN_ACTUAL_VALUE: + real_value = + Lighting_Output_Min_Actual_Value(rpdata->object_instance); + apdu_len = encode_application_real(&apdu[0], real_value); + break; + case PROP_MAX_ACTUAL_VALUE: + real_value = + Lighting_Output_Max_Actual_Value(rpdata->object_instance); + apdu_len = encode_application_real(&apdu[0], real_value); + break; case PROP_PRIORITY_ARRAY: apdu_len = bacnet_array_encode( rpdata->object_instance, rpdata->array_index, @@ -3533,6 +3686,10 @@ bool Lighting_Output_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_known_property( wp_data->application_data, wp_data->application_data_len, &value, @@ -3621,6 +3778,26 @@ bool Lighting_Output_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) &wp_data->error_code); } break; + case PROP_MIN_ACTUAL_VALUE: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_REAL); + if (status) { + status = Lighting_Output_Min_Actual_Value_Write( + wp_data->object_instance, value.type.Real, + wp_data->priority, &wp_data->error_class, + &wp_data->error_code); + } + break; + case PROP_MAX_ACTUAL_VALUE: + status = write_property_type_valid( + wp_data, &value, BACNET_APPLICATION_TAG_REAL); + if (status) { + status = Lighting_Output_Max_Actual_Value_Write( + wp_data->object_instance, value.type.Real, + wp_data->priority, &wp_data->error_class, + &wp_data->error_code); + } + break; case PROP_RELINQUISH_DEFAULT: status = write_property_type_valid( wp_data, &value, BACNET_APPLICATION_TAG_REAL); @@ -3823,6 +4000,25 @@ void Lighting_Output_Context_Set(uint32_t object_instance, void *context) } } +/** + * @brief Get the lighting command data for a specific object instance + * @param object_instance [in] BACnet object instance number + * @return pointer to the lighting command data, or NULL if not found + */ +BACNET_LIGHTING_COMMAND_DATA * +Lighting_Output_Lighting_Command_Data(uint32_t object_instance) +{ + BACNET_LIGHTING_COMMAND_DATA *data = NULL; + struct object_data *pObject; + + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + data = &pObject->Lighting_Command; + } + + return data; +} + /** * @brief Creates a Color object * @param object_instance - object-instance number of the object @@ -3857,9 +4053,10 @@ uint32_t Lighting_Output_Create(uint32_t object_instance) pObject->Description = NULL; pObject->Present_Value = 0.0f; lighting_command_init(&pObject->Lighting_Command); - pObject->Lighting_Command.Key = object_instance; - pObject->Lighting_Command.Notification_Head.callback = - Lighting_Output_Tracking_Value_Callback; + lighting_command_key_set(&pObject->Lighting_Command, object_instance); + lighting_command_tracking_value_callback_set( + &pObject->Lighting_Command, + Lighting_Output_Tracking_Value_Callback); pObject->Last_Lighting_Command.operation = BACNET_LIGHTS_NONE; pObject->Last_Lighting_Command.use_target_level = false; pObject->Last_Lighting_Command.use_ramp_rate = false; @@ -3868,9 +4065,10 @@ uint32_t Lighting_Output_Create(uint32_t object_instance) pObject->Last_Lighting_Command.use_priority = false; pObject->Blink_Warn_Enable = false; pObject->High_End_Trim = 100.0f; - pObject->Lighting_Command.High_Trim_Value = pObject->High_End_Trim; pObject->Low_End_Trim = 1.0f; - pObject->Lighting_Command.Low_Trim_Value = pObject->Low_End_Trim; + lighting_command_trim_set( + &pObject->Lighting_Command, pObject->High_End_Trim, + pObject->Low_End_Trim, pObject->Trim_Fade_Time); pObject->Trim_Fade_Time = 0; pObject->Egress_Time_Seconds = 0; pObject->Default_Fade_Time = 100; diff --git a/src/bacnet/basic/object/lo.h b/src/bacnet/basic/object/lo.h index 319e597afe..8f849d816d 100644 --- a/src/bacnet/basic/object/lo.h +++ b/src/bacnet/basic/object/lo.h @@ -201,6 +201,17 @@ BACNET_STACK_EXPORT bool Lighting_Output_Default_Priority_Set( uint32_t object_instance, unsigned priority); +BACNET_STACK_EXPORT +float Lighting_Output_Min_Actual_Value(uint32_t object_instance); +BACNET_STACK_EXPORT +bool Lighting_Output_Min_Actual_Value_Set( + uint32_t object_instance, float value); +BACNET_STACK_EXPORT +float Lighting_Output_Max_Actual_Value(uint32_t object_instance); +BACNET_STACK_EXPORT +bool Lighting_Output_Max_Actual_Value_Set( + uint32_t object_instance, float value); + BACNET_STACK_EXPORT bool Lighting_Output_Color_Override(uint32_t object_instance); BACNET_STACK_EXPORT @@ -248,6 +259,10 @@ void *Lighting_Output_Context_Get(uint32_t object_instance); BACNET_STACK_EXPORT void Lighting_Output_Context_Set(uint32_t object_instance, void *context); +BACNET_STACK_EXPORT +BACNET_LIGHTING_COMMAND_DATA * +Lighting_Output_Lighting_Command_Data(uint32_t object_instance); + BACNET_STACK_EXPORT uint32_t Lighting_Output_Create(uint32_t object_instance); BACNET_STACK_EXPORT diff --git a/src/bacnet/basic/sys/lighting_command.c b/src/bacnet/basic/sys/lighting_command.c index 050154bd53..4809067785 100644 --- a/src/bacnet/basic/sys/lighting_command.c +++ b/src/bacnet/basic/sys/lighting_command.c @@ -16,12 +16,142 @@ #include "lighting_command.h" /** - * @brief call the lighting command tracking value callbacks + * @brief compare two floating point values to 3 decimal places + * + * @param x1 - first comparison value + * @param x2 - second comparison value + * @return true if the value is the same to 3 decimal points + */ +static bool is_float_equal(float x1, float x2) +{ + return fabs(x1 - x2) < 0.001; +} + +/** + * @brief Clamp the value within the normalized range + * @details The normalized output level is specified as the + * linearized percentage (0..100%) of the possible light output range + * with 0.0% being off, 1.0% being dimmest, and 100.0% being brightest. + * @param value [in] value to clamp within the normalized range + * @return value clamped within the normalized range of 0.0% to 100.0% + */ +float lighting_command_normalized_range_clamp(float value) +{ + float physical_value; + + /* clamp value within physical values, if non-zero */ + if (isless(value, 1.0f)) { + /* jump target to OFF */ + physical_value = 0.0f; + } else if (isgreater(value, 100.0f)) { + physical_value = 100.0f; + } else { + physical_value = value; + } + + return physical_value; +} + +/** + * @brief Clamp the value within the normal ON range + * @details The normal ON output level is specified as the linearized + * percentage (1..100%) of the possible light output range with 1.0% being + * dimmest, and 100.0% being brightest. + * @param value [in] value to clamp within the normalized ON range + * @return value clamped within the normalized ON range of 1.0% to 100.0% + */ +float lighting_command_normalized_on_range_clamp(float value) +{ + float normalized_on_value; + + /* clamp value within 1.0 and 100.0 values */ + if (isless(value, 1.0f)) { + normalized_on_value = 1.0f; + } else if (isgreater(value, 100.0f)) { + normalized_on_value = 100.0f; + } else { + normalized_on_value = value; + } + + return normalized_on_value; +} + +/** + * @brief Adjust Min/Max Actual Value Range + * @details Min_Actual_Value property, of type Real, shall specify + * the physical output level that corresponds to a Present_Value of 1.0%. + * Changing Min_Actual_Value to a value greater than Max_Actual_Value + * shall force Max_Actual_Value to become equal to Min_Actual_Value. + * Min_Actual_Value shall always be a positive number in the + * range 1.0% to 100.0%. + * + * Max_Actual_Value property, of type Real, shall specify the physical + * output level that corresponds to a Present_Value of 100.0%. + * Changing Max_Actual_Value to a value less than Min_Actual_Value + * shall force Min_Actual_Value to become equal to Max_Actual_Value. + * Max_Actual_Value shall always be a positive number in the range + * 1.0% to 100.0%. + */ +static void lighting_command_min_max_value_range_adjust( + struct bacnet_lighting_command_data *data) +{ + float swap_value, min_value, max_value; + + min_value = + lighting_command_normalized_on_range_clamp(data->Min_Actual_Value); + max_value = + lighting_command_normalized_on_range_clamp(data->Max_Actual_Value); + if (isgreater(min_value, max_value)) { + /* swap the configured high and low actual values if they are inverse */ + swap_value = min_value; + min_value = max_value; + max_value = swap_value; + } + data->Min_Actual_Value = min_value; + data->Max_Actual_Value = max_value; +} + +/** + * @brief Calculate the Feedback_Value property value + * + * This property, of type Real, shall indicate the actual value + * of the physical lighting output within the normalized range. + * If the actual value of the physical lighting output in the + * non-normalized range is not off but is less than the + * Min_Actual_Value, then Feedback_Value shall be set to 1.0%. + * If the actual value in the non-normalized range is greater than + * Max_Actual_Value, then Feedback_Value shall be set to 100.0%. + * @param data - dimmer data structure + * @return calculated feedback-value + */ +float lighting_command_normalized_to_physical_value( + float min_value, float max_value, float normalized_value) +{ + float physical_value, x1, x2, x3, y1, y3; + + if (isless(normalized_value, 1.0f)) { + physical_value = 0.0f; + } else if (isgreater(normalized_value, 100.0f)) { + physical_value = max_value; + } else { + x1 = 1.0f; + x2 = normalized_value; + x3 = 100.0f; + y1 = min_value; + y3 = max_value; + physical_value = linear_interpolate(x1, x2, x3, y1, y3); + } + + return physical_value; +} + +/** + * @brief call the lighting command notification callbacks * @param data - dimmer data structure - * @param old_value - value prior to write - * @param value - value of the write + * @param old_value - physical value prior to write + * @param value - physical value of the write */ -static void lighting_command_tracking_value_notify( +static void lighting_command_notify( struct bacnet_lighting_command_data *data, float old_value, float value) { struct lighting_command_notification *head; @@ -46,6 +176,11 @@ void lighting_command_notification_add( { struct lighting_command_notification *head; + if (!data || !notification) { + return; + } + lighting_command_lock(data); + head = &data->Notification_Head; do { if (head->next == notification) { @@ -58,6 +193,7 @@ void lighting_command_notification_add( } head = head->next; } while (head); + lighting_command_unlock(data); } /** @@ -90,6 +226,11 @@ void lighting_command_timer_notfication_add( { struct lighting_command_timer_notification *head; + if (!data || !notification) { + return; + } + lighting_command_lock(data); + head = &data->Timer_Notification_Head; do { if (head->next == notification) { @@ -102,6 +243,7 @@ void lighting_command_timer_notfication_add( } head = head->next; } while (head); + lighting_command_unlock(data); } /** @@ -109,8 +251,8 @@ void lighting_command_timer_notfication_add( * for WARN_OFF/WARN_RELINQUISH operations * @param data - dimmer data structure */ -static void -lighting_command_blink_stop_notify(struct bacnet_lighting_command_data *data) +static void lighting_command_blink_stop_notify_nolock( + struct bacnet_lighting_command_data *data) { if (data->Blink.Callback) { /* do some checking to avoid extra callbacks */ @@ -163,32 +305,6 @@ float lighting_command_step_increment_clamp(float step_increment) return step_increment; } -/** - * @brief Clamp the value within the physical min/max range - * @details The physical output level, or non-normalized range, - * is specified as the linearized percentage (0..100%) - * of the possible light output range with 0.0% being off, - * 1.0% being dimmest, and 100.0% being brightest. - * @param value [in] value to clamp within the physical min/max range - * @return value clamped within the physical min/max range of 0.0% to 100.0% - */ -float lighting_command_physical_range_clamp(float value) -{ - float physical_value; - - /* clamp value within physical values, if non-zero */ - if (isless(value, 1.0f)) { - /* jump target to OFF */ - physical_value = 0.0f; - } else if (isgreater(value, 100.0f)) { - physical_value = 100.0f; - } else { - physical_value = value; - } - - return physical_value; -} - /** * @brief Calculate the target value for a step down command * @param tracking_value [in] current tracking value @@ -288,152 +404,49 @@ static float lighting_command_trim_fade( * @return value clamped within the operating range defined by the High_End_Trim * and Low_End_Trim property values */ -float lighting_command_operating_range_clamp_fade( +static float lighting_command_operating_range_clamp_fade_nolock( struct bacnet_lighting_command_data *data, float value, uint16_t milliseconds) { - float high_trim, low_trim, swap_value; - if (data) { - /* clamp range within physical limits */ - high_trim = - lighting_command_physical_range_clamp(data->High_Trim_Value); - low_trim = lighting_command_physical_range_clamp(data->Low_Trim_Value); - /* valid range check for high and low trim values */ - if (isgreater(low_trim, high_trim)) { - /* swap the trims if they are inverse */ - swap_value = low_trim; - low_trim = high_trim; - high_trim = swap_value; - } /* clamp value within trim values, if non-zero */ if (isless(value, 1.0f)) { /* jump target to OFF if below normalized min */ value = 0.0f; - } else if (isgreater(value, high_trim)) { + } else if (isgreater(value, data->High_Trim_Value)) { value = lighting_command_trim_fade( - data, value, high_trim, milliseconds); + data, value, data->High_Trim_Value, milliseconds); data->In_Progress = BACNET_LIGHTING_TRIM_ACTIVE; - } else if (isless(value, low_trim)) { - value = - lighting_command_trim_fade(data, value, low_trim, milliseconds); + } else if (isless(value, data->Low_Trim_Value)) { + value = lighting_command_trim_fade( + data, value, data->Low_Trim_Value, milliseconds); data->In_Progress = BACNET_LIGHTING_TRIM_ACTIVE; } } else { /* no data, so just clamp value within physical limits */ - value = lighting_command_physical_range_clamp(value); + value = lighting_command_normalized_range_clamp(value); } return value; } -/** - * @brief Clamp the value within the operating range between low and high - * end trim values immediately. - * @details The Operating Range is a subset of the Normalized Range, - * that represents the range of acceptable values for control of the object. - * The Operating Range is defined by the High_End_Trim and Low_End_Trim - * property values. When values are written outside of the Operating Range, - * the Tracking_Value will reflect the actual, clamped normalized light - * output while the Present_Value will reflect the original target value. - * @param data - dimmer data structure - * @param value the value that will be subject to clamping - * @return value clamped within the operating range defined by the High_End_Trim - * and Low_End_Trim property values - */ -float lighting_command_operating_range_clamp( - struct bacnet_lighting_command_data *data, float value) -{ - return lighting_command_operating_range_clamp_fade(data, value, 0); -} - -/** - * @brief Clamp the value within the normalized ON range 1% to 100%. - * @details The physical output level, or non-normalized range, - * is specified as the linearized percentage (0..100%) - * of the possible light output range with 0.0% being off, - * 1.0% being dimmest, and 100.0% being brightest. - * The actual range represents the subset of physical output levels - * defined by Min_Actual_Value and Max_Actual_Value - * (or 1.0 to 100.0% if these properties are not present). - * The normalized range is always 0.0 to 100.0% where - * 1.0% = bottom of the actual range and 100.0% = top of the actual range. - * @param data - dimmer data structure - * @param value [in] value to normalize - * @return normalized value within the range defined by Min_Actual_Value - * and Max_Actual_Value - */ -float lighting_command_normalized_on_range_clamp( - struct bacnet_lighting_command_data *data, float value) -{ - float min_value, max_value, swap_value; - - /* clamp range within physical limits */ - max_value = lighting_command_physical_range_clamp(data->Max_Actual_Value); - min_value = lighting_command_physical_range_clamp(data->Min_Actual_Value); - /* valid range check for high and low trim values */ - if (isgreater(min_value, max_value)) { - /* swap the trims if they are inverse */ - swap_value = min_value; - min_value = max_value; - max_value = swap_value; - } - /* clamp value within trim values, if non-zero */ - if (isgreater(value, max_value)) { - value = max_value; - } else if (isless(value, min_value)) { - value = min_value; - } - - return value; -} - -/** - * @brief Normalize the value to the min/max range - * @details The physical output level, or non-normalized range, - * is specified as the linearized percentage (0..100%) - * of the possible light output range with 0.0% being off, - * 1.0% being dimmest, and 100.0% being brightest. - * The actual range represents the subset of physical output levels - * defined by Min_Actual_Value and Max_Actual_Value - * (or 1.0 to 100.0% if these properties are not present). - * The normalized range is always 0.0 to 100.0% where - * 1.0% = bottom of the actual range and 100.0% = top of the actual range. - * @param data - dimmer data structure - * @param value [in] value to normalize - * @return normalized value within the range defined by - * 0.0%, Min_Actual_Value, and Max_Actual_Value - */ -float lighting_command_normalized_range_clamp( - struct bacnet_lighting_command_data *data, float value) +float lighting_command_operating_range_clamp_fade( + struct bacnet_lighting_command_data *data, + float value, + uint16_t milliseconds) { - float normalized_value; - float min_value, max_value, swap_value; + float clamped_value; - /* clamp range within physical limits */ - max_value = lighting_command_physical_range_clamp(data->Max_Actual_Value); - min_value = lighting_command_physical_range_clamp(data->Min_Actual_Value); - /* valid range check for high and low trim values */ - if (isgreater(min_value, max_value)) { - /* swap the trims if they are inverse */ - swap_value = min_value; - min_value = max_value; - max_value = swap_value; - } - /* clamp value within normalized values, if non-zero */ - if (isless(value, 1.0f)) { - /* jump target to OFF if below normalized min */ - normalized_value = 0.0f; - } else if (isgreater(value, max_value)) { - normalized_value = max_value; - } else if (isless(value, min_value)) { - normalized_value = min_value; - } else { - normalized_value = value; + if (!data) { + return value; } + lighting_command_lock(data); + clamped_value = lighting_command_operating_range_clamp_fade_nolock( + data, value, milliseconds); + lighting_command_unlock(data); - return normalized_value; + return clamped_value; } /** @@ -444,16 +457,25 @@ float lighting_command_normalized_range_clamp( * @param milliseconds - number of milliseconds elapsed */ static void lighting_command_tracking_value_event( - struct bacnet_lighting_command_data *data, float old_value, float value) + struct bacnet_lighting_command_data *data, + float old_value, + float tracking_value) { + float physical_value, old_physical_value; + + lighting_command_min_max_value_range_adjust(data); + physical_value = lighting_command_normalized_to_physical_value( + data->Min_Actual_Value, data->Max_Actual_Value, tracking_value); + old_physical_value = lighting_command_normalized_to_physical_value( + data->Min_Actual_Value, data->Max_Actual_Value, old_value); if (data->Overridden) { - lighting_command_tracking_value_notify(data, old_value, value); + lighting_command_notify(data, old_physical_value, physical_value); if (data->Overridden_Momentary) { data->Overridden = false; } } else if (!data->Out_Of_Service) { data->Overridden_Momentary = false; - lighting_command_tracking_value_notify(data, old_value, value); + lighting_command_notify(data, old_physical_value, physical_value); } else { debug_printf( "Lighting-Command[%lu]-Out-of-Service\n", (unsigned long)data->Key); @@ -475,39 +497,47 @@ static void lighting_command_fade_handler( float target_value; old_value = data->Tracking_Value; - /* clamp Tracking value within the Normalized ON Range */ - target_value = - lighting_command_normalized_on_range_clamp(data, data->Target_Level); - if ((milliseconds >= data->Fade_Time) || - (!islessgreater(data->Tracking_Value, target_value))) { - /* stop fading */ - if (isless(data->Target_Level, 1.0f)) { - /* jump target to OFF if below normalized min */ - data->Tracking_Value = 0.0f; - } else { - data->Tracking_Value = target_value; - } + if (isless(old_value, 1.0f) && isless(data->Target_Level, 1.0f)) { + /* check for OFF to OFF transition */ + data->Tracking_Value = 0.0f; data->In_Progress = BACNET_LIGHTING_IDLE; data->Lighting_Operation = BACNET_LIGHTS_STOP; - data->Fade_Time = 0; } else { - /* fading */ - x1 = 0.0f; - x2 = (float)milliseconds; - x3 = (float)data->Fade_Time; - if (isless(old_value, data->Min_Actual_Value)) { - y1 = data->Min_Actual_Value; + /* clamp Target value within the Normalized ON Range */ + target_value = + lighting_command_normalized_on_range_clamp(data->Target_Level); + if ((milliseconds >= data->Fade_Time) || + (is_float_equal(data->Tracking_Value, target_value))) { + /* stop fading */ + if (isless(data->Target_Level, 1.0f)) { + /* jump target to OFF if below normalized min */ + data->Tracking_Value = 0.0f; + } else { + data->Tracking_Value = target_value; + } + data->In_Progress = BACNET_LIGHTING_IDLE; + data->Lighting_Operation = BACNET_LIGHTS_STOP; + data->Fade_Time = 0; } else { - y1 = old_value; + /* fading in the normalized ON range */ + x1 = 0.0f; + x2 = (float)milliseconds; + x3 = (float)data->Fade_Time; + if (isless(old_value, 1.0f)) { + y1 = 1.0f; + } else { + y1 = old_value; + } + y3 = target_value; + data->Tracking_Value = linear_interpolate(x1, x2, x3, y1, y3); + data->Fade_Time -= milliseconds; + data->In_Progress = BACNET_LIGHTING_FADE_ACTIVE; } - y3 = target_value; - data->Tracking_Value = linear_interpolate(x1, x2, x3, y1, y3); - data->Fade_Time -= milliseconds; - data->In_Progress = BACNET_LIGHTING_FADE_ACTIVE; - } - /* clamp Tracking Value inclusively within the Operating Range */ - data->Tracking_Value = lighting_command_operating_range_clamp_fade( - data, data->Tracking_Value, milliseconds); + /* clamp Tracking Value inclusively within the Operating Range */ + data->Tracking_Value = + lighting_command_operating_range_clamp_fade_nolock( + data, data->Tracking_Value, milliseconds); + } /* notify */ lighting_command_tracking_value_event( data, old_value, data->Tracking_Value); @@ -522,8 +552,7 @@ static void lighting_command_fade_handler( * at a particular percent per second defined by ramp-rate. * While the ramp operation is executing, In_Progress shall be set * to RAMP_ACTIVE, and Tracking_Value shall be updated to reflect the current - * progress of the ramp. shall be clamped to - * Min_Actual_Value and Max_Actual_Value. + * progress of the ramp. * * @param data - dimmer data structure * @param milliseconds - number of milliseconds elapsed @@ -535,70 +564,75 @@ static void lighting_command_ramp_handler( operating_value; old_value = data->Tracking_Value; - /* clamp Tracking value within the Normalized ON Range */ - target_value = - lighting_command_normalized_on_range_clamp(data, data->Target_Level); - if (!islessgreater(data->Tracking_Value, target_value)) { - /* stop ramping */ - if (isless(data->Target_Level, 1.0f)) { - /* jump target to OFF if below normalized min */ - data->Tracking_Value = 0.0f; - } else { - data->Tracking_Value = target_value; - } + if (isless(old_value, 1.0f) && isless(data->Target_Level, 1.0f)) { + /* check for OFF to OFF transition */ + data->Tracking_Value = 0.0f; data->In_Progress = BACNET_LIGHTING_IDLE; data->Lighting_Operation = BACNET_LIGHTS_STOP; } else { - ramp_rate = lighting_command_ramp_rate_clamp(data->Ramp_Rate); - /* determine the number of steps */ - if (milliseconds <= 1000) { - /* percent per second */ - steps = linear_interpolate( - 0.0f, (float)milliseconds, 1000.0f, 0.0f, ramp_rate); - } else { - steps = ((float)milliseconds * ramp_rate) / 1000.0f; - } - if (isless(old_value, target_value)) { - step_value = old_value + steps; - if (isgreater(step_value, target_value)) { - /* stop ramping */ - data->Lighting_Operation = BACNET_LIGHTS_STOP; + /* clamp Target value within the Normalized ON Range */ + target_value = + lighting_command_normalized_on_range_clamp(data->Target_Level); + if (is_float_equal(data->Tracking_Value, target_value)) { + /* stop ramping */ + if (isless(data->Target_Level, 1.0f)) { + /* jump target to OFF if below normalized min */ + data->Tracking_Value = 0.0f; + } else { + data->Tracking_Value = target_value; } - } else if (isgreater(old_value, target_value)) { - if (isgreater(old_value, steps)) { - step_value = old_value - steps; + data->In_Progress = BACNET_LIGHTING_IDLE; + data->Lighting_Operation = BACNET_LIGHTS_STOP; + } else { + ramp_rate = lighting_command_ramp_rate_clamp(data->Ramp_Rate); + /* determine the number of steps */ + if (milliseconds <= 1000) { + /* percent per second */ + steps = linear_interpolate( + 0.0f, (float)milliseconds, 1000.0f, 0.0f, ramp_rate); } else { - step_value = target_value; + steps = ((float)milliseconds * ramp_rate) / 1000.0f; } - if (isless(step_value, target_value)) { + if (isless(old_value, target_value)) { + step_value = old_value + steps; + if (isgreater(step_value, target_value)) { + /* stop ramping */ + data->Lighting_Operation = BACNET_LIGHTS_STOP; + } + } else if (isgreater(old_value, target_value)) { + if (isgreater(old_value, steps)) { + step_value = old_value - steps; + } else { + step_value = target_value; + } + if (isless(step_value, target_value)) { + /* stop ramping */ + data->Lighting_Operation = BACNET_LIGHTS_STOP; + } + } else { /* stop ramping */ + step_value = target_value; data->Lighting_Operation = BACNET_LIGHTS_STOP; } - } else { - /* stop ramping */ - step_value = target_value; - data->Lighting_Operation = BACNET_LIGHTS_STOP; - } - /* clamp target within min/max, if needed */ - step_value = - lighting_command_normalized_on_range_clamp(data, step_value); - if (data->Lighting_Operation == BACNET_LIGHTS_STOP) { - if (isless(data->Target_Level, 1.0f)) { - /* jump target to OFF if below normalized min */ - data->Tracking_Value = 0.0f; + step_value = lighting_command_normalized_on_range_clamp(step_value); + if (data->Lighting_Operation == BACNET_LIGHTS_STOP) { + if (isless(data->Target_Level, 1.0f)) { + /* jump target to OFF if below normalized min */ + data->Tracking_Value = 0.0f; + } else { + data->Tracking_Value = step_value; + } + data->In_Progress = BACNET_LIGHTING_IDLE; } else { data->Tracking_Value = step_value; + data->In_Progress = BACNET_LIGHTING_RAMP_ACTIVE; } - data->In_Progress = BACNET_LIGHTING_IDLE; - } else { - data->Tracking_Value = step_value; - data->In_Progress = BACNET_LIGHTING_RAMP_ACTIVE; } + /* clamp Tracking_Value inclusively within the Operating Range */ + operating_value = lighting_command_operating_range_clamp_fade_nolock( + data, data->Tracking_Value, milliseconds); + data->Tracking_Value = operating_value; } - /* clamp Tracking_Value inclusively within the Operating Range */ - operating_value = lighting_command_operating_range_clamp_fade( - data, data->Tracking_Value, milliseconds); - data->Tracking_Value = operating_value; /* notify */ lighting_command_tracking_value_event( data, old_value, data->Tracking_Value); @@ -608,30 +642,26 @@ static void lighting_command_ramp_handler( * Updates the object tracking value while stepping * * Commands the dimmer to a value equal to the Tracking_Value - * plus the step-increment. The resulting sum shall be clamped to - * Min_Actual_Value and Max_Actual_Value + * plus the step-increment. If the result of the addition is + * greater than 100.0%, the value shall be set to 100.0%. * * @param data [in] dimmer data */ static void lighting_command_step_up_handler(struct bacnet_lighting_command_data *data) { - float old_value, target_value, operating_value; + float old_value, target_value; old_value = data->Tracking_Value; - if (isgreaterequal(old_value, data->Min_Actual_Value)) { - /* inhibit ON if the value is already OFF */ + if (isgreaterequal(old_value, 1.0f)) { + /* inhibit ON if the value is currently OFF */ target_value = lighting_command_step_up_target_value( data->Tracking_Value, data->Step_Increment); - /* clamp Tracking value within the Normalized ON Range */ + /* clamp Tracking value inclusively within the Operating Range */ data->Tracking_Value = - lighting_command_normalized_on_range_clamp(data, target_value); + lighting_command_normalized_on_range_clamp(target_value); data->In_Progress = BACNET_LIGHTING_IDLE; data->Lighting_Operation = BACNET_LIGHTS_STOP; - /* clamp Tracking value inclusively within the Operating Range */ - operating_value = - lighting_command_operating_range_clamp(data, data->Tracking_Value); - data->Tracking_Value = operating_value; /* notify */ lighting_command_tracking_value_event( data, old_value, data->Tracking_Value); @@ -642,8 +672,7 @@ lighting_command_step_up_handler(struct bacnet_lighting_command_data *data) * Updates the object tracking value while stepping * * Commands the dimmer to a value equal to the Tracking_Value - * plus the step-increment. The resulting sum shall be clamped to - * Min_Actual_Value and Max_Actual_Value + * plus the step-increment. * * @param data [in] dimmer data */ @@ -655,14 +684,13 @@ lighting_command_step_down_handler(struct bacnet_lighting_command_data *data) old_value = data->Tracking_Value; target_value = lighting_command_step_down_target_value( data->Tracking_Value, data->Step_Increment); - /* clamp Tracking value within the Normalized ON Range */ data->Tracking_Value = - lighting_command_normalized_on_range_clamp(data, target_value); + lighting_command_normalized_on_range_clamp(target_value); data->In_Progress = BACNET_LIGHTING_IDLE; data->Lighting_Operation = BACNET_LIGHTS_STOP; /* clamp Tracking value inclusively within the Operating Range */ operating_value = - lighting_command_operating_range_clamp(data, data->Tracking_Value); + lighting_command_normalized_range_clamp(data->Tracking_Value); data->Tracking_Value = operating_value; /* notify */ lighting_command_tracking_value_event( @@ -673,27 +701,32 @@ lighting_command_step_down_handler(struct bacnet_lighting_command_data *data) * Updates the object tracking value while stepping * * Commands the dimmer to a value equal to the Tracking_Value - * plus the step-increment. The resulting sum shall be clamped to - * Min_Actual_Value and Max_Actual_Value + * plus the step-increment. + * If the result of the addition is greater than 100.0%, + * the value shall be set to 100.0%. + * When the Tracking_Value is 0.0%, 1.0% is written + * to the specified slot in the priority array. * * @param data [in] dimmer data */ static void lighting_command_step_on_handler(struct bacnet_lighting_command_data *data) { - float old_value, target_value, operating_value; + float old_value, target_value; old_value = data->Tracking_Value; - target_value = lighting_command_step_up_target_value( - data->Tracking_Value, data->Step_Increment); - data->Tracking_Value = - lighting_command_normalized_range_clamp(data, target_value); + if (isless(data->Tracking_Value, 1.0f)) { + /* step is ignored when starting at OFF */ + data->Tracking_Value = 1.0f; + } else { + target_value = lighting_command_step_up_target_value( + data->Tracking_Value, data->Step_Increment); + /* clamp Tracking value inclusively within the Normalized Range */ + data->Tracking_Value = + lighting_command_normalized_range_clamp(target_value); + } data->In_Progress = BACNET_LIGHTING_IDLE; data->Lighting_Operation = BACNET_LIGHTS_STOP; - /* clamp Tracking value inclusively within the Operating Range */ - operating_value = - lighting_command_operating_range_clamp(data, data->Tracking_Value); - data->Tracking_Value = operating_value; /* notify */ lighting_command_tracking_value_event( data, old_value, data->Tracking_Value); @@ -702,28 +735,28 @@ lighting_command_step_on_handler(struct bacnet_lighting_command_data *data) /** * Updates the object tracking value while stepping * - * Commands the dimmer to a value equal to the Tracking_Value - * plus the step-increment. The resulting sum shall be clamped to - * Min_Actual_Value and Max_Actual_Value + * Commands Present_Value to a value equal to the Tracking_Value + * minus the step-increment at the specified priority. + * The step-down operation is implemented by writing + * the Tracking_Value minus step-increment to the specified + * slot in the priority array. + * If the result of the subtraction is less than 1.0%, + * 0.0% is written to the specified slot in the priority array. * * @param data [in] dimmer data */ static void lighting_command_step_off_handler(struct bacnet_lighting_command_data *data) { - float old_value, target_value, operating_value; + float old_value, target_value; old_value = data->Tracking_Value; target_value = lighting_command_step_down_target_value( data->Tracking_Value, data->Step_Increment); data->Tracking_Value = - lighting_command_normalized_range_clamp(data, target_value); + lighting_command_normalized_range_clamp(target_value); data->In_Progress = BACNET_LIGHTING_IDLE; data->Lighting_Operation = BACNET_LIGHTS_STOP; - /* clamp Tracking value inclusively within the Operating Range */ - operating_value = - lighting_command_operating_range_clamp(data, data->Tracking_Value); - data->Tracking_Value = operating_value; /* notify */ lighting_command_tracking_value_event( data, old_value, data->Tracking_Value); @@ -767,7 +800,7 @@ static void lighting_command_blink_handler( } if (data->Blink.Duration == 0) { /* 'end' operation */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); data->In_Progress = BACNET_LIGHTING_IDLE; data->Lighting_Operation = BACNET_LIGHTS_STOP; target_value = data->Blink.End_Value; @@ -799,7 +832,7 @@ static void lighting_command_blink_handler( } if (data->Blink.Count == 0) { /* 'end' operation */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); data->In_Progress = BACNET_LIGHTING_IDLE; data->Lighting_Operation = BACNET_LIGHTS_STOP; target_value = data->Blink.End_Value; @@ -807,10 +840,8 @@ static void lighting_command_blink_handler( } } } - target_value = lighting_command_normalized_range_clamp(data, target_value); /* clamp Tracking value inclusively within the Operating Range */ - operating_value = - lighting_command_operating_range_clamp(data, target_value); + operating_value = lighting_command_normalized_range_clamp(target_value); /* note: The blink-warn notifications shall not be reflected in the tracking value. */ if (data->In_Progress == BACNET_LIGHTING_IDLE) { @@ -823,20 +854,32 @@ static void lighting_command_blink_handler( * @brief Overrides the current lighting command with the provided value * @param data [in] dimmer data */ -void lighting_command_override( +static void lighting_command_override_nolock( struct bacnet_lighting_command_data *data, float value) { float old_value; - if (!data) { - return; - } old_value = data->Tracking_Value; - data->Tracking_Value = lighting_command_physical_range_clamp(value); + data->Tracking_Value = lighting_command_normalized_range_clamp(value); lighting_command_tracking_value_event( data, old_value, data->Tracking_Value); } +/** + * @brief Overrides the current lighting command with the provided value + * @param data [in] dimmer data + */ +void lighting_command_override( + struct bacnet_lighting_command_data *data, float value) +{ + if (!data) { + return; + } + lighting_command_lock(data); + lighting_command_override_nolock(data, value); + lighting_command_unlock(data); +} + /** * @brief Overrides the current lighting command with the provided value * @param data [in] dimmer data @@ -847,9 +890,11 @@ void lighting_command_override_set( if (!data) { return; } + lighting_command_lock(data); data->Overridden = true; data->Overridden_Momentary = false; - lighting_command_override(data, value); + lighting_command_override_nolock(data, value); + lighting_command_unlock(data); } /** @@ -859,21 +904,20 @@ void lighting_command_override_set( void lighting_command_override_clear( struct bacnet_lighting_command_data *data, float value) { - float old_value, normalized_value, operating_value; + float old_value; if (!data) { return; } + lighting_command_lock(data); data->Overridden = false; data->Overridden_Momentary = false; old_value = data->Tracking_Value; /* clamp Tracking value within the Normalized Range */ - normalized_value = lighting_command_normalized_range_clamp(data, value); - /* clamp Tracking value inclusively within the Operating Range */ - operating_value = - lighting_command_operating_range_clamp(data, normalized_value); - data->Tracking_Value = operating_value; - lighting_command_tracking_value_event(data, old_value, operating_value); + data->Tracking_Value = lighting_command_normalized_range_clamp(value); + lighting_command_tracking_value_event( + data, old_value, data->Tracking_Value); + lighting_command_unlock(data); } /** @@ -886,9 +930,11 @@ void lighting_command_override_momentary( if (!data) { return; } + lighting_command_lock(data); data->Overridden = true; data->Overridden_Momentary = true; - lighting_command_override(data, value); + lighting_command_override_nolock(data, value); + lighting_command_unlock(data); } /** @@ -902,8 +948,10 @@ void lighting_command_refresh(struct bacnet_lighting_command_data *data) if (!data) { return; } + lighting_command_lock(data); value = data->Tracking_Value; lighting_command_tracking_value_event(data, value, value); + lighting_command_unlock(data); } /** @@ -918,6 +966,7 @@ void lighting_command_timer( if (!data) { return; } + lighting_command_lock(data); if (data->Overridden) { data->Lighting_Operation = BACNET_LIGHTS_NONE; } @@ -961,6 +1010,7 @@ void lighting_command_timer( break; } lighting_command_timer_notify(data, milliseconds); + lighting_command_unlock(data); } /** @@ -975,8 +1025,9 @@ void lighting_command_fade_to( if (!data) { return; } + lighting_command_lock(data); /* possibly interrupting a blink warn, so notify */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); /* configure the lighting operation */ data->Fade_Time = fade_time; data->Lighting_Operation = BACNET_LIGHTS_FADE_TO; @@ -985,6 +1036,7 @@ void lighting_command_fade_to( /* the last value that was greater than or equal to 1.0%.*/ data->Last_On_Value = value; } + lighting_command_unlock(data); } /** @@ -999,8 +1051,9 @@ void lighting_command_ramp_to( if (!data) { return; } + lighting_command_lock(data); /* possibly interrupting a blink warn, so notify */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); /* configure the lighting operation */ data->Ramp_Rate = lighting_command_ramp_rate_clamp(ramp_rate); data->Lighting_Operation = BACNET_LIGHTS_RAMP_TO; @@ -1009,6 +1062,7 @@ void lighting_command_ramp_to( /* the last value that was greater than or equal to 1.0%.*/ data->Last_On_Value = value; } + lighting_command_unlock(data); } /** @@ -1027,15 +1081,16 @@ void lighting_command_step( if (!data) { return; } + lighting_command_lock(data); /* possibly interrupting a blink warn, so notify */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); /* configure the lighting operation */ if (((operation == BACNET_LIGHTS_STEP_UP) || (operation == BACNET_LIGHTS_STEP_DOWN)) && - (!islessgreater(data->Tracking_Value, 0.0))) { + (is_float_equal(data->Tracking_Value, 0.0))) { /* If the starting level of Tracking_Value is 0.0%, then this operation is ignored. */ - return; + goto done; } data->Lighting_Operation = operation; data->Fade_Time = 0; @@ -1044,30 +1099,29 @@ void lighting_command_step( if (operation == BACNET_LIGHTS_STEP_UP) { target_value = lighting_command_step_up_target_value( data->Tracking_Value, data->Step_Increment); - target_value = - lighting_command_normalized_on_range_clamp(data, target_value); + target_value = lighting_command_normalized_on_range_clamp(target_value); } else if (operation == BACNET_LIGHTS_STEP_DOWN) { target_value = lighting_command_step_down_target_value( data->Tracking_Value, data->Step_Increment); - target_value = - lighting_command_normalized_on_range_clamp(data, target_value); + target_value = lighting_command_normalized_on_range_clamp(target_value); } else if (operation == BACNET_LIGHTS_STEP_ON) { target_value = lighting_command_step_up_target_value( data->Tracking_Value, data->Step_Increment); - target_value = - lighting_command_normalized_range_clamp(data, target_value); + target_value = lighting_command_normalized_range_clamp(target_value); } else if (operation == BACNET_LIGHTS_STEP_OFF) { target_value = lighting_command_step_down_target_value( data->Tracking_Value, data->Step_Increment); - target_value = - lighting_command_normalized_range_clamp(data, target_value); + target_value = lighting_command_normalized_range_clamp(target_value); } else { - return; + goto done; } if (isgreaterequal(target_value, 1.0)) { /* the last value that was greater than or equal to 1.0%.*/ data->Last_On_Value = target_value; } + +done: + lighting_command_unlock(data); } /** @@ -1081,11 +1135,12 @@ void lighting_command_blink_warn( BACNET_LIGHTING_OPERATION operation, struct bacnet_lighting_command_warn_data *blink) { - if (!data) { + if (!data || !blink) { return; } + lighting_command_lock(data); /* possibly interrupting a blink warn, so notify */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); /* configure the new warning */ data->Lighting_Operation = operation; data->Blink.Target_Interval = blink->Interval; @@ -1101,6 +1156,33 @@ void lighting_command_blink_warn( /* configure next interval */ data->Blink.State = false; data->Blink.Interval = blink->Interval; + lighting_command_unlock(data); +} + +/** + * @brief Copy the current blink data from the lighting command + * @param data [in] dimmer object instance + * @param blink [out] BACnet blink data to copy into + */ +void lighting_command_blink_copy( + struct bacnet_lighting_command_data *data, + struct bacnet_lighting_command_warn_data *blink) +{ + if (!data || !blink) { + return; + } + lighting_command_lock(data); + blink->On_Value = data->Blink.On_Value; + blink->Off_Value = data->Blink.Off_Value; + blink->End_Value = data->Blink.End_Value; + blink->Priority = data->Blink.Priority; + blink->Callback = data->Blink.Callback; + blink->Target_Interval = data->Blink.Target_Interval; + blink->Interval = data->Blink.Interval; + blink->Duration = data->Blink.Duration; + blink->Count = data->Blink.Count; + blink->State = data->Blink.State; + lighting_command_unlock(data); } /** @@ -1113,14 +1195,16 @@ void lighting_command_stop(struct bacnet_lighting_command_data *data) if (!data) { return; } + lighting_command_lock(data); /* possibly interrupting a blink warn, so notify */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); /* configure the lighting operation */ data->Lighting_Operation = BACNET_LIGHTS_STOP; if (isgreaterequal(data->Tracking_Value, 1.0)) { /* the last value that was greater than or equal to 1.0%.*/ data->Last_On_Value = data->Tracking_Value; } + lighting_command_unlock(data); } /** @@ -1133,10 +1217,12 @@ void lighting_command_none(struct bacnet_lighting_command_data *data) if (!data) { return; } + lighting_command_lock(data); /* possibly interrupting a blink warn, so notify */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); /* configure the lighting operation */ data->Lighting_Operation = BACNET_LIGHTS_NONE; + lighting_command_unlock(data); } /** @@ -1150,12 +1236,14 @@ void lighting_command_restore_on( if (!data) { return; } + lighting_command_lock(data); /* possibly interrupting a blink warn, so notify */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); /* configure the lighting operation */ data->Fade_Time = fade_time; data->Lighting_Operation = BACNET_LIGHTS_RESTORE_ON; data->Target_Level = data->Last_On_Value; + lighting_command_unlock(data); } /** @@ -1169,12 +1257,14 @@ void lighting_command_default_on( if (!data) { return; } + lighting_command_lock(data); /* possibly interrupting a blink warn, so notify */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); /* configure the lighting operation */ data->Fade_Time = fade_time; data->Lighting_Operation = BACNET_LIGHTS_DEFAULT_ON; data->Target_Level = data->Default_On_Value; + lighting_command_unlock(data); } /** @@ -1188,8 +1278,9 @@ void lighting_command_toggle_restore( if (!data) { return; } + lighting_command_lock(data); /* possibly interrupting a blink warn, so notify */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); /* configure the lighting operation */ data->Fade_Time = fade_time; data->Lighting_Operation = BACNET_LIGHTS_TOGGLE_RESTORE; @@ -1200,6 +1291,7 @@ void lighting_command_toggle_restore( /* not OFF, write 0.0% */ data->Target_Level = 0.0f; } + lighting_command_unlock(data); } /** @@ -1213,8 +1305,9 @@ void lighting_command_toggle_default( if (!data) { return; } + lighting_command_lock(data); /* possibly interrupting a blink warn, so notify */ - lighting_command_blink_stop_notify(data); + lighting_command_blink_stop_notify_nolock(data); /* configure the lighting operation */ data->Fade_Time = fade_time; data->Lighting_Operation = BACNET_LIGHTS_TOGGLE_DEFAULT; @@ -1225,13 +1318,467 @@ void lighting_command_toggle_default( /* not OFF, write 0.0% */ data->Target_Level = 0.0f; } + lighting_command_unlock(data); +} + +/** + * @brief Configure the lighting command to apply low or high trim + * @param data [in] dimmer data + * @param high_end_trim [in] BACnet lighting high end trim + * @param low_end_trim [in] BACnet lighting low end trim + * @param trim_fade_time [in] BACnet lighting trim fade time + */ +void lighting_command_trim_set( + struct bacnet_lighting_command_data *data, + float high_end_trim, + float low_end_trim, + uint32_t trim_fade_time) +{ + float swap_value, high_trim, low_trim; + + if (!data) { + return; + } + lighting_command_lock(data); + /* clamp range within normalized limits */ + high_trim = lighting_command_normalized_on_range_clamp(high_end_trim); + low_trim = lighting_command_normalized_on_range_clamp(low_end_trim); + /* valid range check for high and low trim values */ + if (isgreater(low_trim, high_trim)) { + /* swap the trims if they are inverse */ + swap_value = low_trim; + low_trim = high_trim; + high_trim = swap_value; + } + data->High_Trim_Value = high_trim; + data->Low_Trim_Value = low_trim; + data->Trim_Fade_Time = trim_fade_time; + lighting_command_unlock(data); +} + +/** + * @brief Set the lighting command key + * @param data [in] dimmer data + * @param key [in] BACnet lighting key + */ +void lighting_command_key_set( + struct bacnet_lighting_command_data *data, uint32_t key) +{ + if (!data) { + return; + } + lighting_command_lock(data); + data->Key = key; + lighting_command_unlock(data); +} + +/** + * @brief Set the lighting command tracking value callback + * @param data [in] dimmer data + * @param cb [in] BACnet lighting tracking value callback + */ +void lighting_command_tracking_value_callback_set( + struct bacnet_lighting_command_data *data, + lighting_command_tracking_value_callback cb) +{ + if (!data) { + return; + } + lighting_command_lock(data); + data->Notification_Head.callback = cb; + lighting_command_unlock(data); +} + +/** + * @brief Get the lighting command in progress + * @param data [in] dimmer data + * @return BACNET_LIGHTING_IN_PROGRESS - lighting command in progress + */ +BACNET_LIGHTING_IN_PROGRESS +lighting_command_in_progress_get(struct bacnet_lighting_command_data *data) +{ + BACNET_LIGHTING_IN_PROGRESS in_progress = BACNET_LIGHTING_IDLE; + + if (!data) { + return in_progress; + } + lighting_command_lock(data); + in_progress = data->In_Progress; + lighting_command_unlock(data); + + return in_progress; +} + +/** + * @brief Set the lighting command in progress + * @param data [in] dimmer data + * @param in_progress [in] BACnet lighting in progress + */ +void lighting_command_in_progress_set( + struct bacnet_lighting_command_data *data, + BACNET_LIGHTING_IN_PROGRESS in_progress) +{ + if (!data) { + return; + } + lighting_command_lock(data); + data->In_Progress = in_progress; + lighting_command_unlock(data); +} + +/** + * @brief Get the lighting command tracking value + * @param data [in] dimmer data + * @return float - lighting command tracking value + */ +float lighting_command_tracking_value_get( + struct bacnet_lighting_command_data *data) +{ + float value = 0.0f; + + if (!data) { + return value; + } + lighting_command_lock(data); + value = data->Tracking_Value; + lighting_command_unlock(data); + + return value; +} + +/** + * @brief Set the lighting command tracking value + * @param data [in] dimmer data + * @param value [in] BACnet lighting tracking value + */ +void lighting_command_tracking_value_set( + struct bacnet_lighting_command_data *data, float value) +{ + if (!data) { + return; + } + lighting_command_lock(data); + data->Tracking_Value = value; + lighting_command_unlock(data); +} + +void lighting_command_blink_warn_feature_set( + struct bacnet_lighting_command_data *data, + float off_value, + uint16_t interval, + uint16_t count) +{ + if (!data) { + return; + } + lighting_command_lock(data); + data->Blink.Off_Value = off_value; + data->Blink.Interval = interval; + data->Blink.Count = count; + lighting_command_unlock(data); +} + +/** + * @brief Check if the lighting command blink egress is active + * @param data [in] dimmer data + * @return bool - true if the lighting command blink egress is active + */ +bool lighting_command_blink_egress_active( + struct bacnet_lighting_command_data *data) +{ + bool active = false; + + if (!data) { + return active; + } + lighting_command_lock(data); + active = data->Blink.Duration > 0; + lighting_command_unlock(data); + + return active; +} + +/** + * @brief Get the lighting command out of service + * @param data [in] dimmer data + * @return bool - true if the lighting command is out of service + */ +bool lighting_command_out_of_service_get( + struct bacnet_lighting_command_data *data) +{ + bool value = false; + + if (!data) { + return value; + } + lighting_command_lock(data); + value = data->Out_Of_Service; + lighting_command_unlock(data); + + return value; +} + +/** + * @brief Set the lighting command out of service + * @param data [in] dimmer data + * @param value [in] BACnet lighting out of service + */ +void lighting_command_out_of_service_set( + struct bacnet_lighting_command_data *data, bool value) +{ + if (!data) { + return; + } + lighting_command_lock(data); + data->Out_Of_Service = value; + lighting_command_unlock(data); +} + +/** + * @brief Get the lighting command last on value + * @param data [in] dimmer data + * @return float - lighting command last on value + */ +float lighting_command_last_on_value_get( + struct bacnet_lighting_command_data *data) +{ + float value = 0.0f; + + if (!data) { + return value; + } + lighting_command_lock(data); + value = data->Last_On_Value; + lighting_command_unlock(data); + + return value; +} + +/** + * @brief Set the lighting command last on value + * @param data [in] dimmer data + * @param value [in] BACnet lighting last on value + */ +void lighting_command_last_on_value_set( + struct bacnet_lighting_command_data *data, float value) +{ + if (!data) { + return; + } + lighting_command_lock(data); + data->Last_On_Value = value; + lighting_command_unlock(data); +} + +/** + * @brief Get the lighting command default on value + * @param data [in] dimmer data + * @return float - lighting command default on value + */ +float lighting_command_default_on_value_get( + struct bacnet_lighting_command_data *data) +{ + float value = 0.0f; + + if (!data) { + return value; + } + lighting_command_lock(data); + value = data->Default_On_Value; + lighting_command_unlock(data); + + return value; +} + +/** + * @brief Get the lighting command Min_Actual_Value property value + * @param data [in] dimmer data + * @return float - lighting command Min_Actual_Value + */ +float lighting_command_min_actual_value_get( + struct bacnet_lighting_command_data *data) +{ + float value = 0.0f; + + if (!data) { + return value; + } + lighting_command_lock(data); + value = data->Min_Actual_Value; + lighting_command_unlock(data); + + return value; +} + +/** + * @brief Set the lighting command Min_Actual_Value property value + * @param data [in] dimmer data + * @param value [in] BACnet lighting Min_Actual_Value + */ +void lighting_command_min_actual_value_set( + struct bacnet_lighting_command_data *data, float value) +{ + if (!data) { + return; + } + lighting_command_lock(data); + data->Min_Actual_Value = value; + lighting_command_unlock(data); +} + +/** + * @brief Get the lighting command Max_Actual_Value property value + * @param data [in] dimmer data + * @return float - lighting command Max_Actual_Value + */ +float lighting_command_max_actual_value_get( + struct bacnet_lighting_command_data *data) +{ + float value = 0.0f; + + if (!data) { + return value; + } + lighting_command_lock(data); + value = data->Max_Actual_Value; + lighting_command_unlock(data); + + return value; } +/** + * @brief Set the lighting command Max_Actual_Value property value + * @param data [in] dimmer data + * @param value [in] BACnet lighting Max_Actual_Value + */ +void lighting_command_max_actual_value_set( + struct bacnet_lighting_command_data *data, float value) +{ + if (!data) { + return; + } + lighting_command_lock(data); + data->Max_Actual_Value = value; + lighting_command_unlock(data); +} + +/** + * @brief Set the lighting command default on value + * @param data [in] dimmer data + * @param value [in] BACnet lighting default on value + */ +void lighting_command_default_on_value_set( + struct bacnet_lighting_command_data *data, float value) +{ + if (!data) { + return; + } + lighting_command_lock(data); + data->Default_On_Value = value; + lighting_command_unlock(data); +} + +/** + * @brief Get the lighting command overridden status + * @param data [in] dimmer data + * @return bool - true if the lighting command is overridden + */ +bool lighting_command_overridden_status( + struct bacnet_lighting_command_data *data) +{ + bool status = false; + + if (!data) { + return status; + } + lighting_command_lock(data); + status = data->Overridden || data->Overridden_Momentary; + lighting_command_unlock(data); + + return status; +} + +/** + * @brief Get the lighting command feedback value + * @param data [in] dimmer data + * @return float - lighting command feedback value + */ +float lighting_command_feedback_value(struct bacnet_lighting_command_data *data) +{ + float feedback_value; + + if (!data) { + return 0.0f; + } + lighting_command_lock(data); + lighting_command_min_max_value_range_adjust(data); + feedback_value = lighting_command_normalized_to_physical_value( + data->Min_Actual_Value, data->Max_Actual_Value, data->Tracking_Value); + lighting_command_unlock(data); + + return feedback_value; +} + +/** + * @brief Determine if fade, ramp, or warn command is currently executing + * @param pObject [in] object to apply the trim values to + * @param priority [in] priority of the command + */ +bool lighting_command_active(struct bacnet_lighting_command_data *data) +{ + bool in_progress = false; + + if (!data) { + return in_progress; + } + lighting_command_lock(data); + if ((data->In_Progress == BACNET_LIGHTING_FADE_ACTIVE) || + (data->In_Progress == BACNET_LIGHTING_RAMP_ACTIVE) || + (data->Blink.Duration > 0)) { + in_progress = true; + } + lighting_command_unlock(data); + + return in_progress; +} + +/** + * @brief Locks the lighting command for exclusive access + * @param data [in] dimmer data + */ +void lighting_command_lock(struct bacnet_lighting_command_data *data) +{ + if (!data) { + return; + } + if (data->Lock) { + data->Lock(data); + } +} + +/** + * @brief Unlocks the lighting command for exclusive access + * @param data [in] dimmer data + */ +void lighting_command_unlock(struct bacnet_lighting_command_data *data) +{ + if (!data) { + return; + } + if (data->Unlock) { + data->Unlock(data); + } +} + +/** + * @brief Initializes the lighting command data structure to default values + */ void lighting_command_init(struct bacnet_lighting_command_data *data) { if (!data) { return; } + lighting_command_lock(data); data->Tracking_Value = 0.0f; data->Lighting_Operation = BACNET_LIGHTS_NONE; data->In_Progress = BACNET_LIGHTING_NOT_CONTROLLED; @@ -1256,4 +1803,7 @@ void lighting_command_init(struct bacnet_lighting_command_data *data) data->Blink.State = false; data->Notification_Head.next = NULL; data->Notification_Head.callback = NULL; + data->Timer_Notification_Head.next = NULL; + data->Timer_Notification_Head.callback = NULL; + lighting_command_unlock(data); } diff --git a/src/bacnet/basic/sys/lighting_command.h b/src/bacnet/basic/sys/lighting_command.h index d8babfd39a..9c3980ea7f 100644 --- a/src/bacnet/basic/sys/lighting_command.h +++ b/src/bacnet/basic/sys/lighting_command.h @@ -13,10 +13,10 @@ #include "bacnet/bacdef.h" /** - * @brief Callback for tracking value updates + * @brief Callback for lighting command notifications * @param key - key used to link to specific light - * @param old_value - value prior to write - * @param value - value of the write + * @param old_value - physical value prior to write + * @param value - physical value of the write */ typedef void (*lighting_command_tracking_value_callback)( uint32_t key, float old_value, float value); @@ -41,6 +41,13 @@ struct lighting_command_timer_notification { lighting_command_timer_callback callback; }; +/** + * @brief Callback for locking shared state during lighting command processing + * @param data - dimmer data structure + */ +typedef void (*lighting_command_lock_callback)( + struct bacnet_lighting_command_data *); + /** * @brief Callback that manipulates the value at the specified priority slot after a delay of Egress_Time seconds. @@ -91,6 +98,10 @@ typedef struct bacnet_lighting_command_data { uint32_t Key; struct lighting_command_notification Notification_Head; struct lighting_command_timer_notification Timer_Notification_Head; + /* lock for accessing shared state */ + lighting_command_lock_callback Lock; + lighting_command_lock_callback Unlock; + void *Context; } BACNET_LIGHTING_COMMAND_DATA; #ifdef __cplusplus @@ -117,6 +128,10 @@ void lighting_command_blink_warn( BACNET_LIGHTING_OPERATION operation, struct bacnet_lighting_command_warn_data *blink); BACNET_STACK_EXPORT +void lighting_command_blink_copy( + struct bacnet_lighting_command_data *data, + struct bacnet_lighting_command_warn_data *blink); +BACNET_STACK_EXPORT void lighting_command_stop(struct bacnet_lighting_command_data *data); BACNET_STACK_EXPORT void lighting_command_none(struct bacnet_lighting_command_data *data); @@ -150,22 +165,101 @@ BACNET_STACK_EXPORT float lighting_command_ramp_rate_clamp(float ramp_rate); BACNET_STACK_EXPORT float lighting_command_step_increment_clamp(float step_increment); -BACNET_STACK_EXPORT -float lighting_command_operating_range_clamp( - struct bacnet_lighting_command_data *data, float value); + BACNET_STACK_EXPORT float lighting_command_operating_range_clamp_fade( struct bacnet_lighting_command_data *data, float value, uint16_t milliseconds); BACNET_STACK_EXPORT -float lighting_command_normalized_range_clamp( +float lighting_command_normalized_range_clamp(float value); + +BACNET_STACK_EXPORT +float lighting_command_normalized_on_range_clamp(float value); +BACNET_STACK_EXPORT +float lighting_command_normalized_to_physical_value( + float min_value, float max_value, float normalized_value); +BACNET_STACK_EXPORT +float lighting_command_feedback_value( + struct bacnet_lighting_command_data *data); + +BACNET_STACK_EXPORT +void lighting_command_trim_set( + struct bacnet_lighting_command_data *data, + float High_End_Trim, + float Low_End_Trim, + uint32_t Trim_Fade_Time); +BACNET_STACK_EXPORT +void lighting_command_key_set( + struct bacnet_lighting_command_data *data, uint32_t key); +BACNET_STACK_EXPORT +void lighting_command_tracking_value_callback_set( + struct bacnet_lighting_command_data *data, + lighting_command_tracking_value_callback cb); +BACNET_STACK_EXPORT +BACNET_LIGHTING_IN_PROGRESS +lighting_command_in_progress_get(struct bacnet_lighting_command_data *data); +BACNET_STACK_EXPORT +void lighting_command_in_progress_set( + struct bacnet_lighting_command_data *data, + BACNET_LIGHTING_IN_PROGRESS in_progress); +BACNET_STACK_EXPORT +float lighting_command_tracking_value_get( + struct bacnet_lighting_command_data *data); +BACNET_STACK_EXPORT +void lighting_command_tracking_value_set( + struct bacnet_lighting_command_data *data, float value); +BACNET_STACK_EXPORT +void lighting_command_blink_warn_feature_set( + struct bacnet_lighting_command_data *data, + float off_value, + uint16_t interval, + uint16_t count); +BACNET_STACK_EXPORT +bool lighting_command_blink_egress_active( + struct bacnet_lighting_command_data *data); +BACNET_STACK_EXPORT +bool lighting_command_out_of_service_get( + struct bacnet_lighting_command_data *data); +BACNET_STACK_EXPORT +void lighting_command_out_of_service_set( + struct bacnet_lighting_command_data *data, bool value); +BACNET_STACK_EXPORT +float lighting_command_last_on_value_get( + struct bacnet_lighting_command_data *data); +BACNET_STACK_EXPORT +void lighting_command_last_on_value_set( + struct bacnet_lighting_command_data *data, float value); +BACNET_STACK_EXPORT +float lighting_command_default_on_value_get( + struct bacnet_lighting_command_data *data); +BACNET_STACK_EXPORT +void lighting_command_default_on_value_set( struct bacnet_lighting_command_data *data, float value); BACNET_STACK_EXPORT -float lighting_command_normalized_on_range_clamp( +bool lighting_command_overridden_status( + struct bacnet_lighting_command_data *data); + +BACNET_STACK_EXPORT +float lighting_command_min_actual_value_get( + struct bacnet_lighting_command_data *data); +BACNET_STACK_EXPORT +void lighting_command_min_actual_value_set( struct bacnet_lighting_command_data *data, float value); BACNET_STACK_EXPORT -float lighting_command_physical_range_clamp(float value); +float lighting_command_max_actual_value_get( + struct bacnet_lighting_command_data *data); +BACNET_STACK_EXPORT +void lighting_command_max_actual_value_set( + struct bacnet_lighting_command_data *data, float value); + +BACNET_STACK_EXPORT +bool lighting_command_active(struct bacnet_lighting_command_data *data); + +BACNET_STACK_EXPORT +void lighting_command_lock(struct bacnet_lighting_command_data *data); +BACNET_STACK_EXPORT +void lighting_command_unlock(struct bacnet_lighting_command_data *data); BACNET_STACK_EXPORT void lighting_command_refresh(struct bacnet_lighting_command_data *data); diff --git a/test/bacnet/basic/object/lo/src/main.c b/test/bacnet/basic/object/lo/src/main.c index e484c714cb..0640890d64 100644 --- a/test/bacnet/basic/object/lo/src/main.c +++ b/test/bacnet/basic/object/lo/src/main.c @@ -683,6 +683,32 @@ static void testLightingOutput(void) /* context get/set */ Lighting_Output_Context_Set( instance, Lighting_Output_Context_Get(instance)); + /* min-actual-value get/set */ + real_value = 5.0f; + status = Lighting_Output_Min_Actual_Value_Set(instance, real_value); + zassert_true(status, NULL); + test_real = Lighting_Output_Min_Actual_Value(instance); + zassert_true( + is_float_equal(test_real, real_value), "value=%f test=%f", real_value, + test_real); + real_value = 1.0f; + status = Lighting_Output_Min_Actual_Value_Set(instance, real_value); + zassert_true(status, NULL); + test_real = Lighting_Output_Min_Actual_Value(instance); + zassert_true(is_float_equal(test_real, real_value), NULL); + /* max-actual-value get/set */ + real_value = 95.0f; + status = Lighting_Output_Max_Actual_Value_Set(instance, real_value); + zassert_true(status, NULL); + test_real = Lighting_Output_Max_Actual_Value(instance); + zassert_true( + is_float_equal(test_real, real_value), "value=%f test=%f", real_value, + test_real); + real_value = 100.0f; + status = Lighting_Output_Max_Actual_Value_Set(instance, real_value); + zassert_true(status, NULL); + test_real = Lighting_Output_Max_Actual_Value(instance); + zassert_true(is_float_equal(test_real, real_value), NULL); /* out-of-bounds */ test_instance = Lighting_Output_Create(BACNET_MAX_INSTANCE + 1); zassert_equal(test_instance, BACNET_MAX_INSTANCE, NULL); @@ -695,6 +721,112 @@ static void testLightingOutput(void) return; } +/** + * @brief Test boundary and relationship behavior for Min/Max Actual Value + * + * Verifies: + * - values outside 1.0..100.0 are rejected + * - exact boundary values 1.0 and 100.0 are accepted + * - setting Min above Max clamps Min down to the current Max value + * - setting Max below Min forces Min down to the new Max value + * - calls on a non-existent instance fail gracefully + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(lo_tests, testLightingOutputMinMaxActualValue) +#else +static void testLightingOutputMinMaxActualValue(void) +#endif +{ + const uint32_t instance = 400; + bool status; + float test_real; + + Lighting_Output_Init(); + Lighting_Output_Create(instance); + + /* --- out-of-range rejection for Min_Actual_Value --- */ + status = Lighting_Output_Min_Actual_Value_Set(instance, 0.0f); + zassert_false(status, "Min=0.0 should be rejected (below 1.0)"); + status = Lighting_Output_Min_Actual_Value_Set(instance, -1.0f); + zassert_false(status, "Min=-1.0 should be rejected"); + status = Lighting_Output_Min_Actual_Value_Set(instance, 100.1f); + zassert_false(status, "Min=100.1 should be rejected (above 100.0)"); + status = Lighting_Output_Min_Actual_Value_Set(instance, 200.0f); + zassert_false(status, "Min=200.0 should be rejected"); + + /* --- out-of-range rejection for Max_Actual_Value --- */ + status = Lighting_Output_Max_Actual_Value_Set(instance, 0.0f); + zassert_false(status, "Max=0.0 should be rejected (below 1.0)"); + status = Lighting_Output_Max_Actual_Value_Set(instance, -1.0f); + zassert_false(status, "Max=-1.0 should be rejected"); + status = Lighting_Output_Max_Actual_Value_Set(instance, 100.1f); + zassert_false(status, "Max=100.1 should be rejected (above 100.0)"); + status = Lighting_Output_Max_Actual_Value_Set(instance, 200.0f); + zassert_false(status, "Max=200.0 should be rejected"); + + /* --- exact boundary values must be accepted --- */ + status = Lighting_Output_Min_Actual_Value_Set(instance, 1.0f); + zassert_true(status, "Min=1.0 (lower bound) should be accepted"); + status = Lighting_Output_Max_Actual_Value_Set(instance, 100.0f); + zassert_true(status, "Max=100.0 (upper bound) should be accepted"); + + /* --- relationship invariant: Min > Max clamps Min down to Max --- + * Per the implementation: when the requested Min exceeds the current Max, + * Min is clamped to the current Max value (Max is unchanged). */ + status = Lighting_Output_Max_Actual_Value_Set(instance, 50.0f); + zassert_true(status, NULL); + status = Lighting_Output_Min_Actual_Value_Set(instance, 1.0f); + zassert_true(status, NULL); + /* request Min=80 with Max=50: call succeeds, Min stored as 50 */ + status = Lighting_Output_Min_Actual_Value_Set(instance, 80.0f); + zassert_true(status, "Min=80 > Max=50 should succeed (clamped)"); + test_real = Lighting_Output_Min_Actual_Value(instance); + zassert_true( + is_float_equal(test_real, 50.0f), + "Min should be clamped to Max(50.0), got %f", test_real); + test_real = Lighting_Output_Max_Actual_Value(instance); + zassert_true( + is_float_equal(test_real, 50.0f), "Max should remain 50.0, got %f", + test_real); + + /* --- relationship invariant: Max < Min forces Min down to new Max --- + * Per the implementation: when the requested Max is below the current Min, + * Min is set to the new Max and then Max is set to the new Max. */ + status = Lighting_Output_Max_Actual_Value_Set(instance, 80.0f); + zassert_true(status, NULL); + status = Lighting_Output_Min_Actual_Value_Set(instance, 60.0f); + zassert_true(status, NULL); + /* request Max=30 with Min=60: call succeeds, both become 30 */ + status = Lighting_Output_Max_Actual_Value_Set(instance, 30.0f); + zassert_true(status, "Max=30 < Min=60 should succeed (forces Min down)"); + test_real = Lighting_Output_Max_Actual_Value(instance); + zassert_true( + is_float_equal(test_real, 30.0f), "Max should be 30.0, got %f", + test_real); + test_real = Lighting_Output_Min_Actual_Value(instance); + zassert_true( + is_float_equal(test_real, 30.0f), + "Min should be forced to new Max(30.0), got %f", test_real); + + /* --- non-existent instance fails gracefully --- */ + status = Lighting_Output_Min_Actual_Value_Set(instance + 1, 50.0f); + zassert_false(status, "Min set on non-existent instance should fail"); + status = Lighting_Output_Max_Actual_Value_Set(instance + 1, 50.0f); + zassert_false(status, "Max set on non-existent instance should fail"); + test_real = Lighting_Output_Min_Actual_Value(instance + 1); + zassert_true( + is_float_equal(test_real, 0.0f), + "Min get on non-existent instance should return 0.0"); + test_real = Lighting_Output_Max_Actual_Value(instance + 1); + zassert_true( + is_float_equal(test_real, 0.0f), + "Max get on non-existent instance should return 0.0"); + + status = Lighting_Output_Delete(instance); + zassert_true(status, NULL); + Lighting_Output_Cleanup(); +} + /** * @} */ @@ -708,7 +840,8 @@ void test_main(void) lo_tests, ztest_unit_test(testLightingOutput), ztest_unit_test(testLightingOutputWritablePropertyList), ztest_unit_test(testLightingOutputBlinkStop), - ztest_unit_test(testLightingOutputWarnRelinquishEgress)); + ztest_unit_test(testLightingOutputWarnRelinquishEgress), + ztest_unit_test(testLightingOutputMinMaxActualValue)); ztest_run_test_suite(lo_tests); } diff --git a/test/bacnet/basic/sys/lighting_command/src/main.c b/test/bacnet/basic/sys/lighting_command/src/main.c index 600ee19300..63f528ca3a 100644 --- a/test/bacnet/basic/sys/lighting_command/src/main.c +++ b/test/bacnet/basic/sys/lighting_command/src/main.c @@ -277,6 +277,18 @@ static void test_lighting_command_unit(void) zassert_true( is_float_equal(data.Last_On_Value, 100.0f), "last-on-value=%f", data.Last_On_Value); + /* off to off should not clamp to the minimum ON level */ + data.Tracking_Value = 0.0f; + Tracking_Value = data.Tracking_Value; + target_level = 0.0f; + lighting_command_fade_to(&data, target_level, fade_time); + milliseconds = fade_time / 2; + lighting_command_timer(&data, milliseconds); + zassert_true(data.In_Progress == BACNET_LIGHTING_IDLE, NULL); + zassert_true(is_float_equal(Tracking_Value, 0.0f), NULL); + zassert_true(is_float_equal(data.Tracking_Value, 0.0f), NULL); + zassert_true(data.Lighting_Operation == BACNET_LIGHTS_STOP, NULL); + zassert_true(is_float_equal(data.Last_On_Value, 100.0f), NULL); /* low trim */ data.Low_Trim_Value = 10.0f; target_level = 1.0f; @@ -610,6 +622,20 @@ static void test_lighting_command_unit(void) "Tracking_Value=%f", Tracking_Value); } } while (data.Lighting_Operation != BACNET_LIGHTS_STOP); + zassert_true( + is_float_equal(data.Last_On_Value, data.Max_Actual_Value), NULL); + /* off to off should not clamp to the minimum ON level */ + data.Tracking_Value = 0.0f; + Tracking_Value = data.Tracking_Value; + target_level = 0.0f; + milliseconds = 100; + ramp_rate = 1.0f; + lighting_command_ramp_to(&data, target_level, ramp_rate); + lighting_command_timer(&data, milliseconds); + zassert_true(data.In_Progress == BACNET_LIGHTING_IDLE, NULL); + zassert_true(is_float_equal(Tracking_Value, 0.0f), NULL); + zassert_true(is_float_equal(data.Tracking_Value, 0.0f), NULL); + zassert_true(data.Lighting_Operation == BACNET_LIGHTS_STOP, NULL); zassert_true( is_float_equal(data.Last_On_Value, data.Max_Actual_Value), NULL); From 4a43012d73373c57535f4cbf0c109954980406ad Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Wed, 6 May 2026 06:22:54 -0500 Subject: [PATCH 09/42] feat: get the local IPv4 gateway address and configure the Network Port object (#1335) --- CHANGELOG.md | 4 +- CMakeLists.txt | 2 +- ports/bsd/bip-init.c | 106 ++++++++++++++++++++++++++++++++++++ ports/linux/bip-init.c | 81 ++++++++++++++++++++++++++- ports/win32/bip-init.c | 64 ++++++++++++++++++++++ src/bacnet/datalink/bip.h | 3 + src/bacnet/datalink/dlenv.c | 9 +++ src/bacnet/version.h | 2 +- 8 files changed, 265 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed35dcc6b4..dbbefb2574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ The git repositories are hosted at the following sites: * * -## [1.5.1-rc1] - 2026-04-20 +## [1.5.1-rc2] - 2026-05-26 ### Security @@ -24,6 +24,8 @@ The git repositories are hosted at the following sites: ### Fixed +* Fixed Network Port object local IPv4 gateway address configuration for + Linux/BSD/Windows. (#1335) * Fixed lighting command update notifications to use scaled physical values using min/max actual value. (#1315) * Fixed lighting command off to off behavior. (#1314) diff --git a/CMakeLists.txt b/CMakeLists.txt index ae6270d1c7..bcaef5b2d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project( bacnet-stack - VERSION 1.5.1-rc1 + VERSION 1.5.1 LANGUAGES C) # diff --git a/ports/bsd/bip-init.c b/ports/bsd/bip-init.c index 9a3a5661ae..266cc888a6 100644 --- a/ports/bsd/bip-init.c +++ b/ports/bsd/bip-init.c @@ -8,6 +8,13 @@ #include /* for standard integer types uint8_t etc. */ #include /* for the standard bool type. */ #include +#include +#include +#include +#include +#include +#include +#include #include "bacnet/bacdcode.h" #include "bacnet/bacint.h" #include "bacnet/datalink/bip.h" @@ -32,6 +39,8 @@ static struct in_addr BIP_Broadcast_Addr; /* broadcast binding mechanism */ static bool BIP_Broadcast_Binding_Address_Override; static struct in_addr BIP_Broadcast_Binding_Address; +/* IP gateway - stored here in network byte order */ +static struct in_addr BIP_Gateway_Addr; /* point-to-point interface flag - uses the unicast socket for broadcast */ static bool BIP_Point_To_Point = false; /* enable debugging */ @@ -213,6 +222,21 @@ bool bip_get_broadcast_addr(BACNET_IP_ADDRESS *addr) return true; } +/** + * @brief Get the BACnet/IP default gateway address + * @param addr - network IPv4 address of the gateway + * @return true if a gateway address was found + */ +bool bip_get_gateway_addr(BACNET_IP_ADDRESS *addr) +{ + if (addr) { + memcpy(&addr->address[0], &BIP_Gateway_Addr.s_addr, 4); + addr->port = 0; + } + + return (BIP_Gateway_Addr.s_addr != 0); +} + /** * @brief Set the BACnet/IP subnet mask CIDR prefix * @return true if the subnet mask CIDR prefix is set @@ -576,6 +600,83 @@ int bip_set_broadcast_binding(const char *ip4_broadcast) return 0; } +/** + * @brief Get the default gateway address + * @param ifname [in] The interface name + * @param gateway [out] The gateway address + * @return 0 on success, else -1 + */ +static int bip_get_local_gateway(const char *ifname, struct in_addr *gateway) +{ + int mib[] = { CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_DUMP, 0 }; + size_t l; + char *p, *next, *lim; + struct rt_msghdr *rtm; + struct sockaddr *sa; + struct sockaddr_in *sin; + bool found = false; + unsigned int if_index = 0; + + if (ifname != NULL) { + if_index = if_nametoindex(ifname); + } + + if (sysctl(mib, 6, NULL, &l, NULL, 0) < 0) { + return -1; + } + if (l == 0) { + return -1; + } + p = malloc(l); + if (!p) { + return -1; + } + if (sysctl(mib, 6, p, &l, NULL, 0) < 0) { + free(p); + return -1; + } + lim = p + l; + for (next = p; next < lim; next += rtm->rtm_msglen) { + rtm = (struct rt_msghdr *)next; + sa = (struct sockaddr *)(rtm + 1); + /* Check if it's the default route (dst is 0.0.0.0) */ + if (sa->sa_family == AF_INET && + (if_index == 0 || rtm->rtm_index == if_index)) { + sin = (struct sockaddr_in *)sa; + if (sin->sin_addr.s_addr == 0) { + /* The gateway is the second sockaddr after the destination */ + unsigned int i; + char *cp = (char *)(rtm + 1); + for (i = 0; i < RTAX_MAX; i++) { + if (rtm->rtm_addrs & (1 << i)) { + sa = (struct sockaddr *)cp; + if (i == RTAX_GATEWAY && sa->sa_family == AF_INET) { + gateway->s_addr = + ((struct sockaddr_in *)sa)->sin_addr.s_addr; + found = true; + break; + } + /* advance pointer to next sockaddr */ + if (sa->sa_len > 0) { + cp += + ((sa->sa_len + sizeof(long) - 1) & + ~(sizeof(long) - 1)); + } else { + cp += sizeof(long); + } + } + } + } + } + if (found) { + break; + } + } + free(p); + + return found ? 0 : -1; +} + /** Gets the local IP address and local broadcast address from the system, * and saves it into the BACnet/IP data structures. * @@ -647,6 +748,10 @@ void bip_set_interface(const char *ifname) ntohs(BIP_Port)); fflush(stderr); } + /* setup local gateway address */ + if (BIP_Gateway_Addr.s_addr == 0) { + bip_get_local_gateway(ifname, &BIP_Gateway_Addr); + } } const char *bip_get_interface(void) @@ -814,6 +919,7 @@ void bip_cleanup(void) /* these were set non-zero during interface configuration */ BIP_Address.s_addr = 0; BIP_Broadcast_Addr.s_addr = 0; + BIP_Gateway_Addr.s_addr = 0; return; } diff --git a/ports/linux/bip-init.c b/ports/linux/bip-init.c index 4c347464f0..9b94ec257c 100644 --- a/ports/linux/bip-init.c +++ b/ports/linux/bip-init.c @@ -47,6 +47,8 @@ static struct in_addr BIP_Address; static struct in_addr BIP_Broadcast_Addr; /* IP netmask - stored here in network byte order */ static struct in_addr BIP_Netmask; +/* IP gateway - stored here in network byte order */ +static struct in_addr BIP_Gateway_Addr; /* broadcast binding mechanism */ static bool BIP_Broadcast_Binding_Address_Override; static struct in_addr BIP_Broadcast_Binding_Address; @@ -229,6 +231,25 @@ bool bip_get_broadcast_addr(BACNET_IP_ADDRESS *addr) return true; } +/** + * @brief Get the BACnet/IP default gateway address + * @param addr - network IPv4 address of the gateway + * @return true if a gateway address was found + * + * @note The gateway address is captured during ifname_default(), + * which is called from bip_init(). If bip_init() has not been + * called yet, the address will be zero. + */ +bool bip_get_gateway_addr(BACNET_IP_ADDRESS *addr) +{ + if (addr) { + memcpy(&addr->address[0], &BIP_Gateway_Addr.s_addr, 4); + addr->port = 0; + } + + return (BIP_Gateway_Addr.s_addr != 0); +} + /** * @brief Set the BACnet/IP subnet mask CIDR prefix * @return true if the subnet mask CIDR prefix is set @@ -725,9 +746,9 @@ static char *ifname_default(void) memset(rtInfo, 0, sizeof(struct route_info)); parseRoutes(nlMsg, rtInfo); printRoute(rtInfo); - if (BIP_Interface_Name[0] == 0) { - if ((rtInfo->dstAddr == 0) && (rtInfo->ifName[0] != 0)) { - /* default route */ + if ((rtInfo->dstAddr == 0) && (rtInfo->ifName[0] != 0)) { + /* default route */ + if (BIP_Interface_Name[0] == 0) { memcpy( BIP_Interface_Name, rtInfo->ifName, sizeof(BIP_Interface_Name)); @@ -771,6 +792,58 @@ int bip_set_broadcast_binding(const char *ip4_broadcast) return 0; } +/** + * @brief Find and set the default gateway for the interface via netlink + * @param ifname [in] The named interface + */ +static void bip_set_gateway(const char *ifname) +{ + struct nlmsghdr *nlMsg = NULL; + struct route_info *rtInfo = NULL; + char msgBuf[8192] = { 0 }; + int sock, len, msgSeq = 0; + + if (BIP_Gateway_Addr.s_addr != 0) { + return; + } + if ((sock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)) < 0) { + perror("Socket Creation: "); + return; + } + nlMsg = (struct nlmsghdr *)msgBuf; + nlMsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nlMsg->nlmsg_type = RTM_GETROUTE; + nlMsg->nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST; + nlMsg->nlmsg_seq = msgSeq++; + nlMsg->nlmsg_pid = getpid(); + + if (send(sock, nlMsg, nlMsg->nlmsg_len, 0) < 0) { + debug_fprintf(stderr, "BIP: Write To Socket Failed...\n"); + close(sock); + return; + } + if ((len = readNlSock(sock, msgBuf, sizeof(msgBuf), msgSeq, getpid())) < + 0) { + debug_fprintf(stderr, "BIP: Read From Socket Failed...\n"); + close(sock); + return; + } + + rtInfo = (struct route_info *)malloc(sizeof(struct route_info)); + for (; NLMSG_OK(nlMsg, len); nlMsg = NLMSG_NEXT(nlMsg, len)) { + memset(rtInfo, 0, sizeof(struct route_info)); + parseRoutes(nlMsg, rtInfo); + if ((rtInfo->dstAddr == 0) && (rtInfo->gateWay != 0)) { + if (ifname == NULL || strcmp(ifname, rtInfo->ifName) == 0) { + BIP_Gateway_Addr.s_addr = rtInfo->gateWay; + break; + } + } + } + free(rtInfo); + close(sock); +} + /** Gets the local IP address and local broadcast address from the system, * and saves it into the BACnet/IP data structures. * @@ -841,6 +914,7 @@ void bip_set_interface(const char *ifname) ntohs(BIP_Port)); fflush(stderr); } + bip_set_gateway(ifname); } const char *bip_get_interface(void) @@ -1007,6 +1081,7 @@ void bip_cleanup(void) BIP_Address.s_addr = 0; BIP_Broadcast_Addr.s_addr = 0; BIP_Netmask.s_addr = 0; + BIP_Gateway_Addr.s_addr = 0; return; } diff --git a/ports/win32/bip-init.c b/ports/win32/bip-init.c index 7329cabbec..fcbdd456e9 100644 --- a/ports/win32/bip-init.c +++ b/ports/win32/bip-init.c @@ -36,6 +36,8 @@ static struct in_addr BIP_Broadcast_Addr; /* broadcast binding mechanism */ static bool BIP_Broadcast_Binding_Address_Override; static struct in_addr BIP_Broadcast_Binding_Address; +/* IP gateway - stored here in network byte order */ +static struct in_addr BIP_Gateway_Addr; /* enable debugging */ static bool BIP_Debug; @@ -369,6 +371,21 @@ bool bip_get_broadcast_addr(BACNET_IP_ADDRESS *addr) return true; } +/** + * @brief Get the BACnet/IP default gateway address + * @param addr - network IPv4 address of the gateway + * @return true if a gateway address was found + */ +bool bip_get_gateway_addr(BACNET_IP_ADDRESS *addr) +{ + if (addr) { + memcpy(&addr->address[0], &BIP_Gateway_Addr.s_addr, 4); + addr->port = 0; + } + + return (BIP_Gateway_Addr.s_addr != 0); +} + /** * @brief Set the BACnet/IP subnet mask CIDR prefix * @return true if the subnet mask CIDR prefix is set @@ -673,6 +690,48 @@ static uint32_t getIpMaskForIpAddress(uint32_t ipAddress) return ipMask; } +/* returns the gateway in network byte order */ +static uint32_t getIpGatewayForIpAddress(uint32_t ipAddress) +{ + /* Allocate information for up to 16 NICs */ + IP_ADAPTER_INFO AdapterInfo[16]; + /* Save memory size of buffer */ + DWORD dwBufLen = sizeof(AdapterInfo); + uint32_t ipGateway = 0; + bool found = false; + + PIP_ADAPTER_INFO pAdapterInfo; + + /* GetAdapterInfo: + [out] buffer to receive data + [in] size of receive data buffer */ + DWORD dwStatus = GetAdaptersInfo(AdapterInfo, &dwBufLen); + if (dwStatus == ERROR_SUCCESS) { + /* Verify return value is valid, no buffer overflow + Contains pointer to current adapter info */ + pAdapterInfo = AdapterInfo; + + do { + IP_ADDR_STRING *pIpAddressInfo = &pAdapterInfo->IpAddressList; + do { + unsigned long adapterAddress = + inet_addr(pIpAddressInfo->IpAddress.String); + if (adapterAddress == ipAddress) { + ipGateway = + inet_addr(pAdapterInfo->GatewayList.IpAddress.String); + found = true; + } + pIpAddressInfo = pIpAddressInfo->Next; + } while (pIpAddressInfo && !found); + /* Progress through linked list */ + pAdapterInfo = pAdapterInfo->Next; + /* Terminate on last adapter */ + } while (pAdapterInfo && !found); + } + + return ipGateway; +} + /** * @brief Get the netmask of the BACnet/IP's interface via an ioctl() call. * @param netmask [out] The netmask, in host order. @@ -760,6 +819,10 @@ void bip_set_interface(const char *ifname) if (BIP_Broadcast_Addr.s_addr == 0) { set_broadcast_address(BIP_Address.s_addr); } + /* setup local gateway address */ + if (BIP_Gateway_Addr.s_addr == 0) { + BIP_Gateway_Addr.s_addr = getIpGatewayForIpAddress(BIP_Address.s_addr); + } } static SOCKET createSocket(const struct sockaddr_in *sin) @@ -940,6 +1003,7 @@ void bip_cleanup(void) /* these were set non-zero during interface configuration */ BIP_Address.s_addr = 0; BIP_Broadcast_Addr.s_addr = 0; + BIP_Gateway_Addr.s_addr = 0; return; } diff --git a/src/bacnet/datalink/bip.h b/src/bacnet/datalink/bip.h index 0c454cdeb7..0428393f44 100644 --- a/src/bacnet/datalink/bip.h +++ b/src/bacnet/datalink/bip.h @@ -93,6 +93,9 @@ bool bip_set_broadcast_addr(const BACNET_IP_ADDRESS *addr); BACNET_STACK_EXPORT bool bip_get_broadcast_addr(BACNET_IP_ADDRESS *addr); +BACNET_STACK_EXPORT +bool bip_get_gateway_addr(BACNET_IP_ADDRESS *addr); + BACNET_STACK_EXPORT bool bip_set_subnet_prefix(uint8_t prefix); diff --git a/src/bacnet/datalink/dlenv.c b/src/bacnet/datalink/dlenv.c index d1b95bdad4..c33ae95974 100644 --- a/src/bacnet/datalink/dlenv.c +++ b/src/bacnet/datalink/dlenv.c @@ -419,6 +419,7 @@ static void dlenv_network_port_bip_init(uint32_t instance) bvlc_set_global_address_for_nat(&addr); } } + /* IP Address */ bip_get_addr(&addr); prefix = bip_get_subnet_prefix(); if (Datalink_Debug) { @@ -434,6 +435,14 @@ static void dlenv_network_port_bip_init(uint32_t instance) addr.address[3]); Network_Port_IP_Subnet_Prefix_Set(instance, prefix); Network_Port_Link_Speed_Set(instance, 0.0); + /* IP Gateway */ + if (bip_get_gateway_addr(&addr)) { + Network_Port_IP_Gateway_Set( + instance, addr.address[0], addr.address[1], addr.address[2], + addr.address[3]); + } else { + Network_Port_IP_Gateway_Set(instance, 0, 0, 0, 0); + } #if BBMD_ENABLED bdt_table = bvlc_bdt_list(); fdt_table = bvlc_fdt_list(); diff --git a/src/bacnet/version.h b/src/bacnet/version.h index a569252b98..85e429fb68 100644 --- a/src/bacnet/version.h +++ b/src/bacnet/version.h @@ -15,7 +15,7 @@ #define BACNET_VERSION(x, y, z) (((x) << 16) + ((y) << 8) + (z)) #endif -#define BACNET_VERSION_TEXT "1.5.1-rc1" +#define BACNET_VERSION_TEXT "1.5.1-rc2" #define BACNET_VERSION_CODE BACNET_VERSION(1, 5, 1) #define BACNET_VERSION_MAJOR ((BACNET_VERSION_CODE >> 16) & 0xFF) #define BACNET_VERSION_MINOR ((BACNET_VERSION_CODE >> 8) & 0xFF) From a926e2a2016da6bf30ca537edc4687dca51e4009 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 11:08:16 -0500 Subject: [PATCH 10/42] Fix _WIN32_WINNT definition for IPv6 compatibility on Windows --- ports/win32/bip6.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ports/win32/bip6.c b/ports/win32/bip6.c index 34096a0ad4..19b3410a91 100644 --- a/ports/win32/bip6.c +++ b/ports/win32/bip6.c @@ -5,6 +5,14 @@ * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 * *********************************************************************/ +/* IPv6 and the API (if_nametoindex) inherently requires + Vista or later, so ensure the _WIN32_WINNT is set appropriately */ +#if defined(__MINGW32__) || defined(__MINGW64__) || defined(_WIN32) +#if !defined(_WIN32_WINNT) || (_WIN32_WINNT < 0x0600) +#undef _WIN32_WINNT +#define _WIN32_WINNT 0x0600 +#endif +#endif #include #include #include /* for standard integer types uint8_t etc. */ From 8afaa6da4c7eae687e97adb82463d010260f06a5 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 15:40:50 -0500 Subject: [PATCH 11/42] fix: add PROP_ACCESS_DOORS to Properties_BACnetARRAY --- CHANGELOG.md | 1 + src/bacnet/proplist.c | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbbefb2574..759a83e5dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ The git repositories are hosted at the following sites: ### Fixed +* Fixed access-doors array to be in array list. (#1331) * Fixed Network Port object local IPv4 gateway address configuration for Linux/BSD/Windows. (#1335) * Fixed lighting command update notifications to use scaled physical diff --git a/src/bacnet/proplist.c b/src/bacnet/proplist.c index 6cd272e288..92ebe1d436 100644 --- a/src/bacnet/proplist.c +++ b/src/bacnet/proplist.c @@ -385,6 +385,7 @@ static const int32_t Properties_BACnetARRAY[] = { PROP_ISSUER_CERTIFICATE_FILES, PROP_NEGATIVE_ACCESS_RULES, PROP_POSITIVE_ACCESS_RULES, + PROP_ACCESS_DOORS, #if (INT_MAX > 0xFFFF) PROP_SC_HUB_FUNCTION_ACCEPT_URIS, #endif From 0486d8a59a209a1524a0ce40faad04a20753c37b Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 15:58:24 -0500 Subject: [PATCH 12/42] Bugfix/reject invalid tag write property empty data (#1337) Harden WriteProperty handling across the stack by rejecting zero-length application payloads for non-list properties (returning ERROR_CODE_INVALID_TAG) and by adding defensive wp_data == NULL checks in many object *_Write_Property() handlers. Changes: * Add a regression test ensuring Device_Write_Property() rejects an empty application payload on a non-list property with ERROR_CODE_INVALID_TAG. * Update Device_Write_Property() implementations (core + several ports/variants) to reject empty payloads unless the target property is a BACnetLIST property. * Add wp_data == NULL guards in many object WriteProperty handlers and remove some redundant application_data_len == 0 early-returns. --- CHANGELOG.md | 4 +++ apps/piface/device.c | 16 +++++++++- ports/at91sam7s/device.c | 16 +++++++++- ports/bdk-atxx4-mstp/device.c | 15 +++++++++- ports/stm32f10x/device.c | 15 +++++++++- ports/stm32f4xx/device.c | 13 ++++++++ ports/xplained/device.c | 15 +++++++++- src/bacnet/basic/object/acc.c | 4 +++ src/bacnet/basic/object/access_credential.c | 4 +++ src/bacnet/basic/object/access_door.c | 4 +++ src/bacnet/basic/object/access_point.c | 4 +++ src/bacnet/basic/object/access_rights.c | 4 +++ src/bacnet/basic/object/access_user.c | 4 +++ src/bacnet/basic/object/access_zone.c | 4 +++ src/bacnet/basic/object/ai.c | 3 -- src/bacnet/basic/object/ao.c | 4 +++ src/bacnet/basic/object/auditlog.c | 4 +++ src/bacnet/basic/object/av.c | 3 -- src/bacnet/basic/object/bi.c | 4 +++ src/bacnet/basic/object/bitstring_value.c | 4 +-- src/bacnet/basic/object/blo.c | 4 +++ src/bacnet/basic/object/bo.c | 4 +++ src/bacnet/basic/object/calendar.c | 4 +++ src/bacnet/basic/object/channel.c | 4 +++ .../basic/object/client/device-client.c | 13 ++++++++ src/bacnet/basic/object/color_object.c | 4 +++ src/bacnet/basic/object/color_temperature.c | 4 +++ src/bacnet/basic/object/command.c | 5 ++++ .../basic/object/credential_data_input.c | 4 +++ src/bacnet/basic/object/csv.c | 5 +--- src/bacnet/basic/object/device.c | 13 ++++++++ src/bacnet/basic/object/iv.c | 4 +++ src/bacnet/basic/object/lc.c | 1 + src/bacnet/basic/object/loop.c | 4 +++ src/bacnet/basic/object/lsp.c | 4 +++ src/bacnet/basic/object/lsz.c | 4 +++ src/bacnet/basic/object/ms-input.c | 4 +++ src/bacnet/basic/object/mso.c | 4 +++ src/bacnet/basic/object/msv.c | 4 +++ src/bacnet/basic/object/nc.c | 4 +++ src/bacnet/basic/object/netport.c | 4 +++ src/bacnet/basic/object/osv.c | 4 +-- src/bacnet/basic/object/piv.c | 5 +--- src/bacnet/basic/object/program.c | 4 +++ src/bacnet/basic/object/schedule.c | 4 +++ src/bacnet/basic/object/structured_view.c | 4 +++ src/bacnet/basic/object/time_value.c | 4 +++ src/bacnet/basic/object/timer.c | 4 +++ src/bacnet/basic/object/trendlog.c | 5 +++- src/bacnet/basic/server/bacnet_device.c | 13 ++++++++ .../basic/server/bacnet_device/src/main.c | 30 ++++++++++++++++++- 51 files changed, 295 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 759a83e5dd..412b21649a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ The git repositories are hosted at the following sites: ### Fixed +* Fixed WriteProperty handling across the stack by rejecting zero-length + application payloads for non-list properties (returning + ERROR_CODE_INVALID_TAG) and by adding defensive wp_data == NULL checks + in many object *_Write_Property() handlers. (#1337) * Fixed access-doors array to be in array list. (#1331) * Fixed Network Port object local IPv4 gateway address configuration for Linux/BSD/Windows. (#1335) diff --git a/apps/piface/device.c b/apps/piface/device.c index 4b1edf08de..2c75b1a937 100644 --- a/apps/piface/device.c +++ b/apps/piface/device.c @@ -1750,6 +1750,10 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; /* Ever the pessamist! */ struct object_functions *pObject = NULL; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* initialize the default return values */ wp_data->error_class = ERROR_CLASS_OBJECT; wp_data->error_code = ERROR_CODE_UNKNOWN_OBJECT; @@ -1769,7 +1773,17 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) status = Device_Write_Property_Object_Name( wp_data, pObject->Object_Write_Property); } else { - status = pObject->Object_Write_Property(wp_data); + if ((wp_data->application_data_len == 0) && + !property_list_bacnet_list_member( + wp_data->object_type, wp_data->object_property)) { + /* only list properties can be written with + an empty application payload */ + wp_data->error_class = ERROR_CLASS_SERVICES; + wp_data->error_code = ERROR_CODE_INVALID_TAG; + status = false; + } else { + status = pObject->Object_Write_Property(wp_data); + } } } else { wp_data->error_class = ERROR_CLASS_PROPERTY; diff --git a/ports/at91sam7s/device.c b/ports/at91sam7s/device.c index f511fe5c30..a777d1c1de 100644 --- a/ports/at91sam7s/device.c +++ b/ports/at91sam7s/device.c @@ -259,6 +259,10 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; struct my_object_functions *pObject = NULL; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* initialize the default return values */ pObject = Device_Objects_Find_Functions(wp_data->object_type); if (pObject) { @@ -276,7 +280,17 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) status = Device_Write_Property_Object_Name( wp_data, pObject->Object_Write_Property); } else { - status = pObject->Object_Write_Property(wp_data); + if ((wp_data->application_data_len == 0) && + !property_list_bacnet_list_member( + wp_data->object_type, wp_data->object_property)) { + /* only list properties can be written with + an empty application payload */ + wp_data->error_class = ERROR_CLASS_SERVICES; + wp_data->error_code = ERROR_CODE_INVALID_TAG; + status = false; + } else { + status = pObject->Object_Write_Property(wp_data); + } } } else { if (Device_Objects_Property_List_Member(wp_data->object_type, diff --git a/ports/bdk-atxx4-mstp/device.c b/ports/bdk-atxx4-mstp/device.c index e8c8a8d8fb..043e51e14f 100644 --- a/ports/bdk-atxx4-mstp/device.c +++ b/ports/bdk-atxx4-mstp/device.c @@ -226,6 +226,10 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; struct my_object_functions *pObject = NULL; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* initialize the default return values */ pObject = Device_Objects_Find_Functions(wp_data->object_type); if (pObject) { @@ -243,7 +247,16 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) status = Device_Write_Property_Object_Name( wp_data, pObject->Object_Write_Property); } else { - status = pObject->Object_Write_Property(wp_data); + if ((wp_data->application_data_len == 0) && !property_list_bacnet_list_member( + wp_data->object_type, wp_data->object_property)) { + /* only list properties can be written with + an empty application payload */ + wp_data->error_class = ERROR_CLASS_SERVICES; + wp_data->error_code = ERROR_CODE_INVALID_TAG; + status = false; + } else { + status = pObject->Object_Write_Property(wp_data); + } } } else { if (Device_Objects_Property_List_Member(wp_data->object_type, diff --git a/ports/stm32f10x/device.c b/ports/stm32f10x/device.c index 9ee76510c9..d2897d9819 100644 --- a/ports/stm32f10x/device.c +++ b/ports/stm32f10x/device.c @@ -190,13 +190,26 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; struct my_object_functions *pObject = NULL; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* initialize the default return values */ pObject = Device_Objects_Find_Functions(wp_data->object_type); if (pObject) { if (pObject->Object_Valid_Instance && pObject->Object_Valid_Instance(wp_data->object_instance)) { if (pObject->Object_Write_Property) { - status = pObject->Object_Write_Property(wp_data); + if ((wp_data->application_data_len == 0) && !property_list_bacnet_list_member( + wp_data->object_type, wp_data->object_property)) { + /* only list properties can be written with + an empty application payload */ + wp_data->error_class = ERROR_CLASS_SERVICES; + wp_data->error_code = ERROR_CODE_INVALID_TAG; + status = false; + } else { + status = pObject->Object_Write_Property(wp_data); + } } else { wp_data->error_class = ERROR_CLASS_PROPERTY; wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; diff --git a/ports/stm32f4xx/device.c b/ports/stm32f4xx/device.c index 556f94ef70..d65d68121e 100644 --- a/ports/stm32f4xx/device.c +++ b/ports/stm32f4xx/device.c @@ -1269,6 +1269,10 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; struct my_object_functions *pObject = NULL; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* initialize the default return values */ wp_data->error_class = ERROR_CLASS_OBJECT; wp_data->error_code = ERROR_CODE_UNKNOWN_OBJECT; @@ -1287,6 +1291,15 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) if (wp_data->object_property == PROP_OBJECT_NAME) { status = Device_Write_Property_Object_Name( wp_data, pObject->Object_Write_Property); + } else if ( + (wp_data->application_data_len == 0) && + !property_list_bacnet_list_member( + wp_data->object_type, wp_data->object_property)) { + /* only list properties can be written with + an empty application payload */ + wp_data->error_class = ERROR_CLASS_SERVICES; + wp_data->error_code = ERROR_CODE_INVALID_TAG; + status = false; } else { status = pObject->Object_Write_Property(wp_data); } diff --git a/ports/xplained/device.c b/ports/xplained/device.c index c50224cb0c..03faad5ea1 100644 --- a/ports/xplained/device.c +++ b/ports/xplained/device.c @@ -136,13 +136,26 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; struct my_object_functions *pObject = NULL; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* initialize the default return values */ pObject = Device_Objects_Find_Functions(wp_data->object_type); if (pObject) { if (pObject->Object_Valid_Instance && pObject->Object_Valid_Instance(wp_data->object_instance)) { if (pObject->Object_Write_Property) { - status = pObject->Object_Write_Property(wp_data); + if ((wp_data->application_data_len == 0) && !property_list_bacnet_list_member( + wp_data->object_type, wp_data->object_property)) { + /* only list properties can be written with + an empty application payload */ + wp_data->error_class = ERROR_CLASS_SERVICES; + wp_data->error_code = ERROR_CODE_INVALID_TAG; + status = false; + } else { + status = pObject->Object_Write_Property(wp_data); + } } else { wp_data->error_class = ERROR_CLASS_PROPERTY; wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED; diff --git a/src/bacnet/basic/object/acc.c b/src/bacnet/basic/object/acc.c index 1ef6ed24ce..4b257ba2e2 100644 --- a/src/bacnet/basic/object/acc.c +++ b/src/bacnet/basic/object/acc.c @@ -579,6 +579,10 @@ bool Accumulator_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_known_array_property( wp_data->application_data, wp_data->application_data_len, &value, diff --git a/src/bacnet/basic/object/access_credential.c b/src/bacnet/basic/object/access_credential.c index 9b563c2367..0b1bed026b 100644 --- a/src/bacnet/basic/object/access_credential.c +++ b/src/bacnet/basic/object/access_credential.c @@ -367,6 +367,10 @@ bool Access_Credential_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) BACNET_APPLICATION_DATA_VALUE value = { 0 }; unsigned object_index = 0; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/access_door.c b/src/bacnet/basic/object/access_door.c index 67e665f0b0..5e23606e8f 100644 --- a/src/bacnet/basic/object/access_door.c +++ b/src/bacnet/basic/object/access_door.c @@ -524,6 +524,10 @@ bool Access_Door_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) BACNET_APPLICATION_DATA_VALUE value = { 0 }; unsigned object_index = 0; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/access_point.c b/src/bacnet/basic/object/access_point.c index 46394b2b02..855c56637a 100644 --- a/src/bacnet/basic/object/access_point.c +++ b/src/bacnet/basic/object/access_point.c @@ -358,6 +358,10 @@ bool Access_Point_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/access_rights.c b/src/bacnet/basic/object/access_rights.c index 69522a7bc3..3a23e0bbea 100644 --- a/src/bacnet/basic/object/access_rights.c +++ b/src/bacnet/basic/object/access_rights.c @@ -339,6 +339,10 @@ bool Access_Rights_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) BACNET_APPLICATION_DATA_VALUE value = { 0 }; unsigned object_index = 0; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/access_user.c b/src/bacnet/basic/object/access_user.c index e01d1f12d4..d09328f8bf 100644 --- a/src/bacnet/basic/object/access_user.c +++ b/src/bacnet/basic/object/access_user.c @@ -257,6 +257,10 @@ bool Access_User_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) BACNET_APPLICATION_DATA_VALUE value = { 0 }; unsigned object_index = 0; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/access_zone.c b/src/bacnet/basic/object/access_zone.c index 762b06f57b..f994e7b941 100644 --- a/src/bacnet/basic/object/access_zone.c +++ b/src/bacnet/basic/object/access_zone.c @@ -310,6 +310,10 @@ bool Access_Zone_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) BACNET_APPLICATION_DATA_VALUE value = { 0 }; unsigned object_index = 0; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/ai.c b/src/bacnet/basic/object/ai.c index c8b6c646a7..205641ae54 100644 --- a/src/bacnet/basic/object/ai.c +++ b/src/bacnet/basic/object/ai.c @@ -1195,9 +1195,6 @@ bool Analog_Input_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) if (wp_data == NULL) { return false; } - if (wp_data->application_data_len == 0) { - return false; - } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/ao.c b/src/bacnet/basic/object/ao.c index 2cbd2d1730..b46bd3f885 100644 --- a/src/bacnet/basic/object/ao.c +++ b/src/bacnet/basic/object/ao.c @@ -1185,6 +1185,10 @@ bool Analog_Output_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/auditlog.c b/src/bacnet/basic/object/auditlog.c index 2a8c21b5b1..ec55857f05 100644 --- a/src/bacnet/basic/object/auditlog.c +++ b/src/bacnet/basic/object/auditlog.c @@ -781,6 +781,10 @@ bool Audit_Log_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/av.c b/src/bacnet/basic/object/av.c index 48b3885635..f641b595ec 100644 --- a/src/bacnet/basic/object/av.c +++ b/src/bacnet/basic/object/av.c @@ -990,9 +990,6 @@ bool Analog_Value_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) if (wp_data == NULL) { return false; } - if (wp_data->application_data_len == 0) { - return false; - } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/bi.c b/src/bacnet/basic/object/bi.c index e4b3251540..7f65081e79 100644 --- a/src/bacnet/basic/object/bi.c +++ b/src/bacnet/basic/object/bi.c @@ -1128,6 +1128,10 @@ bool Binary_Input_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) BACNET_APPLICATION_DATA_VALUE value = { 0 }; struct object_data *pObject; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/bitstring_value.c b/src/bacnet/basic/object/bitstring_value.c index b626ae8355..abee2f9c4e 100644 --- a/src/bacnet/basic/object/bitstring_value.c +++ b/src/bacnet/basic/object/bitstring_value.c @@ -714,12 +714,10 @@ bool BitString_Value_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ if (wp_data == NULL) { return false; } - if (wp_data->application_data_len == 0) { - return false; - } /* Decode the some of the request. */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/blo.c b/src/bacnet/basic/object/blo.c index 1a0a5b7d42..e7d2d5be3b 100644 --- a/src/bacnet/basic/object/blo.c +++ b/src/bacnet/basic/object/blo.c @@ -1476,6 +1476,10 @@ bool Binary_Lighting_Output_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/bo.c b/src/bacnet/basic/object/bo.c index 70b0fb9867..96a6ec9a98 100644 --- a/src/bacnet/basic/object/bo.c +++ b/src/bacnet/basic/object/bo.c @@ -1195,6 +1195,10 @@ bool Binary_Output_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/calendar.c b/src/bacnet/basic/object/calendar.c index 3f800fb0c9..44d7c1db16 100644 --- a/src/bacnet/basic/object/calendar.c +++ b/src/bacnet/basic/object/calendar.c @@ -598,6 +598,10 @@ bool Calendar_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool pv_old; bool pv; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/channel.c b/src/bacnet/basic/object/channel.c index b490b5deae..220e57646f 100644 --- a/src/bacnet/basic/object/channel.c +++ b/src/bacnet/basic/object/channel.c @@ -1269,6 +1269,10 @@ bool Channel_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the first value of the request */ len = bacapp_decode_known_property( wp_data->application_data, wp_data->application_data_len, &value, diff --git a/src/bacnet/basic/object/client/device-client.c b/src/bacnet/basic/object/client/device-client.c index 6f29f6c7af..e0aa5d7c38 100644 --- a/src/bacnet/basic/object/client/device-client.c +++ b/src/bacnet/basic/object/client/device-client.c @@ -1387,6 +1387,10 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; /* Ever the pessimist! */ struct object_functions *pObject = NULL; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* initialize the default return values */ wp_data->error_class = ERROR_CLASS_OBJECT; wp_data->error_code = ERROR_CODE_UNKNOWN_OBJECT; @@ -1405,6 +1409,15 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) if (wp_data->object_property == PROP_OBJECT_NAME) { status = Device_Write_Property_Object_Name( wp_data, pObject->Object_Write_Property); + } else if ( + (wp_data->application_data_len == 0) && + !property_list_bacnet_list_member( + wp_data->object_type, wp_data->object_property)) { + /* only list properties can be written with + an empty application payload */ + wp_data->error_class = ERROR_CLASS_SERVICES; + wp_data->error_code = ERROR_CODE_INVALID_TAG; + status = false; } else { status = pObject->Object_Write_Property(wp_data); } diff --git a/src/bacnet/basic/object/color_object.c b/src/bacnet/basic/object/color_object.c index c98c630c82..a2aabeb019 100644 --- a/src/bacnet/basic/object/color_object.c +++ b/src/bacnet/basic/object/color_object.c @@ -1026,6 +1026,10 @@ bool Color_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int apdu_size = 0; const uint8_t *apdu = NULL; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ apdu = wp_data->application_data; apdu_size = wp_data->application_data_len; diff --git a/src/bacnet/basic/object/color_temperature.c b/src/bacnet/basic/object/color_temperature.c index be36f90766..f036996473 100644 --- a/src/bacnet/basic/object/color_temperature.c +++ b/src/bacnet/basic/object/color_temperature.c @@ -1464,6 +1464,10 @@ bool Color_Temperature_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int apdu_size = 0; const uint8_t *apdu = NULL; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ apdu = wp_data->application_data; apdu_size = wp_data->application_data_len; diff --git a/src/bacnet/basic/object/command.c b/src/bacnet/basic/object/command.c index 00c208a508..4694eeb3ba 100644 --- a/src/bacnet/basic/object/command.c +++ b/src/bacnet/basic/object/command.c @@ -508,6 +508,11 @@ bool Command_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) unsigned int object_index = 0; int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/credential_data_input.c b/src/bacnet/basic/object/credential_data_input.c index c2869abe05..38473043e0 100644 --- a/src/bacnet/basic/object/credential_data_input.c +++ b/src/bacnet/basic/object/credential_data_input.c @@ -371,6 +371,10 @@ bool Credential_Data_Input_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) BACNET_APPLICATION_DATA_VALUE value = { 0 }; unsigned object_index = 0; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/csv.c b/src/bacnet/basic/object/csv.c index 857f1773de..dcd50df3e2 100644 --- a/src/bacnet/basic/object/csv.c +++ b/src/bacnet/basic/object/csv.c @@ -743,13 +743,10 @@ bool CharacterString_Value_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) struct characterstring_object *pObject = NULL; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ if (wp_data == NULL) { return false; } - if (wp_data->application_data_len == 0) { - return false; - } - /* Decode the some of the request. */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/device.c b/src/bacnet/basic/object/device.c index 2854b34ef9..7b983fba1d 100644 --- a/src/bacnet/basic/object/device.c +++ b/src/bacnet/basic/object/device.c @@ -3471,6 +3471,10 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; /* Ever the pessimist! */ struct object_functions *pObject = NULL; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } Device_Backup_Failure_Timeout_Restart(); /* initialize the default return values */ wp_data->error_class = ERROR_CLASS_OBJECT; @@ -3496,6 +3500,15 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) wp_data->object_type, wp_data->object_instance, wp_data->object_property)) { status = Write_Property_Proprietary_Callback(wp_data); + } else if ( + (wp_data->application_data_len == 0) && + !property_list_bacnet_list_member( + wp_data->object_type, wp_data->object_property)) { + /* only list properties can be written with + an empty application payload */ + wp_data->error_class = ERROR_CLASS_SERVICES; + wp_data->error_code = ERROR_CODE_INVALID_TAG; + status = false; } else { status = pObject->Object_Write_Property(wp_data); } diff --git a/src/bacnet/basic/object/iv.c b/src/bacnet/basic/object/iv.c index a7466dbc7d..b791f0d481 100644 --- a/src/bacnet/basic/object/iv.c +++ b/src/bacnet/basic/object/iv.c @@ -577,6 +577,10 @@ bool Integer_Value_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int32_t old_value = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/lc.c b/src/bacnet/basic/object/lc.c index 60695801c7..006440e1ce 100644 --- a/src/bacnet/basic/object/lc.c +++ b/src/bacnet/basic/object/lc.c @@ -1534,6 +1534,7 @@ bool Load_Control_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0, count = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ if (wp_data == NULL) { debug_printf("Load_Control_Write_Property(): invalid data\n"); return false; diff --git a/src/bacnet/basic/object/loop.c b/src/bacnet/basic/object/loop.c index d0ed0150f2..70f3ea68ab 100644 --- a/src/bacnet/basic/object/loop.c +++ b/src/bacnet/basic/object/loop.c @@ -1620,6 +1620,10 @@ bool Loop_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_known_array_property( wp_data->application_data, wp_data->application_data_len, &value, diff --git a/src/bacnet/basic/object/lsp.c b/src/bacnet/basic/object/lsp.c index a9a799f45a..57de7a891e 100644 --- a/src/bacnet/basic/object/lsp.c +++ b/src/bacnet/basic/object/lsp.c @@ -602,6 +602,10 @@ bool Life_Safety_Point_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/lsz.c b/src/bacnet/basic/object/lsz.c index fe69e3143c..f12c313363 100644 --- a/src/bacnet/basic/object/lsz.c +++ b/src/bacnet/basic/object/lsz.c @@ -783,6 +783,10 @@ bool Life_Safety_Zone_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/ms-input.c b/src/bacnet/basic/object/ms-input.c index f575c42f0f..ba50fdb19a 100644 --- a/src/bacnet/basic/object/ms-input.c +++ b/src/bacnet/basic/object/ms-input.c @@ -883,6 +883,10 @@ bool Multistate_Input_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the first chunk of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/mso.c b/src/bacnet/basic/object/mso.c index 49f5634e55..a75788dad0 100644 --- a/src/bacnet/basic/object/mso.c +++ b/src/bacnet/basic/object/mso.c @@ -1194,6 +1194,10 @@ bool Multistate_Output_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the first chunk of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/msv.c b/src/bacnet/basic/object/msv.c index d6d9a45e73..73e4f6e339 100644 --- a/src/bacnet/basic/object/msv.c +++ b/src/bacnet/basic/object/msv.c @@ -885,6 +885,10 @@ bool Multistate_Value_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/nc.c b/src/bacnet/basic/object/nc.c index f98d923574..56f174f222 100644 --- a/src/bacnet/basic/object/nc.c +++ b/src/bacnet/basic/object/nc.c @@ -370,6 +370,10 @@ bool Notification_Class_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) uint8_t idx; int len = 0; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } CurrentNotify = &NC_Info[Notification_Class_Instance_To_Index( wp_data->object_instance)]; diff --git a/src/bacnet/basic/object/netport.c b/src/bacnet/basic/object/netport.c index 1997261ca1..284f2e3f16 100644 --- a/src/bacnet/basic/object/netport.c +++ b/src/bacnet/basic/object/netport.c @@ -4552,6 +4552,10 @@ bool Network_Port_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) uint32_t capacity; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } if (!Network_Port_Valid_Instance(wp_data->object_instance)) { wp_data->error_class = ERROR_CLASS_OBJECT; wp_data->error_code = ERROR_CODE_UNKNOWN_OBJECT; diff --git a/src/bacnet/basic/object/osv.c b/src/bacnet/basic/object/osv.c index b1f9d0eae3..aaca71adac 100644 --- a/src/bacnet/basic/object/osv.c +++ b/src/bacnet/basic/object/osv.c @@ -659,12 +659,10 @@ bool OctetString_Value_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ if (wp_data == NULL) { return false; } - if (wp_data->application_data_len == 0) { - return false; - } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/piv.c b/src/bacnet/basic/object/piv.c index 9153044486..45c253fdce 100644 --- a/src/bacnet/basic/object/piv.c +++ b/src/bacnet/basic/object/piv.c @@ -470,13 +470,10 @@ bool PositiveInteger_Value_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) BACNET_APPLICATION_DATA_VALUE value = { 0 }; POSITIVEINTEGER_VALUE_DESCR *pObject = NULL; + /* Valid data? */ if (wp_data == NULL) { return false; } - if (wp_data->application_data_len == 0) { - return false; - } - /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/program.c b/src/bacnet/basic/object/program.c index 1fbe69e426..c67aca9c79 100644 --- a/src/bacnet/basic/object/program.c +++ b/src/bacnet/basic/object/program.c @@ -937,6 +937,10 @@ bool Program_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/schedule.c b/src/bacnet/basic/object/schedule.c index 3b74ed16fe..420eda7c82 100644 --- a/src/bacnet/basic/object/schedule.c +++ b/src/bacnet/basic/object/schedule.c @@ -940,6 +940,10 @@ bool Schedule_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_known_array_property( wp_data->application_data, wp_data->application_data_len, &value, diff --git a/src/bacnet/basic/object/structured_view.c b/src/bacnet/basic/object/structured_view.c index 45b7d8d810..64d689fba6 100644 --- a/src/bacnet/basic/object/structured_view.c +++ b/src/bacnet/basic/object/structured_view.c @@ -1586,6 +1586,10 @@ bool Structured_View_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) BACNET_UNSIGNED_INTEGER array_size = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_known_array_property( wp_data->application_data, wp_data->application_data_len, &value, diff --git a/src/bacnet/basic/object/time_value.c b/src/bacnet/basic/object/time_value.c index f1b7bf3b65..5f40736402 100644 --- a/src/bacnet/basic/object/time_value.c +++ b/src/bacnet/basic/object/time_value.c @@ -669,6 +669,10 @@ bool Time_Value_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) BACNET_APPLICATION_DATA_VALUE value = { 0 }; int len = 0; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/object/timer.c b/src/bacnet/basic/object/timer.c index 7ee187deeb..4ee1afd64e 100644 --- a/src/bacnet/basic/object/timer.c +++ b/src/bacnet/basic/object/timer.c @@ -1941,6 +1941,10 @@ bool Timer_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* decode the some of the request */ len = bacapp_decode_known_array_property( wp_data->application_data, wp_data->application_data_len, &value, diff --git a/src/bacnet/basic/object/trendlog.c b/src/bacnet/basic/object/trendlog.c index 892e885462..00ddfe3c11 100644 --- a/src/bacnet/basic/object/trendlog.c +++ b/src/bacnet/basic/object/trendlog.c @@ -539,6 +539,10 @@ bool Trend_Log_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool bEffectiveEnable; int log_index; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } /* Pin down which log to look at */ log_index = Trend_Log_Instance_To_Index(wp_data->object_instance); if (log_index >= MAX_TREND_LOGS) { @@ -547,7 +551,6 @@ bool Trend_Log_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) return false; } CurrentLog = &LogInfo[log_index]; - /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); diff --git a/src/bacnet/basic/server/bacnet_device.c b/src/bacnet/basic/server/bacnet_device.c index ec55c922f3..f3d1afad92 100644 --- a/src/bacnet/basic/server/bacnet_device.c +++ b/src/bacnet/basic/server/bacnet_device.c @@ -3335,6 +3335,10 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; /* Ever the pessimist! */ struct object_functions *pObject = NULL; + /* Valid data? */ + if (wp_data == NULL) { + return false; + } Device_Backup_Failure_Timeout_Restart(); /* initialize the default return values */ wp_data->error_class = ERROR_CLASS_OBJECT; @@ -3360,6 +3364,15 @@ bool Device_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) wp_data->object_type, wp_data->object_instance, wp_data->object_property)) { status = Write_Property_Proprietary_Callback(wp_data); + } else if ( + (wp_data->application_data_len == 0) && + !property_list_bacnet_list_member( + wp_data->object_type, wp_data->object_property)) { + /* only list properties can be written with + an empty application payload */ + wp_data->error_class = ERROR_CLASS_SERVICES; + wp_data->error_code = ERROR_CODE_INVALID_TAG; + status = false; } else { status = pObject->Object_Write_Property(wp_data); } diff --git a/test/bacnet/basic/server/bacnet_device/src/main.c b/test/bacnet/basic/server/bacnet_device/src/main.c index 2c2796dcea..8e341cb7c9 100644 --- a/test/bacnet/basic/server/bacnet_device/src/main.c +++ b/test/bacnet/basic/server/bacnet_device/src/main.c @@ -464,6 +464,33 @@ static void testDevice(void) zassert_true(property_list.Required.count > 0, NULL); } } + +/** + * @brief Test Device_Write_Property with empty application payload + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(device_tests, test_Device_Write_Property_Empty_Payload) +#else +static void test_Device_Write_Property_Empty_Payload(void) +#endif +{ + BACNET_WRITE_PROPERTY_DATA wp_data = { 0 }; + bool status; + + Device_Init(NULL); + wp_data.object_type = OBJECT_DEVICE; + wp_data.object_instance = Device_Object_Instance_Number(); + /* Choose a property that is NOT a list property */ + wp_data.object_property = PROP_DESCRIPTION; + wp_data.application_data_len = 0; + + status = Device_Write_Property(&wp_data); + + zassert_false(status, NULL); + zassert_equal(wp_data.error_class, ERROR_CLASS_SERVICES, NULL); + zassert_equal(wp_data.error_code, ERROR_CODE_INVALID_TAG, NULL); +} + /** * @} */ @@ -475,7 +502,8 @@ void test_main(void) { ztest_test_suite( device_tests, ztest_unit_test(testDevice), - ztest_unit_test(test_Device_Data_Sharing)); + ztest_unit_test(test_Device_Data_Sharing), + ztest_unit_test(test_Device_Write_Property_Empty_Payload)); ztest_run_test_suite(device_tests); } From 24f18ebfb777cdceb3763274097f494c1dd7f98f Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 16:09:44 -0500 Subject: [PATCH 13/42] fix: update lighting command tests to use physical value scaling and add min/max actual value getters/setters --- .../basic/sys/lighting_command/src/main.c | 91 ++++++++++++------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/test/bacnet/basic/sys/lighting_command/src/main.c b/test/bacnet/basic/sys/lighting_command/src/main.c index 63f528ca3a..791dcd329d 100644 --- a/test/bacnet/basic/sys/lighting_command/src/main.c +++ b/test/bacnet/basic/sys/lighting_command/src/main.c @@ -235,15 +235,23 @@ static void test_lighting_command_unit(void) lighting_command_timer(&data, milliseconds); zassert_true(data.In_Progress == BACNET_LIGHTING_IDLE, NULL); - /* normalized range clamp testing */ + /* normalized range scaling to physical values */ data.Max_Actual_Value = 95.0f; data.Min_Actual_Value = 5.0f; - target_level = lighting_command_normalized_range_clamp(&data, 0.1f); + target_level = lighting_command_normalized_to_physical_value( + data.Min_Actual_Value, data.Max_Actual_Value, 0.1f); zassert_true(is_float_equal(target_level, 0.0f), NULL); - target_level = lighting_command_normalized_range_clamp(&data, 100.0f); + target_level = lighting_command_normalized_to_physical_value( + data.Min_Actual_Value, data.Max_Actual_Value, 100.1f); zassert_true(is_float_equal(target_level, data.Max_Actual_Value), NULL); - target_level = lighting_command_normalized_range_clamp(&data, 1.0f); - zassert_true(is_float_equal(target_level, data.Min_Actual_Value), NULL); + target_level = lighting_command_normalized_to_physical_value( + data.Min_Actual_Value, data.Max_Actual_Value, 50.0f); + zassert_true( + islessequal(target_level, data.Max_Actual_Value), "physical-value=%.2f", + target_level); + zassert_true( + isgreaterequal(target_level, data.Min_Actual_Value), + "physical-value=%.2f", target_level); data.Max_Actual_Value = 100.0f; data.Min_Actual_Value = 1.0f; @@ -306,8 +314,8 @@ static void test_lighting_command_unit(void) lighting_command_timer(&data, milliseconds); zassert_true(data.In_Progress == BACNET_LIGHTING_IDLE, NULL); zassert_true(is_float_equal(Tracking_Value, target_level), NULL); - data.Low_Trim_Value = data.Min_Actual_Value; zassert_true(is_float_equal(data.Last_On_Value, 1.0f), NULL); + data.Low_Trim_Value = 1.0f; /* high trim */ data.High_Trim_Value = 90.0f; target_level = 100.0f; @@ -317,8 +325,8 @@ static void test_lighting_command_unit(void) lighting_command_timer(&data, milliseconds); zassert_true(data.In_Progress == BACNET_LIGHTING_TRIM_ACTIVE, NULL); zassert_true(is_float_equal(Tracking_Value, data.High_Trim_Value), NULL); - data.High_Trim_Value = data.Max_Actual_Value; zassert_true(is_float_equal(data.Last_On_Value, target_level), NULL); + data.High_Trim_Value = 100.0f; /* trim fade time */ target_level = 80.0f; milliseconds = 10; @@ -408,21 +416,21 @@ static void test_lighting_command_unit(void) zassert_true(is_float_equal(target_step, 100.0f), NULL); /* physical range clamping */ - target_level = lighting_command_physical_range_clamp(0.0f); + target_level = lighting_command_normalized_range_clamp(0.0f); zassert_true(is_float_equal(target_level, 0.0f), NULL); - target_level = lighting_command_physical_range_clamp(0.5f); + target_level = lighting_command_normalized_range_clamp(0.5f); zassert_true(is_float_equal(target_level, 0.0f), NULL); - target_level = lighting_command_physical_range_clamp(0.9f); + target_level = lighting_command_normalized_range_clamp(0.9f); zassert_true(is_float_equal(target_level, 0.0f), NULL); - target_level = lighting_command_physical_range_clamp(1.0f); + target_level = lighting_command_normalized_range_clamp(1.0f); zassert_true(is_float_equal(target_level, 1.0f), NULL); - target_level = lighting_command_physical_range_clamp(50.0f); + target_level = lighting_command_normalized_range_clamp(50.0f); zassert_true(is_float_equal(target_level, 50.0f), NULL); - target_level = lighting_command_physical_range_clamp(100.0f); + target_level = lighting_command_normalized_range_clamp(100.0f); zassert_true(is_float_equal(target_level, 100.0f), NULL); - target_level = lighting_command_physical_range_clamp(100.1f); + target_level = lighting_command_normalized_range_clamp(100.1f); zassert_true(is_float_equal(target_level, 100.0f), NULL); - target_level = lighting_command_physical_range_clamp(150.0f); + target_level = lighting_command_normalized_range_clamp(150.0f); zassert_true(is_float_equal(target_level, 100.0f), NULL); /* step UP - inhibit ON */ @@ -483,7 +491,7 @@ static void test_lighting_command_unit(void) is_float_equal(data.Last_On_Value, data.Max_Actual_Value), NULL); /* step DOWN, not off */ target_step = 1.0f; - target_level = data.Min_Actual_Value + target_step; + target_level = 1.0f + target_step; milliseconds = 10; lighting_command_fade_to(&data, target_level, 0); lighting_command_timer(&data, milliseconds); @@ -492,18 +500,17 @@ static void test_lighting_command_unit(void) lighting_command_step(&data, BACNET_LIGHTS_STEP_DOWN, target_step); lighting_command_timer(&data, milliseconds); zassert_true(data.In_Progress == BACNET_LIGHTING_IDLE, NULL); - zassert_true(is_float_equal(Tracking_Value, data.Min_Actual_Value), NULL); - zassert_true( - is_float_equal(data.Last_On_Value, data.Min_Actual_Value), NULL); - /* clamp to min */ + zassert_true(is_float_equal(Tracking_Value, 1.0f), NULL); + zassert_true(is_float_equal(data.Last_On_Value, 1.0f), NULL); + /* clamp to min normal */ target_step = 100.0f; lighting_command_step(&data, BACNET_LIGHTS_STEP_DOWN, target_step); lighting_command_timer(&data, milliseconds); zassert_true(data.In_Progress == BACNET_LIGHTING_IDLE, NULL); - zassert_true(is_float_equal(Tracking_Value, data.Min_Actual_Value), NULL); + zassert_true(is_float_equal(Tracking_Value, 1.0f), NULL); /* step DOWN and off */ target_step = 100.0f; - target_level = data.Min_Actual_Value; + target_level = 1.0f; milliseconds = 10; lighting_command_fade_to(&data, target_level, 0); lighting_command_timer(&data, milliseconds); @@ -513,8 +520,7 @@ static void test_lighting_command_unit(void) lighting_command_timer(&data, milliseconds); zassert_true(data.In_Progress == BACNET_LIGHTING_IDLE, NULL); zassert_true(is_float_equal(Tracking_Value, 0.0f), NULL); - zassert_true( - is_float_equal(data.Last_On_Value, data.Min_Actual_Value), NULL); + zassert_true(is_float_equal(data.Last_On_Value, 1.0f), NULL); /* blink warn - immediate off */ data.Blink.Interval = 0; data.Blink.Duration = 0; @@ -574,8 +580,8 @@ static void test_lighting_command_unit(void) data.In_Progress == BACNET_LIGHTING_RAMP_ACTIVE, "In_Progress=%d", data.In_Progress); zassert_true( - isgreater(data.Tracking_Value, data.Min_Actual_Value), - "Tracking_Value=%f", Tracking_Value); + isgreater(data.Tracking_Value, 1.0f), "Tracking_Value=%f", + Tracking_Value); zassert_true( isless(data.Tracking_Value, data.Max_Actual_Value), "Tracking_Value=%f", Tracking_Value); @@ -583,7 +589,7 @@ static void test_lighting_command_unit(void) } while (data.Lighting_Operation != BACNET_LIGHTS_STOP); /* slower ramp down */ - target_level = data.Min_Actual_Value; + target_level = 1.0f; milliseconds = 33; ramp_rate = 0.1f; do { @@ -594,15 +600,14 @@ static void test_lighting_command_unit(void) data.In_Progress == BACNET_LIGHTING_RAMP_ACTIVE, "In_Progress=%d", data.In_Progress); zassert_true( - isgreater(data.Tracking_Value, data.Min_Actual_Value), - "Tracking_Value=%f", Tracking_Value); + isgreater(data.Tracking_Value, 1.0f), "Tracking_Value=%f", + Tracking_Value); zassert_true( isless(data.Tracking_Value, data.Max_Actual_Value), "Tracking_Value=%f", Tracking_Value); } } while (data.Lighting_Operation != BACNET_LIGHTS_STOP); - zassert_true( - is_float_equal(data.Last_On_Value, data.Min_Actual_Value), NULL); + zassert_true(is_float_equal(data.Last_On_Value, 1.0f), NULL); /* large elapsed timer - ramp up */ target_level = data.Max_Actual_Value; milliseconds = 2000; @@ -615,8 +620,8 @@ static void test_lighting_command_unit(void) data.In_Progress == BACNET_LIGHTING_RAMP_ACTIVE, "In_Progress=%d", data.In_Progress); zassert_true( - isgreater(data.Tracking_Value, data.Min_Actual_Value), - "Tracking_Value=%f", Tracking_Value); + isgreater(data.Tracking_Value, 1.0f), "Tracking_Value=%f", + Tracking_Value); zassert_true( isless(data.Tracking_Value, data.Max_Actual_Value), "Tracking_Value=%f", Tracking_Value); @@ -709,7 +714,27 @@ static void test_lighting_command_unit(void) lighting_command_timer(&data, milliseconds); zassert_equal(data.Lighting_Operation, BACNET_LIGHTS_PROPRIETARY_MAX, NULL); + /* min-actual-value get/set */ + lighting_command_min_actual_value_set(&data, 5.0f); + target_level = lighting_command_min_actual_value_get(&data); + zassert_true(is_float_equal(target_level, 5.0f), NULL); + lighting_command_min_actual_value_set(&data, 1.0f); + target_level = lighting_command_min_actual_value_get(&data); + zassert_true(is_float_equal(target_level, 1.0f), NULL); + /* max-actual-value get/set */ + lighting_command_max_actual_value_set(&data, 95.0f); + target_level = lighting_command_max_actual_value_get(&data); + zassert_true(is_float_equal(target_level, 95.0f), NULL); + lighting_command_max_actual_value_set(&data, 100.0f); + target_level = lighting_command_max_actual_value_get(&data); + zassert_true(is_float_equal(target_level, 100.0f), NULL); /* null check code coverage */ + lighting_command_min_actual_value_set(NULL, 1.0f); + target_level = lighting_command_min_actual_value_get(NULL); + zassert_true(is_float_equal(target_level, 0.0f), NULL); + lighting_command_max_actual_value_set(NULL, 100.0f); + target_level = lighting_command_max_actual_value_get(NULL); + zassert_true(is_float_equal(target_level, 0.0f), NULL); lighting_command_override_set(NULL, override_level); lighting_command_override_clear(NULL, override_level); lighting_command_override_momentary(NULL, override_level); From e9402daefc6146385f4f9c487daa12ce4c4f1137 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 10:07:16 -0500 Subject: [PATCH 14/42] Add unit target to Makefiles for running unit tests --- Makefile | 4 ++++ test/Makefile | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/Makefile b/Makefile index 10c03e5c25..6563023154 100644 --- a/Makefile +++ b/Makefile @@ -570,6 +570,10 @@ test: retest: $(MAKE) -s -j -C test retest +.PHONY: unit +unit: + $(MAKE) -s -j -C test unit + .PHONY: test-bsc test-bsc: $(MAKE) -s -C test clean diff --git a/test/Makefile b/test/Makefile index ea9c362b71..6e8bb6179a 100644 --- a/test/Makefile +++ b/test/Makefile @@ -64,6 +64,12 @@ retest: [ -d $(BUILD_DIR) ] && cd $(BUILD_DIR) && ctest $(CTEST_OPTIONS) [ -d $(BUILD_DIR) ] && $(MAKE) -C $(BUILD_DIR) lcov +.PHONY: unit +unit: + [ -d $(BUILD_DIR) ] || mkdir -p $(BUILD_DIR) + [ -d $(BUILD_DIR) ] && cd $(BUILD_DIR) && cmake .. && cd .. + [ -d $(BUILD_DIR) ] && cd $(BUILD_DIR) && ctest && cd .. + .PHONY: report report: [ -d $(BUILD_DIR) ] && cat $(BUILD_DIR)/Testing/Temporary/LastTest*.log From 46f4db6a79fb530bb842c7524b040ea62ab7f7a1 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 14 May 2026 14:40:59 -0500 Subject: [PATCH 15/42] Fix off-by-one error in Life_Safety_Point_Read_Property for accepted modes (#1349) --- src/bacnet/basic/object/lsp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bacnet/basic/object/lsp.c b/src/bacnet/basic/object/lsp.c index 57de7a891e..b931a1a649 100644 --- a/src/bacnet/basic/object/lsp.c +++ b/src/bacnet/basic/object/lsp.c @@ -570,7 +570,7 @@ int Life_Safety_Point_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) apdu_len = encode_application_enumerated(&apdu[0], mode); break; case PROP_ACCEPTED_MODES: - for (mode = 0; mode <= LIFE_SAFETY_MODE_RESERVED_MIN; mode++) { + for (mode = 0; mode < LIFE_SAFETY_MODE_RESERVED_MIN; mode++) { len = encode_application_enumerated(&apdu[apdu_len], mode); apdu_len += len; } From f7fa4e3c8cec7584f6141e36f5b2d6491bb9e3af Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 16:53:12 -0500 Subject: [PATCH 16/42] Uninitialized Value Use in AtomicReadFile-ACK (#1344) * fix: initialize BACNET data structures to prevent undefined behavior during decoding * fix: initialize octet strings to prevent uninitialized value decoding in ARF and AWF services * fix: add tests for oversized octet string handling in atomic read/write file services * fix: restrict record count in ARF and BACfile services to prevent out of bounds access * fix: truncate octet strings in bacfile_read_record_data function --- CHANGELOG.md | 7 ++ SECURITY.md | 74 +++++++------------ apps/perl/perl_bindings.c | 4 +- apps/piface/device.c | 2 +- apps/readfile/main.c | 2 +- ports/at91sam7s/device.c | 2 +- ports/bdk-atxx4-mstp/device.c | 2 +- ports/stm32f4xx/device.c | 2 +- src/bacnet/arf.c | 16 +++- src/bacnet/awf.c | 2 + src/bacnet/bacapp.c | 3 + src/bacnet/basic/object/bacfile.c | 71 +++++++++++++++--- .../basic/object/client/device-client.c | 2 +- src/bacnet/basic/object/device.c | 2 +- src/bacnet/basic/server/bacnet_device.c | 2 +- src/bacnet/basic/service/h_arf_a.c | 2 +- src/bacnet/basic/service/s_arfs.c | 8 +- src/bacnet/channel_value.c | 3 + src/bacnet/ihave.c | 1 + src/bacnet/whoami.c | 2 + src/bacnet/youare.c | 3 + test/bacnet/arf/src/main.c | 70 ++++++++++++++++++ test/bacnet/awf/src/main.c | 72 ++++++++++++++++++ test/bacnet/bacdcode/src/main.c | 4 +- 24 files changed, 281 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 412b21649a..bf40a0c243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ The git repositories are hosted at the following sites: ### Security +* Secured AtomicReadFile-ACK Record-Access Encoder by initializing + BACNET_CHARACTER_STRING and OCTET_STRING to prevent uninitialized + usage and conditional information disclosure. (#1344) +* Secured AtomicReadFile and AtomicWriteFile callbacks into bacfile.c + by adding null checks and fixing out-of-bounds read/write.(#1344) * Secured WriteProperty to Structured View subordinate-list that caused a NULL pointer dereference in bacnet_device_object_reference_decode(). (#1321) * Secured AtomicReadFile handler by implementing bounds checks for @@ -24,6 +29,8 @@ The git repositories are hosted at the following sites: ### Fixed +* Fixed off-by-one error in Life_Safety_Point_Read_Property for + accepted modes property. (#1349) * Fixed WriteProperty handling across the stack by rejecting zero-length application payloads for non-list properties (returning ERROR_CODE_INVALID_TAG) and by adding defensive wp_data == NULL checks diff --git a/SECURITY.md b/SECURITY.md index d117c4bd0b..feade4b5f7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,70 +3,52 @@ ## Supported Versions The following versions of the BACnet Stack C library are -currently being supported with security updates. +currently being supported with security updates in this branch: | Version | Supported | | ------- | ------------------ | -| 1.4.x | :white_check_mark: | -| 1.3.x | :white_check_mark: | -| 1.2.x | :white_check_mark: | -| 1.1.x | :white_check_mark: | -| 1.0.x | :white_check_mark: | +| 1.5.x | :white_check_mark: | +| 1.4.x | :x: | +| 1.3.x | :x: | +| 1.2.x | :x: | +| 1.1.x | :x: | +| 1.0.x | :x: | | 0.9.x | :x: | -| 0.8.x | :white_check_mark: | +| 0.8.x | :x: | | 0.7.x | :x: | | < 0.6.x | :x: | ## Coordinated Vulnerability Disclosure -From time to time a vulnerability is disclosed to [CVE](https://www.cve.org/) +Vulnerabilites are disclosed to [CVE](https://www.cve.org/) +or [GHSA](https://github.com/bacnet-stack/bacnet-stack/security/advisories?state=published) and a record is created to identify, define, and catalog publicly disclosed -cybersecurity vulnerabilities. +cybersecurity vulnerabilities. Here are the published vulnerability records: -Here are the known CVE records: +Here are the known CVE records for v1.5.x: -[CVE-2026-26264](https://www.cve.org/CVERecord?id=CVE-2026-26264) - -WriteProperty decoding length underflow leads to OOB read and crash -[GHSA-phjh-v45p-gmjj](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-phjh-v45p-gmjj) +[CVE-2026-46677](https://www.cve.org/CVERecord?id=CVE-2026-46677) - +Client-Side Out-of-Bounds Read in AtomicReadFile-ACK Record-Access Handling via RecordCount / fileData[] Mismatch +[GHSA-rv5h-cxwq-q3mh](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-rv5h-cxwq-q3mh) -[CVE-2026-21870](https://www.cve.org/CVERecord?id=CVE-2026-21870) - -Off-by-one Stack-based Buffer Overflow in tokenizer_string -[GHSA-pc83-wp6w-93mx](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-pc83-wp6w-93mx) +[CVE-2026-46676](https://www.cve.org/CVERecord?id=CVE-2026-46676) - +Uninitialized Value Use in AtomicReadFile-ACK Record-Access Encoder Causes Response Corruption and Conditional Information Disclosure +[GHSA-2fwp-32cj-g3x4](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-2fwp-32cj-g3x4) -[CVE-2026-21878](https://www.cve.org/CVERecord?id=CVE-2026-21878) - -Improper Limitation of a Pathname to a Restricted Directory -[GHSA-p8rx-c26w-545j](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-p8rx-c26w-545j) +[CVE-2026-46674](https://www.cve.org/CVERecord?id=CVE-2026-46674) - +Out-of-Bounds Read in AtomicWriteFile Record Decoder via Unbounded returnedRecordCount +[GHSA-8384-pwhh-cxjh](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8384-pwhh-cxjh) -[CVE-2025-66624](https://www.cve.org/CVERecord?id=CVE-2025-66624) - -BACnet-stack MS/TP reply matcher OOB read -[GHSA-8wgw-5h6x-qgqg](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8wgw-5h6x-qgqg) - -[CVE-2023-38341](https://www.cve.org/CVERecord?id=CVE-2023-38341) - -Multiple out-of-bounds accesses in bacerror code paths -[#81](https://sourceforge.net/p/bacnet/bugs/81/) - -[CVE-2023-38340](https://www.cve.org/CVERecord?id=CVE-2023-38340) - -Out of bounds accesses in bacnet_npdu_decode -[#80](https://sourceforge.net/p/bacnet/bugs/80/) - -[CVE-2023-38339](https://www.cve.org/CVERecord?id=CVE-2023-38339) - -Out of bounds jump in h_apdu.c:apdu_handler -[#79](https://sourceforge.net/p/bacnet/bugs/79/) - -[CVE-2019-12480](https://www.cve.org/CVERecord?id=CVE-2019-12480) - -Invalid read in bacserv when decoding alarm tags -[#62](https://sourceforge.net/p/bacnet/bugs/62/) - -[CVE-2018-10238](https://www.cve.org/CVERecord?id=CVE-2018-10238) - -Segmentation fault leading to denial of service -[#61](https://sourceforge.net/p/bacnet/bugs/61/) +[CVE-2026-45265](https://www.cve.org/CVERecord?id=CVE-2026-45265) - +Atomic-Read-File RecordCount Stack-Based Out-of-Bounds Write +[GHSA-v3gx-mwrp-xvh5](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-v3gx-mwrp-xvh5) ## Reporting a Vulnerability -Please use the "bugs" feature of Sourceforge.net to report a vulnerability, -where it will be tracked until it is resolved. -https://sourceforge.net/p/bacnet/bugs/ +Privately discuss, fix, and publish information about security +vulnerabilities in this library using Github Security Advisories: +https://github.com/bacnet-stack/bacnet-stack/security/advisories/new -Vulnerabilities can also be reported using "issues" at Github. +Alternatively, vulnerabilities can be reported using "issues" at Github. https://github.com/bacnet-stack/bacnet-stack/issues diff --git a/apps/perl/perl_bindings.c b/apps/perl/perl_bindings.c index 88d90e2b02..fef5b47fb5 100644 --- a/apps/perl/perl_bindings.c +++ b/apps/perl/perl_bindings.c @@ -245,7 +245,7 @@ static void AtomicReadFileAckHandler( BACNET_CONFIRMED_SERVICE_ACK_DATA *service_data) { int len = 0; - BACNET_ATOMIC_READ_FILE_DATA data; + BACNET_ATOMIC_READ_FILE_DATA data = { 0 }; if (address_match(&Target_Address, src) && (service_data->invoke_id == Request_Invoke_ID)) { @@ -295,7 +295,7 @@ static void My_Read_Property_Ack_Handler( BACNET_CONFIRMED_SERVICE_ACK_DATA *service_data) { int len = 0; - BACNET_READ_PROPERTY_DATA data; + BACNET_READ_PROPERTY_DATA data = { 0 }; if (address_match(&Target_Address, src) && (service_data->invoke_id == Request_Invoke_ID)) { diff --git a/apps/piface/device.c b/apps/piface/device.c index 2c75b1a937..29869a02b7 100644 --- a/apps/piface/device.c +++ b/apps/piface/device.c @@ -1687,7 +1687,7 @@ static bool Device_Write_Property_Object_Name( { bool status = false; /* return value */ int len = 0; - BACNET_CHARACTER_STRING value; + BACNET_CHARACTER_STRING value = { 0 }; BACNET_OBJECT_TYPE object_type = OBJECT_NONE; uint32_t object_instance = 0; int apdu_size = 0; diff --git a/apps/readfile/main.c b/apps/readfile/main.c index dd0a96e4d2..ce2d422416 100644 --- a/apps/readfile/main.c +++ b/apps/readfile/main.c @@ -97,7 +97,7 @@ static void AtomicReadFileAckHandler( { int len = 0; int result = 0; - BACNET_ATOMIC_READ_FILE_DATA data; + BACNET_ATOMIC_READ_FILE_DATA data = { 0 }; FILE *pFile = NULL; /* stream pointer */ size_t octets_written = 0; size_t octet_count = 0; diff --git a/ports/at91sam7s/device.c b/ports/at91sam7s/device.c index a777d1c1de..7c04bc2c69 100644 --- a/ports/at91sam7s/device.c +++ b/ports/at91sam7s/device.c @@ -205,7 +205,7 @@ static bool Device_Write_Property_Object_Name( { bool status = false; /* return value */ int len = 0; - BACNET_CHARACTER_STRING value; + BACNET_CHARACTER_STRING value = { 0 }; BACNET_OBJECT_TYPE object_type = OBJECT_NONE; uint32_t object_instance = 0; int apdu_size = 0; diff --git a/ports/bdk-atxx4-mstp/device.c b/ports/bdk-atxx4-mstp/device.c index 043e51e14f..b012c97f78 100644 --- a/ports/bdk-atxx4-mstp/device.c +++ b/ports/bdk-atxx4-mstp/device.c @@ -172,7 +172,7 @@ static bool Device_Write_Property_Object_Name( { bool status = false; /* return value */ int len = 0; - BACNET_CHARACTER_STRING value; + BACNET_CHARACTER_STRING value = { 0 }; BACNET_OBJECT_TYPE object_type = OBJECT_NONE; uint32_t object_instance = 0; int apdu_size = 0; diff --git a/ports/stm32f4xx/device.c b/ports/stm32f4xx/device.c index d65d68121e..0186d3da40 100644 --- a/ports/stm32f4xx/device.c +++ b/ports/stm32f4xx/device.c @@ -1206,7 +1206,7 @@ static bool Device_Write_Property_Object_Name( { bool status = false; /* return value */ int len = 0; - BACNET_CHARACTER_STRING value; + BACNET_CHARACTER_STRING value = { 0 }; BACNET_OBJECT_TYPE object_type = OBJECT_NONE; uint32_t object_instance = 0; int apdu_size = 0; diff --git a/src/bacnet/arf.c b/src/bacnet/arf.c index ba8ff003b9..1bfe683325 100644 --- a/src/bacnet/arf.c +++ b/src/bacnet/arf.c @@ -334,6 +334,7 @@ int arf_ack_service_encode_apdu( int apdu_len = 0; /* total length of the apdu, return value */ int len = 0; uint32_t i = 0; + BACNET_UNSIGNED_INTEGER record_count = 0; /* endOfFile */ len = encode_application_boolean(apdu, data->endOfFile); @@ -374,13 +375,17 @@ int arf_ack_service_encode_apdu( if (apdu) { apdu += len; } - len = encode_application_unsigned( - apdu, data->type.record.RecordCount); + /* restrict the record count to the size of the fileData array */ + record_count = data->type.record.RecordCount; + if (record_count > ARRAY_SIZE(data->fileData)) { + record_count = ARRAY_SIZE(data->fileData); + } + len = encode_application_unsigned(apdu, record_count); apdu_len += len; if (apdu) { apdu += len; } - for (i = 0; i < data->type.record.RecordCount; i++) { + for (i = 0; i < record_count; i++) { len = encode_application_octet_string(apdu, &data->fileData[i]); apdu_len += len; if (apdu) { @@ -488,6 +493,7 @@ int arf_ack_decode_service_request( if (data) { octet_string = &data->fileData[0]; } + octetstring_init(octet_string, NULL, 0); len = bacnet_octet_string_application_decode( &apdu[apdu_len], apdu_size - apdu_len, octet_string); if (len <= 0) { @@ -521,6 +527,9 @@ int arf_ack_decode_service_request( if (len <= 0) { return BACNET_STATUS_ERROR; } + if (record_count > BACNET_READ_FILE_RECORD_COUNT) { + return BACNET_STATUS_ERROR; + } if (data) { data->type.record.RecordCount = record_count; } @@ -534,6 +543,7 @@ int arf_ack_decode_service_request( } else { octet_string = NULL; } + octetstring_init(octet_string, NULL, 0); len = bacnet_octet_string_application_decode( &apdu[apdu_len], apdu_size - apdu_len, octet_string); if (len <= 0) { diff --git a/src/bacnet/awf.c b/src/bacnet/awf.c index ad822974f3..76fdaf2bf4 100644 --- a/src/bacnet/awf.c +++ b/src/bacnet/awf.c @@ -226,6 +226,7 @@ int awf_decode_service_request( if (data) { octet_string = &data->fileData[0]; } + octetstring_init(octet_string, NULL, 0); len = bacnet_octet_string_application_decode( &apdu[apdu_len], apdu_size - apdu_len, octet_string); if (len <= 0) { @@ -274,6 +275,7 @@ int awf_decode_service_request( } else { octet_string = NULL; } + octetstring_init(octet_string, NULL, 0); len = bacnet_octet_string_application_decode( &apdu[apdu_len], apdu_size - apdu_len, octet_string); if (len <= 0) { diff --git a/src/bacnet/bacapp.c b/src/bacnet/bacapp.c index 7758950325..f4fa485935 100644 --- a/src/bacnet/bacapp.c +++ b/src/bacnet/bacapp.c @@ -1352,6 +1352,7 @@ int bacapp_decode_application_tag_value( #endif #if defined(BACAPP_OCTET_STRING) case BACNET_APPLICATION_TAG_OCTET_STRING: + octetstring_init(&value->type.Octet_String, NULL, 0); apdu_len = bacnet_octet_string_application_decode( apdu, apdu_size, &value->type.Octet_String); if (apdu_len == 0) { @@ -1363,6 +1364,7 @@ int bacapp_decode_application_tag_value( #endif #if defined(BACAPP_CHARACTER_STRING) case BACNET_APPLICATION_TAG_CHARACTER_STRING: + characterstring_init_ansi(&value->type.Character_String, ""); apdu_len = bacnet_character_string_application_decode( apdu, apdu_size, &value->type.Character_String); if (apdu_len == 0) { @@ -1374,6 +1376,7 @@ int bacapp_decode_application_tag_value( #endif #if defined(BACAPP_BIT_STRING) case BACNET_APPLICATION_TAG_BIT_STRING: + bitstring_init(&value->type.Bit_String); apdu_len = bacnet_bitstring_application_decode( apdu, apdu_size, &value->type.Bit_String); if (apdu_len == 0) { diff --git a/src/bacnet/basic/object/bacfile.c b/src/bacnet/basic/object/bacfile.c index a69b83d177..7c21ceb73e 100644 --- a/src/bacnet/basic/object/bacfile.c +++ b/src/bacnet/basic/object/bacfile.c @@ -1030,6 +1030,12 @@ uint32_t bacfile_instance_from_tsm(uint8_t invokeID) } #endif +/** + * @brief Read stream data from a file + * @param data - pointer to the data structure to fill + * @return true - if successful + * @return false - if failed or file not found + */ bool bacfile_read_stream_data(BACNET_ATOMIC_READ_FILE_DATA *data) { const char *pathname = NULL; @@ -1037,6 +1043,9 @@ bool bacfile_read_stream_data(BACNET_ATOMIC_READ_FILE_DATA *data) size_t len = 0; size_t requestedOctetCount = 0; + if (!data) { + return false; + } pathname = bacfile_pathname(data->object_instance); if (pathname) { found = true; @@ -1061,26 +1070,48 @@ bool bacfile_read_stream_data(BACNET_ATOMIC_READ_FILE_DATA *data) return found; } +/** + * @brief Read record data from a file + * @param data - pointer to the data structure to fill + * @return true - if successful + * @return false - if failed or file not found + */ bool bacfile_read_record_data(BACNET_ATOMIC_READ_FILE_DATA *data) { const char *pathname = NULL; bool found = false; bool status = false; + size_t len = 0; uint32_t i = 0; + size_t max_records = 0; + if (!data) { + return false; + } + max_records = + min(data->type.record.RecordCount, ARRAY_SIZE(data->fileData)); pathname = bacfile_pathname(data->object_instance); if (pathname) { found = true; - data->endOfFile = false; - for (i = 0; i < data->type.record.RecordCount; i++) { - status = bacfile_read_record_data_callback( - pathname, data->type.record.fileStartRecord, i, - octetstring_value(&data->fileData[i]), - octetstring_capacity(&data->fileData[i])); - if (!status) { - data->endOfFile = true; - data->type.record.RecordCount = i; - break; + if (max_records > 0) { + data->endOfFile = false; + for (i = 0; i < max_records; i++) { + status = bacfile_read_record_data_callback( + pathname, data->type.record.fileStartRecord, i, + octetstring_value(&data->fileData[i]), + octetstring_capacity(&data->fileData[i])); + if (status) { + /* our records are NULL terminated C strings + read with fgets() */ + len = bacnet_strnlen( + (const char *)octetstring_value(&data->fileData[i]), + octetstring_capacity(&data->fileData[i])); + octetstring_truncate(&data->fileData[i], len); + } else { + data->endOfFile = true; + data->type.record.RecordCount = i; + break; + } } } } @@ -1100,6 +1131,9 @@ bool bacfile_write_stream_data(BACNET_ATOMIC_WRITE_FILE_DATA *data) bool status = false; size_t bytes_written = 0; + if (!data) { + return false; + } if (bacfile_read_only(data->object_instance)) { /* if the file is read-only, then we cannot write to it */ return false; @@ -1135,11 +1169,17 @@ bool bacfile_write_record_data(const BACNET_ATOMIC_WRITE_FILE_DATA *data) const char *pathname = NULL; bool found = false; size_t i = 0; + size_t max_records = 0; + if (!data) { + return false; + } if (bacfile_read_only(data->object_instance)) { /* if the file is read-only, then we cannot write to it */ return false; } + max_records = + min(data->type.record.returnedRecordCount, ARRAY_SIZE(data->fileData)); pathname = bacfile_pathname(data->object_instance); if (pathname) { found = true; @@ -1148,7 +1188,7 @@ bool bacfile_write_record_data(const BACNET_ATOMIC_WRITE_FILE_DATA *data) as an append to the current end of file. If the 'File Start Record' parameter is 0, open the file as a clean slate. */ - for (i = 0; i < data->type.record.returnedRecordCount; i++) { + for (i = 0; i < max_records; i++) { bacfile_write_record_data_callback( pathname, data->type.record.fileStartRecord, i, octetstring_value((BACNET_OCTET_STRING *)&data->fileData[i]), @@ -1172,6 +1212,9 @@ bool bacfile_read_ack_stream_data( bool found = false; const char *pathname = NULL; + if (!data) { + return false; + } pathname = bacfile_pathname(instance); if (pathname) { found = true; @@ -1198,6 +1241,12 @@ bool bacfile_read_ack_record_data( const char *pathname = NULL; uint32_t i = 0; + if (!data) { + return false; + } + if (data->type.record.RecordCount > ARRAY_SIZE(data->fileData)) { + return false; + } pathname = bacfile_pathname(instance); if (pathname) { found = true; diff --git a/src/bacnet/basic/object/client/device-client.c b/src/bacnet/basic/object/client/device-client.c index e0aa5d7c38..01b2c31c59 100644 --- a/src/bacnet/basic/object/client/device-client.c +++ b/src/bacnet/basic/object/client/device-client.c @@ -1318,7 +1318,7 @@ static bool Device_Write_Property_Object_Name( { bool status = false; /* return value */ int len = 0; - BACNET_CHARACTER_STRING value; + BACNET_CHARACTER_STRING value = { 0 }; BACNET_OBJECT_TYPE object_type = OBJECT_NONE; uint32_t object_instance = 0; int apdu_size = 0; diff --git a/src/bacnet/basic/object/device.c b/src/bacnet/basic/object/device.c index 7b983fba1d..1d99dec96d 100644 --- a/src/bacnet/basic/object/device.c +++ b/src/bacnet/basic/object/device.c @@ -3389,7 +3389,7 @@ static bool Device_Write_Property_Object_Name( { bool status = false; /* return value */ int len = 0; - BACNET_CHARACTER_STRING value; + BACNET_CHARACTER_STRING value = { 0 }; BACNET_OBJECT_TYPE object_type = OBJECT_NONE; uint32_t object_instance = 0; int apdu_size = 0; diff --git a/src/bacnet/basic/server/bacnet_device.c b/src/bacnet/basic/server/bacnet_device.c index f3d1afad92..9ac72eeaca 100644 --- a/src/bacnet/basic/server/bacnet_device.c +++ b/src/bacnet/basic/server/bacnet_device.c @@ -3253,7 +3253,7 @@ static bool Device_Write_Property_Object_Name( { bool status = false; /* return value */ int len = 0; - BACNET_CHARACTER_STRING value; + BACNET_CHARACTER_STRING value = { 0 }; BACNET_OBJECT_TYPE object_type = OBJECT_NONE; uint32_t object_instance = 0; int apdu_size = 0; diff --git a/src/bacnet/basic/service/h_arf_a.c b/src/bacnet/basic/service/h_arf_a.c index 9af412eb00..60186c72d2 100644 --- a/src/bacnet/basic/service/h_arf_a.c +++ b/src/bacnet/basic/service/h_arf_a.c @@ -32,7 +32,7 @@ void handler_atomic_read_file_ack( BACNET_CONFIRMED_SERVICE_ACK_DATA *service_data) { int len = 0; - BACNET_ATOMIC_READ_FILE_DATA data; + BACNET_ATOMIC_READ_FILE_DATA data = { 0 }; uint32_t instance = 0; (void)src; diff --git a/src/bacnet/basic/service/s_arfs.c b/src/bacnet/basic/service/s_arfs.c index 5f4c36f757..2c206771e2 100644 --- a/src/bacnet/basic/service/s_arfs.c +++ b/src/bacnet/basic/service/s_arfs.c @@ -30,16 +30,16 @@ uint8_t Send_Atomic_Read_File_Stream( int fileStartPosition, unsigned requestedOctetCount) { - BACNET_ADDRESS dest; - BACNET_ADDRESS my_address; - BACNET_NPDU_DATA npdu_data; + BACNET_ADDRESS dest = { 0 }; + BACNET_ADDRESS my_address = { 0 }; + BACNET_NPDU_DATA npdu_data = { 0 }; unsigned max_apdu = 0; uint8_t invoke_id = 0; bool status = false; int len = 0; int pdu_len = 0; int bytes_sent = 0; - BACNET_ATOMIC_READ_FILE_DATA data; + BACNET_ATOMIC_READ_FILE_DATA data = { 0 }; /* if we are forbidden to send, don't send! */ if (!dcc_communication_enabled()) { diff --git a/src/bacnet/channel_value.c b/src/bacnet/channel_value.c index dcf17f25a0..2b337552c7 100644 --- a/src/bacnet/channel_value.c +++ b/src/bacnet/channel_value.c @@ -1290,18 +1290,21 @@ int bacnet_channel_value_no_coerce_decode( #endif #if defined(CHANNEL_OCTET_STRING) case BACNET_APPLICATION_TAG_OCTET_STRING: + octetstring_init(&value->type.Octet_String, NULL, 0); len = bacnet_octet_string_application_decode( apdu, apdu_size, &value->type.Octet_String); break; #endif #if defined(CHANNEL_CHARACTER_STRING) case BACNET_APPLICATION_TAG_CHARACTER_STRING: + characterstring_init_ansi(&value->type.Character_String, ""); len = bacnet_character_string_application_decode( apdu, apdu_size, &value->type.Character_String); break; #endif #if defined(CHANNEL_BIT_STRING) case BACNET_APPLICATION_TAG_BIT_STRING: + bitstring_init(&value->type.Bit_String); len = bacnet_bitstring_application_decode( apdu, apdu_size, &value->type.Bit_String); break; diff --git a/src/bacnet/ihave.c b/src/bacnet/ihave.c index 6202031f3f..10c55bf2e4 100644 --- a/src/bacnet/ihave.c +++ b/src/bacnet/ihave.c @@ -115,6 +115,7 @@ int ihave_decode_service_request( if (data) { decoded_string = &data->object_name; } + characterstring_init_ansi(decoded_string, ""); len = bacnet_character_string_application_decode( &apdu[apdu_len], apdu_size - apdu_len, decoded_string); if (len <= 0) { diff --git a/src/bacnet/whoami.c b/src/bacnet/whoami.c index 92bf35adb7..7174f5150d 100644 --- a/src/bacnet/whoami.c +++ b/src/bacnet/whoami.c @@ -134,6 +134,7 @@ int who_am_i_request_decode( return BACNET_STATUS_ERROR; } apdu_len += len; + characterstring_init_ansi(model_name, ""); len = bacnet_character_string_application_decode( &apdu[apdu_len], apdu_size - apdu_len, model_name); if (len > 0) { @@ -141,6 +142,7 @@ int who_am_i_request_decode( } else { return BACNET_STATUS_ERROR; } + characterstring_init_ansi(serial_number, ""); len = bacnet_character_string_application_decode( &apdu[apdu_len], apdu_size - apdu_len, serial_number); if (len > 0) { diff --git a/src/bacnet/youare.c b/src/bacnet/youare.c index b81c9905d3..c46b2a7f6f 100644 --- a/src/bacnet/youare.c +++ b/src/bacnet/youare.c @@ -171,6 +171,7 @@ int you_are_request_decode( return BACNET_STATUS_ERROR; } apdu_len += len; + characterstring_init_ansi(model_name, ""); len = bacnet_character_string_application_decode( &apdu[apdu_len], apdu_size - apdu_len, model_name); if (len > 0) { @@ -178,6 +179,7 @@ int you_are_request_decode( } else { return BACNET_STATUS_ERROR; } + characterstring_init_ansi(serial_number, ""); len = bacnet_character_string_application_decode( &apdu[apdu_len], apdu_size - apdu_len, serial_number); if (len > 0) { @@ -203,6 +205,7 @@ int you_are_request_decode( } else { return BACNET_STATUS_ERROR; } + octetstring_init(mac_address, NULL, 0); len = bacnet_octet_string_application_decode( &apdu[apdu_len], apdu_size - apdu_len, mac_address); if (len > 0) { diff --git a/test/bacnet/arf/src/main.c b/test/bacnet/arf/src/main.c index d60d0bfba5..a6371cc8a0 100644 --- a/test/bacnet/arf/src/main.c +++ b/test/bacnet/arf/src/main.c @@ -6,6 +6,7 @@ */ #include #include +#include /** * @addtogroup bacnet_tests @@ -154,6 +155,19 @@ static void testAtomicReadFileAccess(const BACNET_ATOMIC_READ_FILE_DATA *data) } } +static int +encode_application_octet_string_with_raw_length(uint8_t *apdu, uint32_t length) +{ + int apdu_len = 0; + + apdu_len = + encode_tag(apdu, BACNET_APPLICATION_TAG_OCTET_STRING, false, length); + memset(&apdu[apdu_len], 0xA5, length); + apdu_len += (int)length; + + return apdu_len; +} + #if defined(CONFIG_ZTEST_NEW_API) ZTEST(arf_tests, testAtomicReadFile) #else @@ -179,6 +193,61 @@ static void testAtomicReadFile(void) return; } +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(arf_tests, testAtomicReadFileAckOversizedOctetString) +#else +static void testAtomicReadFileAckOversizedOctetString(void) +#endif +{ + BACNET_ATOMIC_READ_FILE_DATA data; + uint8_t apdu[2048] = { 0 }; + uint32_t oversized_length = MAX_OCTET_STRING_BYTES + 1; + int apdu_len = 0; + int len = 0; + + zassert_true((oversized_length + 16U) < sizeof(apdu), NULL); + + memset(&data, 0xA5, sizeof(data)); + apdu_len = 0; + len = encode_application_boolean(&apdu[apdu_len], true); + apdu_len += len; + len = encode_opening_tag(&apdu[apdu_len], 0); + apdu_len += len; + len = encode_application_signed(&apdu[apdu_len], 1234); + apdu_len += len; + len = encode_application_octet_string_with_raw_length( + &apdu[apdu_len], oversized_length); + apdu_len += len; + len = encode_closing_tag(&apdu[apdu_len], 0); + apdu_len += len; + + len = arf_ack_decode_service_request(apdu, apdu_len, &data); + zassert_equal(len, apdu_len, NULL); + zassert_true(data.endOfFile, NULL); + zassert_equal(data.access, FILE_STREAM_ACCESS, NULL); + zassert_equal(data.type.stream.fileStartPosition, 1234, NULL); + zassert_equal(octetstring_length(&data.fileData[0]), 0, NULL); + + apdu_len = 0; + len = encode_application_boolean(&apdu[apdu_len], false); + apdu_len += len; + len = encode_opening_tag(&apdu[apdu_len], 0); + apdu_len += len; + len = encode_application_signed(&apdu[apdu_len], -17); + apdu_len += len; + len = encode_application_octet_string_with_raw_length(&apdu[apdu_len], 3); + apdu_len += len; + len = encode_closing_tag(&apdu[apdu_len], 0); + apdu_len += len; + + len = arf_ack_decode_service_request(apdu, apdu_len, &data); + zassert_equal(len, apdu_len, NULL); + zassert_false(data.endOfFile, NULL); + zassert_equal(data.access, FILE_STREAM_ACCESS, NULL); + zassert_equal(data.type.stream.fileStartPosition, -17, NULL); + zassert_equal(octetstring_length(&data.fileData[0]), 3, NULL); +} + #if defined(CONFIG_ZTEST_NEW_API) ZTEST(arf_tests, testAtomicReadFileMalformed) #else @@ -226,6 +295,7 @@ void test_main(void) ztest_test_suite( arf_tests, ztest_unit_test(testAtomicReadFile), ztest_unit_test(testAtomicReadFileAck), + ztest_unit_test(testAtomicReadFileAckOversizedOctetString), ztest_unit_test(testAtomicReadFileMalformed)); ztest_run_test_suite(arf_tests); diff --git a/test/bacnet/awf/src/main.c b/test/bacnet/awf/src/main.c index 39d0c0a24c..daa0837535 100644 --- a/test/bacnet/awf/src/main.c +++ b/test/bacnet/awf/src/main.c @@ -7,6 +7,7 @@ */ #include #include +#include /** * @addtogroup bacnet_tests @@ -143,6 +144,19 @@ testAtomicWriteFileAckAccess(const BACNET_ATOMIC_WRITE_FILE_DATA *data) } } +static int +encode_application_octet_string_with_raw_length(uint8_t *apdu, uint32_t length) +{ + int apdu_len = 0; + + apdu_len = + encode_tag(apdu, BACNET_APPLICATION_TAG_OCTET_STRING, false, length); + memset(&apdu[apdu_len], 0xA5, length); + apdu_len += (int)length; + + return apdu_len; +} + #if defined(CONFIG_ZTEST_NEW_API) ZTEST(awf_tests, testAtomicWriteFileAck) #else @@ -162,6 +176,63 @@ static void testAtomicWriteFileAck(void) return; } +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(awf_tests, testAtomicWriteFileOversizedOctetString) +#else +static void testAtomicWriteFileOversizedOctetString(void) +#endif +{ + BACNET_ATOMIC_WRITE_FILE_DATA data; + uint8_t apdu[2048] = { 0 }; + uint32_t oversized_length = MAX_OCTET_STRING_BYTES + 1; + int apdu_len = 0; + int len = 0; + + zassert_true((oversized_length + 24U) < sizeof(apdu), NULL); + + memset(&data, 0xA5, sizeof(data)); + apdu_len = 0; + len = encode_application_object_id(&apdu[apdu_len], OBJECT_FILE, 77); + apdu_len += len; + len = encode_opening_tag(&apdu[apdu_len], 0); + apdu_len += len; + len = encode_application_signed(&apdu[apdu_len], 321); + apdu_len += len; + len = encode_application_octet_string_with_raw_length( + &apdu[apdu_len], oversized_length); + apdu_len += len; + len = encode_closing_tag(&apdu[apdu_len], 0); + apdu_len += len; + + len = awf_decode_service_request(apdu, apdu_len, &data); + zassert_equal(len, apdu_len, NULL); + zassert_equal(data.object_type, OBJECT_FILE, NULL); + zassert_equal(data.object_instance, 77, NULL); + zassert_equal(data.access, FILE_STREAM_ACCESS, NULL); + zassert_equal(data.type.stream.fileStartPosition, 321, NULL); + zassert_equal(octetstring_length(&data.fileData[0]), 0, NULL); + + apdu_len = 0; + len = encode_application_object_id(&apdu[apdu_len], OBJECT_FILE, 99); + apdu_len += len; + len = encode_opening_tag(&apdu[apdu_len], 0); + apdu_len += len; + len = encode_application_signed(&apdu[apdu_len], -42); + apdu_len += len; + len = encode_application_octet_string_with_raw_length(&apdu[apdu_len], 3); + apdu_len += len; + len = encode_closing_tag(&apdu[apdu_len], 0); + apdu_len += len; + + len = awf_decode_service_request(apdu, apdu_len, &data); + zassert_equal(len, apdu_len, NULL); + zassert_equal(data.object_type, OBJECT_FILE, NULL); + zassert_equal(data.object_instance, 99, NULL); + zassert_equal(data.access, FILE_STREAM_ACCESS, NULL); + zassert_equal(data.type.stream.fileStartPosition, -42, NULL); + zassert_equal(octetstring_length(&data.fileData[0]), 3, NULL); +} + #if defined(CONFIG_ZTEST_NEW_API) ZTEST(awf_tests, testAtomicWriteFileMalformed) #else @@ -212,6 +283,7 @@ void test_main(void) ztest_test_suite( awf_tests, ztest_unit_test(testAtomicWriteFile), ztest_unit_test(testAtomicWriteFileAck), + ztest_unit_test(testAtomicWriteFileOversizedOctetString), ztest_unit_test(testAtomicWriteFileMalformed)); ztest_run_test_suite(awf_tests); diff --git a/test/bacnet/bacdcode/src/main.c b/test/bacnet/bacdcode/src/main.c index ac71fdc2dd..48a94c1da9 100644 --- a/test/bacnet/bacdcode/src/main.c +++ b/test/bacnet/bacdcode/src/main.c @@ -1190,8 +1190,8 @@ static void testBACDCodeCharacterString(void) { uint8_t apdu[MAX_APDU] = { 0 }; uint8_t encoded_apdu[MAX_APDU] = { 0 }; - BACNET_CHARACTER_STRING value; - BACNET_CHARACTER_STRING test_value; + BACNET_CHARACTER_STRING value = { 0 }; + BACNET_CHARACTER_STRING test_value = { 0 }; char test_name[MAX_APDU] = { "" }; int i; /* for loop counter */ int apdu_len = 0, len = 0, null_len = 0, tag_len, test_len; From 687c55b516bddcbeeff30b4829af081b4b719ccc Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 16:56:16 -0500 Subject: [PATCH 17/42] Channel member self-reference causes uncontrolled recursion (#1345) * Fix uncontrolled recursion in Channel_Write_Members and add self-reference check for channel members * Refactor Channel property handling to use device object property reference and unsigned value specific decoders * Fix handling of Write_Status in Channel_Write_Members to properly reflect success or failure of property writes --- CHANGELOG.md | 5 + SECURITY.md | 4 + src/bacnet/basic/object/channel.c | 234 +++++++++++++++++++----------- 3 files changed, 157 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf40a0c243..29b9c3b9c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ The git repositories are hosted at the following sites: ### Security +* Secured Channel object member self-reference that caused uncontrolled + recursion. Changed Channel property handling to use device object + property reference and unsigned value specific decoders. Fixed handling + of Write_Status in Channel_Write_Members to properly reflect success + or failure of property writes. (#1345) * Secured AtomicReadFile-ACK Record-Access Encoder by initializing BACNET_CHARACTER_STRING and OCTET_STRING to prevent uninitialized usage and conditional information disclosure. (#1344) diff --git a/SECURITY.md b/SECURITY.md index feade4b5f7..ac7a7557e7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -28,6 +28,10 @@ cybersecurity vulnerabilities. Here are the published vulnerability records: Here are the known CVE records for v1.5.x: +[CVE-2026-47217](https://www.cve.org/CVERecord?id=CVE-2026-47217) - +Channel member self-reference causes uncontrolled recursion and stack overflow in default BACnet/IP server +[GHSA-wjw5-q9g6-2764](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-wjw5-q9g6-2764) + [CVE-2026-46677](https://www.cve.org/CVERecord?id=CVE-2026-46677) - Client-Side Out-of-Bounds Read in AtomicReadFile-ACK Record-Access Handling via RecordCount / fileData[] Mismatch [GHSA-rv5h-cxwq-q3mh](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-rv5h-cxwq-q3mh) diff --git a/src/bacnet/basic/object/channel.c b/src/bacnet/basic/object/channel.c index 220e57646f..b04335293a 100644 --- a/src/bacnet/basic/object/channel.c +++ b/src/bacnet/basic/object/channel.c @@ -717,71 +717,116 @@ static bool Channel_Write_Members( unsigned m = 0; const BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMember = NULL; - if (pObject && value) { - pObject->Write_Status = BACNET_WRITE_STATUS_IN_PROGRESS; - debug_printf( - "channel[%lu].Channel_Write_Members\n", - (unsigned long)object_instance); - - for (m = 0; m < CHANNEL_MEMBERS_MAX; m++) { - pMember = &pObject->Members[m]; - /* NOTE: our implementation is for internal objects only */ - /* NOTE: we could check to match our Device ID, but then - we would need to update all channels when our device ID - changed. Instead, we'll just screen when members are - set. */ - if ((pMember->deviceIdentifier.type == OBJECT_DEVICE) && - (pMember->deviceIdentifier.instance != BACNET_MAX_INSTANCE) && - (pMember->objectIdentifier.instance != BACNET_MAX_INSTANCE)) { - wp_data.object_type = pMember->objectIdentifier.type; - wp_data.object_instance = pMember->objectIdentifier.instance; - wp_data.object_property = pMember->propertyIdentifier; - wp_data.array_index = pMember->arrayIndex; - wp_data.error_class = ERROR_CLASS_PROPERTY; - wp_data.error_code = ERROR_CODE_SUCCESS; - wp_data.priority = priority; - wp_data.application_data_len = sizeof(wp_data.application_data); - status = Channel_Write_Member_Value(&wp_data, value); - if (status) { - debug_printf( - "channel[%lu].Channel_Write_Member[%u] coerced\n", - (unsigned long)object_instance, m); - if (Write_Property_Internal_Callback) { - status = write_property_bacnet_array_valid(&wp_data); + if (!pObject) { + return false; + } + if (!value) { + return false; + } + if (pObject->Write_Status == BACNET_WRITE_STATUS_IN_PROGRESS) { + return false; + } + pObject->Write_Status = BACNET_WRITE_STATUS_IN_PROGRESS; + debug_printf( + "channel[%lu].Channel_Write_Members\n", (unsigned long)object_instance); + + for (m = 0; m < CHANNEL_MEMBERS_MAX; m++) { + pMember = &pObject->Members[m]; + /* NOTE: our implementation is for internal objects only */ + /* NOTE: we could check to match our Device ID, but then + we would need to update all channels when our device ID + changed. Instead, we'll just screen when members are + set. */ + if ((pMember->deviceIdentifier.type == OBJECT_DEVICE) && + (pMember->deviceIdentifier.instance != BACNET_MAX_INSTANCE) && + (pMember->objectIdentifier.instance != BACNET_MAX_INSTANCE)) { + wp_data.object_type = pMember->objectIdentifier.type; + wp_data.object_instance = pMember->objectIdentifier.instance; + wp_data.object_property = pMember->propertyIdentifier; + wp_data.array_index = pMember->arrayIndex; + wp_data.error_class = ERROR_CLASS_PROPERTY; + wp_data.error_code = ERROR_CODE_SUCCESS; + wp_data.priority = priority; + wp_data.application_data_len = sizeof(wp_data.application_data); + status = Channel_Write_Member_Value(&wp_data, value); + if (status) { + debug_printf( + "channel[%lu].Channel_Write_Member[%u] coerced\n", + (unsigned long)object_instance, m); + if (Write_Property_Internal_Callback) { + status = write_property_bacnet_array_valid(&wp_data); + if (status) { + status = Write_Property_Internal_Callback(&wp_data); if (status) { - status = Write_Property_Internal_Callback(&wp_data); - if (status) { - wp_data.error_code = ERROR_CODE_SUCCESS; - } + wp_data.error_code = ERROR_CODE_SUCCESS; } - debug_printf( - "channel[%lu].Channel_Write_Member[%u] " - "%s-%u %s %s\n", - (unsigned long)object_instance, m, - bactext_object_type_name(wp_data.object_type), - wp_data.object_instance, - bactext_property_name(wp_data.object_property), - bactext_error_code_name(wp_data.error_code)); } - } else { - wp_data.error_code = ERROR_CODE_PARAMETER_OUT_OF_RANGE; debug_printf( "channel[%lu].Channel_Write_Member[%u] " - "coercion failed!\n", - (unsigned long)object_instance, m); - pObject->Write_Status = BACNET_WRITE_STATUS_FAILED; + "%s-%u %s %s\n", + (unsigned long)object_instance, m, + bactext_object_type_name(wp_data.object_type), + wp_data.object_instance, + bactext_property_name(wp_data.object_property), + bactext_error_code_name(wp_data.error_code)); + if (!status) { + if ((bacnet_null_application_decode( + wp_data.application_data, + wp_data.application_data_len) > 0) && + ((wp_data.error_code == + ERROR_CODE_REJECT_INVALID_PARAMETER_DATA_TYPE) || + (wp_data.error_code == + ERROR_CODE_INVALID_DATA_TYPE))) { + /* A special exception shall be the writing of + a Null value. If a Null value is written and + WriteProperty or WritePropertyMultiple services + subsequently receive an ERROR_INVALID_DATATYPE or + REJECT_INVALID_PARAMETER_DATA_TYPE, + it shall not be treated as a FAILED value. + This is specifically to allow Channel objects + to point to both commandable and non-commandable + properties with the same channel.*/ + } else { + /* The FAILED value indicates that the Channel + object has processed a property in and received + an error, reject, or abort for at least one + of the writes. */ + pObject->Write_Status = BACNET_WRITE_STATUS_FAILED; + } + } + } else { + /* NOTE: internal callback not valid, + so ignore the writes and report no error */ } - Channel_Write_Property_Notify( - object_instance, status, &wp_data); } else { + /* coercion failed */ + wp_data.error_code = ERROR_CODE_PARAMETER_OUT_OF_RANGE; debug_printf( - "channel[%lu].Channel_Write_Member[%u] invalid!\n", + "channel[%lu].Channel_Write_Member[%u] " + "coercion failed!\n", (unsigned long)object_instance, m); + /* The FAILED value indicates that the Channel object + has processed all of the properties in + List_Of_Object_Property_References and + encountered a coercion failure, or received an error, + reject, or abort for at least one of the writes.*/ + pObject->Write_Status = BACNET_WRITE_STATUS_FAILED; } + Channel_Write_Property_Notify(object_instance, status, &wp_data); + } else { + debug_printf( + "channel[%lu].Channel_Write_Member[%u] invalid!\n", + (unsigned long)object_instance, m); } - if (pObject->Write_Status == BACNET_WRITE_STATUS_IN_PROGRESS) { - pObject->Write_Status = BACNET_WRITE_STATUS_SUCCESSFUL; - } + } + if (pObject->Write_Status == BACNET_WRITE_STATUS_IN_PROGRESS) { + /* the Write_Status property shall be set to either + SUCCESSFUL or FAILED. The SUCCESSFUL value indicates + that the Channel object has processed all of the properties + in List_Of_Object_Property_References and did not have + any coercion errors, and did not receive any errors, + rejects, or aborts. */ + pObject->Write_Status = BACNET_WRITE_STATUS_SUCCESSFUL; } return status; @@ -1099,20 +1144,39 @@ int Channel_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) static int Channel_List_Of_Object_Property_References_Length( uint32_t object_instance, uint8_t *apdu, size_t apdu_size) { - BACNET_APPLICATION_DATA_VALUE value = { 0 }; + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE value = { 0 }; int len = 0; struct object_data *pObject; pObject = Object_Data(object_instance); if (pObject) { - len = bacapp_decode_known_property( - apdu, apdu_size, &value, OBJECT_CHANNEL, - PROP_LIST_OF_OBJECT_PROPERTY_REFERENCES); + len = bacnet_device_object_property_reference_decode( + apdu, apdu_size, &value); } return len; } +/** + * @brief For a given object instance-number and reference, determines if the + * reference is for a direct self present-value reference + * @param channel_instance [in] BACnet channel object instance number + * @param ref [in] BACnet device object property reference + * @return true if the reference is for a direct self present-value + */ +static bool Channel_Member_Is_Direct_Self_Present_Value( + uint32_t channel_instance, + const BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *ref) +{ + if (!ref) { + return false; + } + + return (ref->objectIdentifier.type == OBJECT_CHANNEL) && + (ref->objectIdentifier.instance == channel_instance) && + (ref->propertyIdentifier == PROP_PRESENT_VALUE); +} + /** * @brief Write a value to a BACnetARRAY property element value * @param object_instance [in] BACnet network port object instance number @@ -1131,7 +1195,7 @@ static BACNET_ERROR_CODE Channel_List_Of_Object_Property_References_Write( size_t application_data_len) { BACNET_ERROR_CODE error_code = ERROR_CODE_UNKNOWN_OBJECT; - BACNET_APPLICATION_DATA_VALUE value = { 0 }; + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE value = { 0 }; int len = 0; bool status; struct object_data *pObject; @@ -1144,23 +1208,24 @@ static BACNET_ERROR_CODE Channel_List_Of_Object_Property_References_Write( (void)array_size; error_code = ERROR_CODE_WRITE_ACCESS_DENIED; } else { - len = bacapp_decode_known_property( - application_data, application_data_len, &value, OBJECT_CHANNEL, - PROP_LIST_OF_OBJECT_PROPERTY_REFERENCES); + len = bacnet_device_object_property_reference_decode( + application_data, application_data_len, &value); if (len > 0) { - if (value.tag == - BACNET_APPLICATION_TAG_DEVICE_OBJECT_PROPERTY_REFERENCE) { + status = Channel_Member_Is_Direct_Self_Present_Value( + object_instance, &value); + if (status) { + error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } else { status = List_Of_Object_Property_References_Set( - pObject, array_index - 1, - &value.type.Device_Object_Property_Reference); + pObject, array_index - 1, &value); if (status) { error_code = ERROR_CODE_SUCCESS; } else { error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; } - } else { - error_code = ERROR_CODE_INVALID_DATA_TYPE; } + } else if (len == 0) { + error_code = ERROR_CODE_INVALID_DATA_TYPE; } else { error_code = ERROR_CODE_ABORT_OTHER; } @@ -1180,14 +1245,14 @@ static BACNET_ERROR_CODE Channel_List_Of_Object_Property_References_Write( static int Channel_Control_Groups_Length( uint32_t object_instance, uint8_t *apdu, size_t apdu_size) { - BACNET_APPLICATION_DATA_VALUE value = { 0 }; + BACNET_UNSIGNED_INTEGER value_unsigned = 0; int len = 0; struct object_data *pObject; pObject = Object_Data(object_instance); if (pObject) { - len = bacapp_decode_known_property( - apdu, apdu_size, &value, OBJECT_CHANNEL, PROP_CONTROL_GROUPS); + len = bacnet_unsigned_application_decode( + apdu, apdu_size, &value_unsigned); } return len; @@ -1211,7 +1276,7 @@ static BACNET_ERROR_CODE Channel_Control_Groups_Write( size_t application_data_len) { BACNET_ERROR_CODE error_code = ERROR_CODE_UNKNOWN_OBJECT; - BACNET_APPLICATION_DATA_VALUE value = { 0 }; + BACNET_UNSIGNED_INTEGER value_unsigned = 0; uint16_t control_group; int len = 0; bool status; @@ -1225,26 +1290,23 @@ static BACNET_ERROR_CODE Channel_Control_Groups_Write( (void)array_size; error_code = ERROR_CODE_WRITE_ACCESS_DENIED; } else { - len = bacapp_decode_known_property( - application_data, application_data_len, &value, OBJECT_CHANNEL, - PROP_CONTROL_GROUPS); + len = bacnet_unsigned_application_decode( + application_data, application_data_len, &value_unsigned); if (len > 0) { - if (value.tag == BACNET_APPLICATION_TAG_UNSIGNED_INT) { - if (value.type.Unsigned_Int <= UINT16_MAX) { - control_group = (uint16_t)value.type.Unsigned_Int; - status = Control_Groups_Element_Set( - pObject, array_index, control_group); - if (status) { - error_code = ERROR_CODE_SUCCESS; - } else { - error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; - } + if (value_unsigned <= UINT16_MAX) { + control_group = (uint16_t)value_unsigned; + status = Control_Groups_Element_Set( + pObject, array_index, control_group); + if (status) { + error_code = ERROR_CODE_SUCCESS; } else { error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; } } else { - error_code = ERROR_CODE_INVALID_DATA_TYPE; + error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; } + } else if (len == 0) { + error_code = ERROR_CODE_INVALID_DATA_TYPE; } else { error_code = ERROR_CODE_ABORT_OTHER; } From 726d51fe22f020620d27fd5da96f6093e43cc091 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 17:00:29 -0500 Subject: [PATCH 18/42] fix: prevent uncontrolled recursion from Timer object self-references (#1347) * fix: prevent uncontrolled recursion from Timer object self-references * test: add regression test for Timer self-reference reentrant write guard --- CHANGELOG.md | 2 + SECURITY.md | 7 ++- src/bacnet/basic/object/timer.c | 42 +++++++++++++- test/bacnet/basic/object/timer/src/main.c | 69 ++++++++++++++++++++++- 4 files changed, 115 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b9c3b9c1..2774ef9073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The git repositories are hosted at the following sites: ### Security +* Secured Timer object State_Change_Values property self-reference that + caused uncontrolled recursion. (#1347) * Secured Channel object member self-reference that caused uncontrolled recursion. Changed Channel property handling to use device object property reference and unsigned value specific decoders. Fixed handling diff --git a/SECURITY.md b/SECURITY.md index ac7a7557e7..88b00147dd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,15 +18,16 @@ currently being supported with security updates in this branch: | 0.7.x | :x: | | < 0.6.x | :x: | - ## Coordinated Vulnerability Disclosure Vulnerabilites are disclosed to [CVE](https://www.cve.org/) or [GHSA](https://github.com/bacnet-stack/bacnet-stack/security/advisories?state=published) and a record is created to identify, define, and catalog publicly disclosed -cybersecurity vulnerabilities. Here are the published vulnerability records: +cybersecurity vulnerabilities. Here are the published vulnerability records +for v1.5.x: -Here are the known CVE records for v1.5.x: +Uncontrolled recursion in Timer object writeback path leads to remote server stack overflow +[GHSA-7r8r-2rj2-5wvr](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-7r8r-2rj2-5wvr) [CVE-2026-47217](https://www.cve.org/CVERecord?id=CVE-2026-47217) - Channel member self-reference causes uncontrolled recursion and stack overflow in default BACnet/IP server diff --git a/src/bacnet/basic/object/timer.c b/src/bacnet/basic/object/timer.c index 4ee1afd64e..73e6781ff5 100644 --- a/src/bacnet/basic/object/timer.c +++ b/src/bacnet/basic/object/timer.c @@ -69,6 +69,7 @@ struct object_data { BACNET_RELIABILITY Reliability; bool Out_Of_Service : 1; bool Changed : 1; + bool Writeback_Active : 1; void *Context; }; @@ -254,6 +255,34 @@ static bool Timer_Reference_List_Member_Empty( return status; } +/** + * For a given object instance-number, determines if the member is a + * self-reference with properties that can re-enter Timer transition logic + * + * @param object_instance - object-instance number of the object + * @param pMember - object property reference element + * @return true if the member is self + */ +static bool Timer_Reference_List_Member_Self( + uint32_t object_instance, + const BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMember) +{ + bool status = false; + + if (pMember) { + if ((pMember->objectIdentifier.type == OBJECT_TIMER) && + (pMember->objectIdentifier.instance == object_instance) && + (pMember->deviceIdentifier.type == OBJECT_DEVICE) && + (pMember->deviceIdentifier.instance != BACNET_MAX_INSTANCE) && + (((pMember->propertyIdentifier == PROP_PRESENT_VALUE) || + (pMember->propertyIdentifier == PROP_TIMER_RUNNING)))) { + status = true; + } + } + + return status; +} + /** * For a given object instance-number, returns the list member element * @param object_instance - object-instance number of the object @@ -519,6 +548,11 @@ static bool Timer_Write_Members( const BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *pMember = NULL; if (pObject && value) { + if (pObject->Writeback_Active) { + /* Prevent recursive writeback */ + return false; + } + pObject->Writeback_Active = true; for (i = 0; i < BACNET_TIMER_MANIPULATED_PROPERTIES_MAX; i++) { pMember = &pObject->Manipulated_Properties[i]; if (Timer_Reference_List_Member_Empty(pMember)) { @@ -549,6 +583,7 @@ static bool Timer_Write_Members( Timer_Write_Property_Notify(object_instance, status, &wp_data); } } + pObject->Writeback_Active = false; } return status; @@ -1856,7 +1891,7 @@ static BACNET_ERROR_CODE Timer_List_Of_Object_Property_References_Add( bool status = false; if ((!application_data) && (application_data_len == 0)) { - /* empty the BACnetLIST - remove all before adding */ + /* empty the BACnetLIST - remove all */ (void)Timer_Reference_List_Member_Element_Remove(object_instance, NULL); error_code = ERROR_CODE_SUCCESS; } else { @@ -1866,6 +1901,11 @@ static BACNET_ERROR_CODE Timer_List_Of_Object_Property_References_Add( if (Timer_Reference_List_Member_Empty(&new_value)) { /* The element value is out of range for the property. */ error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + } else if (Timer_Reference_List_Member_Self( + object_instance, &new_value)) { + /* The element value is a self-reference with + properties that can re-enter Timer transition logic */ + error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; } else { status = Timer_Reference_List_Member_Element_Add( object_instance, &new_value); diff --git a/test/bacnet/basic/object/timer/src/main.c b/test/bacnet/basic/object/timer/src/main.c index 8fae6ff2c4..0bc365ebe1 100644 --- a/test/bacnet/basic/object/timer/src/main.c +++ b/test/bacnet/basic/object/timer/src/main.c @@ -26,6 +26,18 @@ static bool Write_Property_Internal(BACNET_WRITE_PROPERTY_DATA *wp_data) return true; } +static unsigned Reentrant_Write_Count; +static bool +Reentrant_Write_Property_Internal(BACNET_WRITE_PROPERTY_DATA *wp_data) +{ + Reentrant_Write_Count++; + + /* Exercise re-entrant self-write path through Timer_Write_Property(). */ + (void)Timer_Write_Property(wp_data); + + return true; +} + static struct timer_write_property_notification Write_Property_Notification; static BACNET_WRITE_PROPERTY_DATA Write_Property_Notification_Data; static uint32_t Write_Property_Notification_Instance; @@ -759,6 +771,60 @@ static void test_Timer_Operation(void) /* cleanup all */ Timer_Cleanup(); } + +/** + * @brief Regression test for self-reference writeback recursion guard + */ +static void test_Timer_Self_Reference_Reentrant_Write(void) +{ + const uint32_t instance = 124; + bool status = false; + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE member = { 0 }; + BACNET_TIMER_STATE_CHANGE_VALUE *value = NULL; + + Timer_Init(); + Timer_Create(instance); + status = Timer_Valid_Instance(instance); + zassert_true(status, NULL); + + Timer_Write_Property_Internal_Callback_Set( + Reentrant_Write_Property_Internal); + + member.deviceIdentifier.type = OBJECT_DEVICE; + member.deviceIdentifier.instance = 0; + member.objectIdentifier.type = OBJECT_TIMER; + member.objectIdentifier.instance = instance; + member.propertyIdentifier = PROP_PRESENT_VALUE; + member.arrayIndex = BACNET_ARRAY_ALL; + status = Timer_Reference_List_Member_Element_Set(instance, 0, &member); + zassert_true(status, NULL); + + value = + Timer_State_Change_Value(instance, TIMER_TRANSITION_IDLE_TO_RUNNING); + zassert_not_null(value, NULL); + value->tag = BACNET_APPLICATION_TAG_UNSIGNED_INT; + value->type.Unsigned_Int = 10; + value = + Timer_State_Change_Value(instance, TIMER_TRANSITION_RUNNING_TO_RUNNING); + zassert_not_null(value, NULL); + value->tag = BACNET_APPLICATION_TAG_UNSIGNED_INT; + value->type.Unsigned_Int = 10; + + Reentrant_Write_Count = 0; + status = Timer_Running_Set(instance, true); + zassert_true(status, NULL); + zassert_equal(Reentrant_Write_Count, 1, NULL); + + /* A second outer write proves the recursion guard was cleared. */ + status = Timer_Running_Set(instance, true); + zassert_true(status, NULL); + zassert_equal(Reentrant_Write_Count, 2, NULL); + + status = Timer_Delete(instance); + zassert_true(status, NULL); + Timer_Cleanup(); + Timer_Write_Property_Internal_Callback_Set(NULL); +} /** * @} */ @@ -767,7 +833,8 @@ void test_main(void) { ztest_test_suite( timer_tests, ztest_unit_test(test_Timer_Read_Write), - ztest_unit_test(test_Timer_Operation)); + ztest_unit_test(test_Timer_Operation), + ztest_unit_test(test_Timer_Self_Reference_Reentrant_Write)); ztest_run_test_suite(timer_tests); } From aa0bba9243ed38956813254f09cc854d54b7f0c5 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Mon, 18 May 2026 14:49:30 -0500 Subject: [PATCH 19/42] Fix Device_End_Restore to delete existing objects only after the first record is decoded (#1352) --- CHANGELOG.md | 2 ++ SECURITY.md | 9 +++++++-- src/bacnet/basic/object/device.c | 7 +++++-- src/bacnet/basic/server/bacnet_device.c | 7 +++++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2774ef9073..910dcb3672 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The git repositories are hosted at the following sites: ### Security +* Secured Device ENDRESTORE so that it does not delete existing objects + until after the first record is decoded. (#1352) * Secured Timer object State_Change_Values property self-reference that caused uncontrolled recursion. (#1347) * Secured Channel object member self-reference that caused uncontrolled diff --git a/SECURITY.md b/SECURITY.md index 88b00147dd..7112930cd5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,8 +23,13 @@ currently being supported with security updates in this branch: Vulnerabilites are disclosed to [CVE](https://www.cve.org/) or [GHSA](https://github.com/bacnet-stack/bacnet-stack/security/advisories?state=published) and a record is created to identify, define, and catalog publicly disclosed -cybersecurity vulnerabilities. Here are the published vulnerability records -for v1.5.x: +cybersecurity vulnerabilities. + +Here are the published vulnerability records for v1.5.x: + +[CVE-2026-47257](https://www.cve.org/CVERecord?id=CVE-2026-47257) - +ReinitializeDevice ENDRESTORE can delete existing objects on an empty restore file and still return success +[GHSA-x6pp-3pf3-f87r](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-x6pp-3pf3-f87r) Uncontrolled recursion in Timer object writeback path leads to remote server stack overflow [GHSA-7r8r-2rj2-5wvr](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-7r8r-2rj2-5wvr) diff --git a/src/bacnet/basic/object/device.c b/src/bacnet/basic/object/device.c index 1d99dec96d..5d97a24420 100644 --- a/src/bacnet/basic/object/device.c +++ b/src/bacnet/basic/object/device.c @@ -3909,8 +3909,6 @@ void Device_End_Restore(void) datetime_local(&bdateTime.date, &bdateTime.time, NULL, NULL); bacapp_timestamp_datetime_set(&Last_Restore_Time, &bdateTime); - /* delete all existing objects before restore */ - Device_Delete_Objects(); /* create objects from the backup file */ file_size = bacfile_file_size(Configuration_Files[0]); while (offset < file_size) { @@ -3920,6 +3918,11 @@ void Device_End_Restore(void) decoded_len = create_object_decode_service_request( apdu, apdu_len, &create_data); if (decoded_len > 0) { + if (offset == 0) { + /* delete all existing objects before restore + after the first record is successfully decoded */ + Device_Delete_Objects(); + } offset += decoded_len; create_data.error_class = ERROR_CLASS_PROPERTY; create_data.error_code = ERROR_CODE_SUCCESS; diff --git a/src/bacnet/basic/server/bacnet_device.c b/src/bacnet/basic/server/bacnet_device.c index 9ac72eeaca..271cf187cd 100644 --- a/src/bacnet/basic/server/bacnet_device.c +++ b/src/bacnet/basic/server/bacnet_device.c @@ -3773,8 +3773,6 @@ void Device_End_Restore(void) datetime_local(&bdateTime.date, &bdateTime.time, NULL, NULL); bacapp_timestamp_datetime_set(&Last_Restore_Time, &bdateTime); - /* delete all existing objects before restore */ - Device_Delete_Objects(); /* create objects from the backup file */ file_size = bacfile_file_size(Configuration_Files[0]); while (offset < file_size) { @@ -3784,6 +3782,11 @@ void Device_End_Restore(void) decoded_len = create_object_decode_service_request( apdu, apdu_len, &create_data); if (decoded_len > 0) { + if (offset == 0) { + /* delete all existing objects before restore + after the first record is successfully decoded */ + Device_Delete_Objects(); + } offset += decoded_len; create_data.error_class = ERROR_CLASS_PROPERTY; create_data.error_code = ERROR_CODE_SUCCESS; From f5a18ec3e9b761d2e5a70b93ff7ecd48b79f1870 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 17:11:01 -0500 Subject: [PATCH 20/42] Fix buffer overflow vulnerability in Notification Class AddListElement and RemoveListElement. (#1353) --- CHANGELOG.md | 2 + SECURITY.md | 8 + src/bacnet/basic/object/nc.c | 16 +- test/bacnet/basic/object/nc/src/main.c | 256 ++++++++++++++++++++++++- 4 files changed, 280 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 910dcb3672..d0fd2d444c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The git repositories are hosted at the following sites: ### Security +* Secured Notification Class AddListElement and RemoveListElement stack + based buffer overflow. (#1353) * Secured Device ENDRESTORE so that it does not delete existing objects until after the first record is decoded. (#1352) * Secured Timer object State_Change_Values property self-reference that diff --git a/SECURITY.md b/SECURITY.md index 7112930cd5..9791a73856 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,6 +27,14 @@ cybersecurity vulnerabilities. Here are the published vulnerability records for v1.5.x: +[CVE-2026-47259](https://www.cve.org/CVERecord?id=CVE-2026-47259) - +Stack-based buffer overflow in Notification Class RemoveListElement recipient-list decoding +[GHSA-9w9m-w7w5-rrv3](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-9w9m-w7w5-rrv3) + +[CVE-2026-47258](https://www.cve.org/CVERecord?id=CVE-2026-47258) - +Stack-based buffer overflow in Notification Class AddListElement recipient-list decoding +[GHSA-rjmv-3mcm-r83j](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-rjmv-3mcm-r83j) + [CVE-2026-47257](https://www.cve.org/CVERecord?id=CVE-2026-47257) - ReinitializeDevice ENDRESTORE can delete existing objects on an empty restore file and still return success [GHSA-x6pp-3pf3-f87r](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-x6pp-3pf3-f87r) diff --git a/src/bacnet/basic/object/nc.c b/src/bacnet/basic/object/nc.c index 56f174f222..a57ac98914 100644 --- a/src/bacnet/basic/object/nc.c +++ b/src/bacnet/basic/object/nc.c @@ -926,6 +926,13 @@ int Notification_Class_Add_List_Element(BACNET_LIST_ELEMENT_DATA *list_element) if (len > 0) { new_element_count++; application_data_len -= len; + if (new_element_count >= NC_MAX_RECIPIENTS) { + list_element->first_failed_element_number = new_element_count; + list_element->error_class = ERROR_CLASS_RESOURCES; + list_element->error_code = + ERROR_CODE_NO_SPACE_TO_ADD_LIST_ELEMENT; + return BACNET_STATUS_ERROR; + } } else { list_element->first_failed_element_number = new_element_count; list_element->error_class = ERROR_CLASS_PROPERTY; @@ -1089,10 +1096,17 @@ int Notification_Class_Remove_List_Element( if (len > 0) { remove_element_count++; application_data_len -= len; + if (remove_element_count >= NC_MAX_RECIPIENTS) { + list_element->first_failed_element_number = + remove_element_count; + list_element->error_class = ERROR_CLASS_SERVICES; + list_element->error_code = ERROR_CODE_LIST_ELEMENT_NOT_FOUND; + return BACNET_STATUS_ERROR; + } } else { list_element->first_failed_element_number = remove_element_count; list_element->error_class = ERROR_CLASS_PROPERTY; - list_element->error_code = ERROR_CODE_INVALID_DATA_ENCODING; + list_element->error_code = ERROR_CODE_INVALID_DATA_TYPE; return BACNET_STATUS_ERROR; } } diff --git a/test/bacnet/basic/object/nc/src/main.c b/test/bacnet/basic/object/nc/src/main.c index e5c760853e..88df61f337 100644 --- a/test/bacnet/basic/object/nc/src/main.c +++ b/test/bacnet/basic/object/nc/src/main.c @@ -402,6 +402,258 @@ static void test_Notification_Class_Common_Reporting(void) Notification_Class_common_reporting_function(&event_data); } +/** + * @brief Test security fix for excessive recipient list elements + * + * This test verifies the fix for the vulnerability where a remote BACnet + * client can send a crafted AddListElement request containing more than + * NC_MAX_RECIPIENTS recipient-list elements. The vulnerability caused a + * stack-based out-of-bounds write as the server decoded elements into a + * fixed-size stack array before enforcing the capacity limit. + * + * This test verifies that: + * 1. Multiple recipients can be added (up to NC_MAX_RECIPIENTS-1) + * 2. Attempting to add more than NC_MAX_RECIPIENTS elements is rejected + * 3. The error codes are set correctly for overflow conditions + * 4. The stack buffer is protected from overflow + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST( + notification_class_tests, test_Notification_Class_Add_List_Element_Overflow) +#else +static void test_Notification_Class_Add_List_Element_Overflow(void) +#endif +{ + const uint32_t instance = 1; + int err = 0; + BACNET_LIST_ELEMENT_DATA list_element = { 0 }; + BACNET_DESTINATION destination = { 0 }; + BACNET_DESTINATION recipient_list[NC_MAX_RECIPIENTS] = { 0 }; + uint8_t apdu[MAX_APDU] = { 0 }; + uint8_t apdu_large[MAX_APDU] = { 0 }; + int len = 0; + int total_len = 0; + unsigned i = 0, count = 0, encoded_count = 0; + bool status = false; + + Notification_Class_Init(); + zassert_true(Notification_Class_Valid_Instance(instance), NULL); + + /* Setup common destination parameters */ + for (unsigned j = 0; j < MAX_BACNET_DAYS_OF_WEEK; j++) { + bitstring_set_bit(&destination.ValidDays, j, true); + } + datetime_set_time(&destination.FromTime, 0, 0, 0, 0); + datetime_set_time(&destination.ToTime, 23, 59, 59, 99); + destination.ConfirmedNotify = true; + bitstring_set_bit(&destination.Transitions, TRANSITION_TO_OFFNORMAL, true); + bitstring_set_bit(&destination.Transitions, TRANSITION_TO_FAULT, true); + bitstring_set_bit(&destination.Transitions, TRANSITION_TO_NORMAL, true); + + /* Test 1: Successfully add one recipient */ + total_len = 0; + bacnet_recipient_device_set(&destination.Recipient, OBJECT_DEVICE, 100); + destination.ProcessIdentifier = 0; + len = bacnet_destination_encode(&apdu[total_len], &destination); + zassert(len > 0, "encode failed", NULL); + total_len += len; + + /* Add a single recipient */ + list_element.object_type = OBJECT_NOTIFICATION_CLASS; + list_element.object_instance = instance; + list_element.object_property = PROP_RECIPIENT_LIST; + list_element.array_index = BACNET_ARRAY_ALL; + list_element.application_data = apdu; + list_element.application_data_len = total_len; + list_element.first_failed_element_number = 0; + err = Notification_Class_Add_List_Element(&list_element); + zassert_equal(err, BACNET_STATUS_OK, "Failed to add a single recipient"); + + /* Test 2: Attempt to add exactly NC_MAX_RECIPIENTS in a single request + This should be rejected because the security fix prevents decoding more + than NC_MAX_RECIPIENTS-1 elements to avoid buffer overflow */ + total_len = 0; + for (i = 0; i < NC_MAX_RECIPIENTS && total_len < (int)sizeof(apdu_large); + i++) { + bacnet_recipient_device_set( + &destination.Recipient, OBJECT_DEVICE, 200 + i); + destination.ProcessIdentifier = 100 + i; + len = bacnet_destination_encode(&apdu_large[total_len], &destination); + if (len > 0) { + total_len += len; + } else { + break; + } + } + encoded_count = i; + /* Try to add all NC_MAX_RECIPIENTS in one request - should be rejected */ + list_element.application_data = apdu_large; + list_element.application_data_len = total_len; + list_element.first_failed_element_number = 0; + list_element.error_class = 0; + list_element.error_code = 0; + err = Notification_Class_Add_List_Element(&list_element); + /* The security fix should reject this because it exceeds the buffer size */ + if (encoded_count >= NC_MAX_RECIPIENTS) { + zassert_not_equal( + err, BACNET_STATUS_OK, + "Should reject request to add NC_MAX_RECIPIENTS elements"); + zassert_equal( + list_element.error_class, ERROR_CLASS_RESOURCES, + "Error class should be RESOURCES for overflow"); + zassert_equal( + list_element.error_code, ERROR_CODE_NO_SPACE_TO_ADD_LIST_ELEMENT, + "Error code should be NO_SPACE_TO_ADD_LIST_ELEMENT"); + } + + /* Test 3: Verify existing recipients are not corrupted */ + status = + Notification_Class_Get_Recipient_List(instance, &recipient_list[0]); + zassert_true(status, NULL); + /* Verify we have at least one recipient (stack was not corrupted) */ + count = 0; + for (i = 0; i < NC_MAX_RECIPIENTS; i++) { + if (!bacnet_recipient_device_wildcard(&recipient_list[i].Recipient)) { + count++; + } + } + zassert(count > 0, "Recipient list should not be corrupted", NULL); + + return; +} + +/** + * @brief Test Remove_List_Element function + * + * This test verifies the security and functionality of the Remove function: + * 1. Tests successful removal of existing recipients + * 2. Tests the decode buffer overflow protection + * 3. Tests error handling for invalid inputs + * 4. Tests that removed elements are properly cleared + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST( + notification_class_tests, + test_Notification_Class_Remove_List_Element_Overflow) +#else +static void test_Notification_Class_Remove_List_Element_Overflow(void) +#endif +{ + const uint32_t instance = 1; + int err = 0; + BACNET_LIST_ELEMENT_DATA list_element = { 0 }; + BACNET_DESTINATION destination = { 0 }; + uint8_t apdu_add[MAX_APDU] = { 0 }; + uint8_t apdu_remove[MAX_APDU] = { 0 }; + int len = 0; + int total_len = 0; + unsigned i = 0; + + Notification_Class_Init(); + zassert_true(Notification_Class_Valid_Instance(instance), NULL); + + /* Setup common destination parameters */ + for (unsigned j = 0; j < MAX_BACNET_DAYS_OF_WEEK; j++) { + bitstring_set_bit(&destination.ValidDays, j, true); + } + datetime_set_time(&destination.FromTime, 0, 0, 0, 0); + datetime_set_time(&destination.ToTime, 23, 59, 59, 99); + destination.ConfirmedNotify = true; + bitstring_set_bit(&destination.Transitions, TRANSITION_TO_OFFNORMAL, true); + bitstring_set_bit(&destination.Transitions, TRANSITION_TO_FAULT, true); + bitstring_set_bit(&destination.Transitions, TRANSITION_TO_NORMAL, true); + + /* Test 1: Add two recipients for removal testing */ + total_len = 0; + for (i = 0; i < 2 && total_len < (int)sizeof(apdu_add); i++) { + bacnet_recipient_device_set( + &destination.Recipient, OBJECT_DEVICE, 100 + i); + destination.ProcessIdentifier = i; + len = bacnet_destination_encode(&apdu_add[total_len], &destination); + zassert(len > 0, "encode failed for add", NULL); + total_len += len; + } + + list_element.object_type = OBJECT_NOTIFICATION_CLASS; + list_element.object_instance = instance; + list_element.object_property = PROP_RECIPIENT_LIST; + list_element.array_index = BACNET_ARRAY_ALL; + list_element.application_data = apdu_add; + list_element.application_data_len = total_len; + list_element.first_failed_element_number = 0; + err = Notification_Class_Add_List_Element(&list_element); + zassert_equal(err, BACNET_STATUS_OK, "Failed to add recipients"); + + /* Add first recipient */ + total_len = 0; + destination.ProcessIdentifier = 0; + len = bacnet_destination_encode(&apdu_add[0], &destination); + zassert(len > 0, "encode failed for first recipient", NULL); + list_element.object_type = OBJECT_NOTIFICATION_CLASS; + list_element.object_instance = instance; + list_element.object_property = PROP_RECIPIENT_LIST; + list_element.array_index = BACNET_ARRAY_ALL; + list_element.application_data = apdu_add; + list_element.application_data_len = len; + list_element.first_failed_element_number = 0; + err = Notification_Class_Add_List_Element(&list_element); + zassert_equal(err, BACNET_STATUS_OK, "Failed to add first recipient"); + + /* Test 2: Successfully remove the added recipient */ + total_len = 0; + bacnet_recipient_device_set(&destination.Recipient, OBJECT_DEVICE, 100); + len = bacnet_destination_encode(&apdu_remove[total_len], &destination); + zassert(len > 0, "encode failed for remove", NULL); + total_len = len; + list_element.application_data = apdu_remove; + list_element.application_data_len = total_len; + list_element.first_failed_element_number = 0; + err = Notification_Class_Remove_List_Element(&list_element); + zassert_equal(err, BACNET_STATUS_OK, "Failed to remove existing recipient"); + + /* Test 3: Test overflow protection on decode buffer - attempt excessive + * elements */ + total_len = 0; + unsigned extra_count = 3; + for (i = 0; i < (NC_MAX_RECIPIENTS + extra_count) && + total_len < (int)sizeof(apdu_remove); + i++) { + bacnet_recipient_device_set( + &destination.Recipient, OBJECT_DEVICE, 200 + i); + destination.ProcessIdentifier = 200 + i; + len = bacnet_destination_encode(&apdu_remove[total_len], &destination); + if (len > 0) { + total_len += len; + } else { + break; + } + } + list_element.application_data = apdu_remove; + list_element.application_data_len = total_len; + list_element.first_failed_element_number = 0; + list_element.error_class = 0; + list_element.error_code = 0; + err = Notification_Class_Remove_List_Element(&list_element); + /* Should be rejected - these don't exist in the list or overflow */ + zassert( + err != BACNET_STATUS_ABORT, "Should not abort (overflow protected)", + NULL); + + /* Test 4: Verify function properly handles null input */ + err = Notification_Class_Remove_List_Element(NULL); + zassert_equal(err, BACNET_STATUS_ABORT, "Should abort on null input"); + + /* Test 5: Verify invalid property fails */ + list_element.object_property = PROP_PRIORITY; + list_element.application_data = apdu_remove; + list_element.application_data_len = 1; + err = Notification_Class_Remove_List_Element(&list_element); + zassert_equal(err, BACNET_STATUS_ERROR, "Should error on invalid property"); + zassert_equal(list_element.error_class, ERROR_CLASS_PROPERTY, NULL); + + return; +} + /** * @} */ @@ -417,7 +669,9 @@ void test_main(void) ztest_unit_test(test_Notification_Class_Priority), ztest_unit_test(test_Notification_Class_Ack_Required), ztest_unit_test(test_Notification_Class_Recipient_List), - ztest_unit_test(test_Notification_Class_Common_Reporting)); + ztest_unit_test(test_Notification_Class_Common_Reporting), + ztest_unit_test(test_Notification_Class_Add_List_Element_Overflow), + ztest_unit_test(test_Notification_Class_Remove_List_Element_Overflow)); ztest_run_test_suite(notification_class_tests); } From f0b0d0946824c231af09568106c7e16c4912288e Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 17:15:26 -0500 Subject: [PATCH 21/42] Fix buffer overflow in Life Safety Point and Zone Read_Property for accepted-modes property (#1354) --- CHANGELOG.md | 2 ++ SECURITY.md | 4 ++++ src/bacnet/basic/object/lsp.c | 10 +++++++++- src/bacnet/basic/object/lsz.c | 8 +++++++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0fd2d444c..c62b56a27f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The git repositories are hosted at the following sites: ### Security +* Secured Life Safety Point and Zone Read_Property of accepted-modes property + buffer overflow. (#1354) * Secured Notification Class AddListElement and RemoveListElement stack based buffer overflow. (#1353) * Secured Device ENDRESTORE so that it does not delete existing objects diff --git a/SECURITY.md b/SECURITY.md index 9791a73856..af9cef2e0c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,6 +27,10 @@ cybersecurity vulnerabilities. Here are the published vulnerability records for v1.5.x: +[CVE-2026-47711](https://www.cve.org/CVERecord?id=CVE-2026-47711) - +Stack buffer overflow in Loop internal ReadProperty path via Life_Safety_Zone accepted-modes property +[GHSA-vrpm-9gm2-x552](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-vrpm-9gm2-x552) + [CVE-2026-47259](https://www.cve.org/CVERecord?id=CVE-2026-47259) - Stack-based buffer overflow in Notification Class RemoveListElement recipient-list decoding [GHSA-9w9m-w7w5-rrv3](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-9w9m-w7w5-rrv3) diff --git a/src/bacnet/basic/object/lsp.c b/src/bacnet/basic/object/lsp.c index b931a1a649..47978725be 100644 --- a/src/bacnet/basic/object/lsp.c +++ b/src/bacnet/basic/object/lsp.c @@ -502,12 +502,14 @@ int Life_Safety_Point_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) bool state = false; BACNET_RELIABILITY reliability = RELIABILITY_NO_FAULT_DETECTED; uint8_t *apdu = NULL; + int apdu_size = 0; if ((rpdata == NULL) || (rpdata->application_data == NULL) || (rpdata->application_data_len == 0)) { return 0; } apdu = rpdata->application_data; + apdu_size = rpdata->application_data_len; switch (rpdata->object_property) { case PROP_OBJECT_IDENTIFIER: apdu_len = encode_application_object_id( @@ -571,7 +573,13 @@ int Life_Safety_Point_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) break; case PROP_ACCEPTED_MODES: for (mode = 0; mode < LIFE_SAFETY_MODE_RESERVED_MIN; mode++) { - len = encode_application_enumerated(&apdu[apdu_len], mode); + len = bacnet_enumerated_application_encode( + &apdu[apdu_len], apdu_size - apdu_len, mode); + if (len <= 0) { + rpdata->error_code = + ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + return BACNET_STATUS_ABORT; + } apdu_len += len; } break; diff --git a/src/bacnet/basic/object/lsz.c b/src/bacnet/basic/object/lsz.c index f12c313363..d01fd5430e 100644 --- a/src/bacnet/basic/object/lsz.c +++ b/src/bacnet/basic/object/lsz.c @@ -745,7 +745,13 @@ int Life_Safety_Zone_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) break; case PROP_ACCEPTED_MODES: for (mode = 0; mode < LIFE_SAFETY_MODE_RESERVED_MIN; mode++) { - len = encode_application_enumerated(&apdu[apdu_len], mode); + len = bacnet_enumerated_application_encode( + &apdu[apdu_len], apdu_size - apdu_len, mode); + if (len <= 0) { + rpdata->error_code = + ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + return BACNET_STATUS_ABORT; + } apdu_len += len; } break; From 6f66cb254cf2d55ae87e789f2d9045a46fd4658d Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Tue, 26 May 2026 17:20:07 -0500 Subject: [PATCH 22/42] Fix buffer overflow issues in Loop object internal Read_Property function (#1355) --- CHANGELOG.md | 1 + SECURITY.md | 4 ++++ src/bacnet/basic/object/loop.c | 9 +++++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c62b56a27f..c2d0754948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The git repositories are hosted at the following sites: ### Security +* Secured Loop object internal Read_Property function buffer overflow. (#1355) * Secured Life Safety Point and Zone Read_Property of accepted-modes property buffer overflow. (#1354) * Secured Notification Class AddListElement and RemoveListElement stack diff --git a/SECURITY.md b/SECURITY.md index af9cef2e0c..4c7c74769a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,6 +27,10 @@ cybersecurity vulnerabilities. Here are the published vulnerability records for v1.5.x: +[CVE-2026-47710](https://www.cve.org/CVERecord?id=CVE-2026-47710) - +Loop reference to long Structured View description causes stack-buffer-overflow +[GHSA-2xm5-gjpc-9m6q](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-2xm5-gjpc-9m6q) + [CVE-2026-47711](https://www.cve.org/CVERecord?id=CVE-2026-47711) - Stack buffer overflow in Loop internal ReadProperty path via Life_Safety_Zone accepted-modes property [GHSA-vrpm-9gm2-x552](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-vrpm-9gm2-x552) diff --git a/src/bacnet/basic/object/loop.c b/src/bacnet/basic/object/loop.c index 70f3ea68ab..808ddadfff 100644 --- a/src/bacnet/basic/object/loop.c +++ b/src/bacnet/basic/object/loop.c @@ -45,6 +45,7 @@ static const BACNET_OBJECT_TYPE Object_Type = OBJECT_LOOP; /* handling for manipulated and reference properties */ static write_property_function Write_Property_Internal_Callback; static read_property_function Read_Property_Internal_Callback; +static uint8_t Read_Property_Buffer[MAX_APDU]; /* Write Property notification callbacks for logging or other purposes */ static struct loop_write_property_notification Write_Property_Notification_Head; @@ -1990,7 +1991,6 @@ static bool Loop_Read_Variable_Reference_Update( const BACNET_OBJECT_PROPERTY_REFERENCE *reference, float *value) { BACNET_READ_PROPERTY_DATA data = { 0 }; - uint8_t apdu[32] = { 0 }; int apdu_len = 0, len = 0; bool status = false; @@ -1999,8 +1999,8 @@ static bool Loop_Read_Variable_Reference_Update( data.object_instance = reference->object_identifier.instance; data.object_property = reference->property_identifier; data.array_index = reference->property_array_index; - data.application_data = apdu; - data.application_data_len = sizeof(apdu); + data.application_data = Read_Property_Buffer; + data.application_data_len = sizeof(Read_Property_Buffer); data.error_class = ERROR_CLASS_PROPERTY; data.error_code = ERROR_CODE_UNKNOWN_PROPERTY; if (Read_Property_Internal_Callback) { @@ -2008,7 +2008,8 @@ static bool Loop_Read_Variable_Reference_Update( } if (apdu_len > 0) { /* expecting only application tagged REAL values */ - len = bacnet_real_application_decode(apdu, apdu_len, value); + len = bacnet_real_application_decode( + Read_Property_Buffer, apdu_len, value); if (len > 0) { status = true; } From a12da8c63b90804e7497af90dd232c1e84bab27c Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Wed, 27 May 2026 14:49:53 -0500 Subject: [PATCH 23/42] Fix AtomicReadFile and AtomicWriteFile bounds checking on start position. (#1362) --- CHANGELOG.md | 5 +- SECURITY.md | 3 + ports/posix/bacfile-posix.c | 34 ++++++- src/bacnet/basic/object/bacfile.c | 70 +++++++------ src/bacnet/basic/service/h_arf.c | 123 ++++++++++++++--------- src/bacnet/basic/service/h_awf.c | 105 +++++++++++++------ src/bacnet/basic/service/h_awf.h | 10 ++ src/bacnet/basic/sys/bramfs.c | 35 ++++++- src/bacnet/basic/sys/bsramfs.c | 15 +++ src/bacnet/version.h | 2 +- test/bacnet/basic/sys/bramfs/src/main.c | 97 +++++++++++++++++- test/bacnet/basic/sys/bsramfs/src/main.c | 62 +++++++++++- 12 files changed, 444 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d0754948..2a90d1c4ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,13 @@ The git repositories are hosted at the following sites: * * -## [1.5.1-rc2] - 2026-05-26 +## [1.5.1-rc3] - 2026-05-27 ### Security +* Secured AtomicReadFile/AtomicWriteFile logic and underlying file backends + by adding explicit invalid-start-position guards which reject negative + and out-of-range positions/records. (#1362) * Secured Loop object internal Read_Property function buffer overflow. (#1355) * Secured Life Safety Point and Zone Read_Property of accepted-modes property buffer overflow. (#1354) diff --git a/SECURITY.md b/SECURITY.md index 4c7c74769a..1875c7488a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,6 +27,9 @@ cybersecurity vulnerabilities. Here are the published vulnerability records for v1.5.x: +AtomicReadFile/AtomicWriteFile stream fileStartPosition validation flaw causes out-of-bounds read/write in RAM file backends +[GHSA-8759-hx7g-94qx](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8759-hx7g-94qx) + [CVE-2026-47710](https://www.cve.org/CVERecord?id=CVE-2026-47710) - Loop reference to long Structured View description causes stack-buffer-overflow [GHSA-2xm5-gjpc-9m6q](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-2xm5-gjpc-9m6q) diff --git a/ports/posix/bacfile-posix.c b/ports/posix/bacfile-posix.c index 5150fc20e9..6a3fbeee18 100644 --- a/ports/posix/bacfile-posix.c +++ b/ports/posix/bacfile-posix.c @@ -90,6 +90,8 @@ bool bacfile_posix_file_size_set(const char *pathname, size_t file_size) * @brief Reads stream data from a file * @param pathname - name of the file to read from * @param fileStartPosition - starting position in the file + * If the 'File Start Position' parameter is either less than 0 + * or exceeds the actual file size, then an error is returned. * @param fileData - data buffer to read into * @param fileDataLen - size of the data buffer * @return number of bytes read, or 0 if not successful @@ -103,9 +105,18 @@ size_t bacfile_posix_read_stream_data( FILE *pFile = NULL; size_t len = 0; + if (fileStartPosition < 0) { + /* invalid file start position */ + return 0; + } if (filename_path_valid(pathname)) { pFile = fopen(pathname, "rb"); if (pFile) { + if (fileStartPosition > fsize(pFile)) { + /* invalid file start position */ + fclose(pFile); + return 0; + } (void)fseek(pFile, fileStartPosition, SEEK_SET); len = fread(fileData, 1, fileDataLen, pFile); fclose(pFile); @@ -121,6 +132,13 @@ size_t bacfile_posix_read_stream_data( * @brief Writes stream data to a file * @param pathname - name of the file to write to * @param fileStartPosition - starting position in the file + * If the 'File Start Position' parameter exceeds the actual file size, + * then the file shall be extended to the size indicated, + * but the contents of any intervening octets or records + * shall be a local matter. + * If this parameter has the special value -1, + * then the write operation shall be treated + * as an append to the current end of file. * @param fileData - data buffer to write from * @param fileDataLen - size of the data buffer * @return number of bytes written, or 0 if not successful @@ -143,7 +161,7 @@ size_t bacfile_posix_write_stream_data( value -1, then the write operation shall be treated as an append to the current end of file. */ pFile = fopen(pathname, "ab+"); - } else { + } else if (fileStartPosition > 0) { /* open for update */ pFile = fopen(pathname, "rb+"); } @@ -165,6 +183,10 @@ size_t bacfile_posix_write_stream_data( * @brief Writes record data to a file * @param pathname - name of the file to write to * @param fileStartRecord - starting record in the file + * If 'File Start Record' parameter has the special + * value -1, then the write operation shall be treated + * as an append to the current end of file, + * and fileIndexRecord can be ignored. * @param fileIndexRecord - index of the record to read * @param fileData - data buffer to read into * @param fileDataLen - size of the data buffer @@ -195,7 +217,7 @@ bool bacfile_posix_write_record_data( as an append to the current end of file. */ pFile = fopen(pathname, "ab+"); fileSeekRecord = fileIndexRecord; - } else { + } else if (fileStartRecord > 0) { /* open for update */ pFile = fopen(pathname, "rb+"); fileSeekRecord = fileStartRecord + fileIndexRecord; @@ -226,6 +248,8 @@ bool bacfile_posix_write_record_data( * @brief Reads record data from a file * @param pathname - name of the file to read from * @param fileStartRecord - starting record in the file + * If the 'File Start Record' parameter is either less than 0 + * or exceeds the actual file size, then an error is returned. * @param fileIndexRecord - index of the record to read * @param fileData - data buffer to read into * @param fileDataLen - size of the data buffer @@ -245,6 +269,10 @@ bool bacfile_posix_read_record_data( const char *pData = NULL; size_t fileSeekRecord = 0; + if (fileStartRecord < 0) { + /* invalid file start record */ + return false; + } if (filename_path_valid(pathname)) { pFile = fopen(pathname, "rb"); if (pFile) { @@ -257,7 +285,7 @@ bool bacfile_posix_read_record_data( } } if ((i == fileSeekRecord) && (fileDataLen <= sizeof(dummy_data))) { - /* copy the record data */ + /* We found the record, so copy it */ memmove(fileData, &dummy_data[0], fileDataLen); status = true; } diff --git a/src/bacnet/basic/object/bacfile.c b/src/bacnet/basic/object/bacfile.c index 7c21ceb73e..47706f7b6d 100644 --- a/src/bacnet/basic/object/bacfile.c +++ b/src/bacnet/basic/object/bacfile.c @@ -1040,7 +1040,7 @@ bool bacfile_read_stream_data(BACNET_ATOMIC_READ_FILE_DATA *data) { const char *pathname = NULL; bool found = false; - size_t len = 0; + size_t len = 0, file_size = 0; size_t requestedOctetCount = 0; if (!data) { @@ -1049,17 +1049,24 @@ bool bacfile_read_stream_data(BACNET_ATOMIC_READ_FILE_DATA *data) pathname = bacfile_pathname(data->object_instance); if (pathname) { found = true; - requestedOctetCount = data->type.stream.requestedOctetCount; - if (requestedOctetCount > octetstring_capacity(&data->fileData[0])) { - requestedOctetCount = octetstring_capacity(&data->fileData[0]); - } - len = bacfile_read_stream_data_callback( - pathname, data->type.stream.fileStartPosition, - octetstring_value(&data->fileData[0]), requestedOctetCount); - if (len < requestedOctetCount) { - data->endOfFile = true; + file_size = bacfile_file_size_callback(pathname); + if ((data->type.stream.fileStartPosition >= 0) && + (data->type.stream.fileStartPosition < file_size)) { + requestedOctetCount = data->type.stream.requestedOctetCount; + if (requestedOctetCount > + octetstring_capacity(&data->fileData[0])) { + requestedOctetCount = octetstring_capacity(&data->fileData[0]); + } + len = bacfile_read_stream_data_callback( + pathname, data->type.stream.fileStartPosition, + octetstring_value(&data->fileData[0]), requestedOctetCount); + if (len < requestedOctetCount) { + data->endOfFile = true; + } else { + data->endOfFile = false; + } } else { - data->endOfFile = false; + data->endOfFile = true; } octetstring_truncate(&data->fileData[0], len); } else { @@ -1093,27 +1100,32 @@ bool bacfile_read_record_data(BACNET_ATOMIC_READ_FILE_DATA *data) pathname = bacfile_pathname(data->object_instance); if (pathname) { found = true; - if (max_records > 0) { - data->endOfFile = false; - for (i = 0; i < max_records; i++) { - status = bacfile_read_record_data_callback( - pathname, data->type.record.fileStartRecord, i, - octetstring_value(&data->fileData[i]), + } + if (found && (data->type.record.fileStartRecord >= 0) && + (data->type.record.fileStartRecord < ARRAY_SIZE(data->fileData)) && + (max_records > 0)) { + data->endOfFile = false; + for (i = 0; i < max_records; i++) { + status = bacfile_read_record_data_callback( + pathname, data->type.record.fileStartRecord, i, + octetstring_value(&data->fileData[i]), + octetstring_capacity(&data->fileData[i])); + if (status) { + /* our records are NULL terminated C strings + read with fgets() */ + len = bacnet_strnlen( + (const char *)octetstring_value(&data->fileData[i]), octetstring_capacity(&data->fileData[i])); - if (status) { - /* our records are NULL terminated C strings - read with fgets() */ - len = bacnet_strnlen( - (const char *)octetstring_value(&data->fileData[i]), - octetstring_capacity(&data->fileData[i])); - octetstring_truncate(&data->fileData[i], len); - } else { - data->endOfFile = true; - data->type.record.RecordCount = i; - break; - } + octetstring_truncate(&data->fileData[i], len); + } else { + data->endOfFile = true; + data->type.record.RecordCount = i; + break; } } + } else { + data->endOfFile = true; + data->type.record.RecordCount = 0; } return found; diff --git a/src/bacnet/basic/service/h_arf.c b/src/bacnet/basic/service/h_arf.c index 79f839c697..69d98c76cf 100644 --- a/src/bacnet/basic/service/h_arf.c +++ b/src/bacnet/basic/service/h_arf.c @@ -92,6 +92,7 @@ void handler_atomic_read_file( BACNET_ADDRESS my_address; BACNET_ERROR_CLASS error_class = ERROR_CLASS_OBJECT; BACNET_ERROR_CODE error_code = ERROR_CODE_UNKNOWN_OBJECT; + BACNET_UNSIGNED_INTEGER file_size; #if PRINT_ENABLED fprintf(stderr, "Received Atomic-Read-File Request!\n"); @@ -116,57 +117,81 @@ void handler_atomic_read_file( } len = arf_decode_service_request(service_request, service_len, &data); /* bad decoding - send an abort */ - if (len < 0) { - len = abort_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, - ABORT_REASON_OTHER, true); - debug_print("ARF: Bad Encoding. Sending Abort!\n"); - goto ARF_ABORT; - } - if (data.object_type == OBJECT_FILE) { - if (!bacfile_valid_instance(data.object_instance)) { + if (!error) { + if (len < 0) { + DEBUG_PRINTF("ARF: Bad Encoding. Sending Abort!\n"); + error_code = ERROR_CODE_ABORT_OTHER; error = true; - } else if (data.access == FILE_STREAM_ACCESS) { - if (data.type.stream.requestedOctetCount <= - octetstring_capacity(&data.fileData[0])) { - bacfile_read_stream_data(&data); - debug_fprintf( - stderr, "ARF: Stream offset %d, %d octets.\n", - (int)data.type.stream.fileStartPosition, - (int)data.type.stream.requestedOctetCount); - len = arf_ack_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, - &data); - } else { - len = abort_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, - ABORT_REASON_SEGMENTATION_NOT_SUPPORTED, true); - debug_fprintf( - stderr, - "ARF: Too Big To Send (%d >= %d). " - "Sending Abort!\n", - (int)data.type.stream.requestedOctetCount, - (int)octetstring_capacity(&data.fileData[0])); - } - } else if (data.access == FILE_RECORD_ACCESS) { - if (data.type.record.RecordCount > BACNET_READ_FILE_RECORD_COUNT) { - error_class = ERROR_CLASS_SERVICES; - error_code = ERROR_CODE_INCONSISTENT_PARAMETERS; - error = true; - } else if ( - data.type.record.fileStartRecord >= - BACNET_READ_FILE_RECORD_COUNT) { - error_class = ERROR_CLASS_SERVICES; - error_code = ERROR_CODE_INVALID_FILE_START_POSITION; + } else if (data.object_type == OBJECT_FILE) { + if (!bacfile_valid_instance(data.object_instance)) { + error_code = ERROR_CODE_UNKNOWN_OBJECT; error = true; - } else if (bacfile_read_record_data(&data)) { - debug_fprintf( - stderr, "ARF: fileStartRecord %d, %u RecordCount.\n", - (int)data.type.record.fileStartRecord, - (unsigned)data.type.record.RecordCount); - len = arf_ack_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, - &data); + } else if (data.access == FILE_STREAM_ACCESS) { + file_size = bacfile_file_size(data.object_instance); + if (file_size > INT32_MAX) { + file_size = INT32_MAX; + } + if ((data.type.stream.fileStartPosition < 0) || + (data.type.stream.fileStartPosition > file_size)) { + /* If the 'File Start Position' parameter is either less + than 0 or exceeds the actual file size, then the appropriate + error is returned in a 'Result(-)' response.*/ + error = true; + error_code = ERROR_CODE_INVALID_FILE_START_POSITION; + } else if ( + data.type.stream.requestedOctetCount <= + octetstring_capacity(&data.fileData[0])) { + bacfile_read_stream_data(&data); + DEBUG_PRINTF( + "ARF: Stream offset %d, %d octets.\n", + (int)data.type.stream.fileStartPosition, + (int)data.type.stream.requestedOctetCount); + len = arf_ack_encode_apdu( + &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, &data); + pdu_len += len; + } else { + error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + error = true; + DEBUG_PRINTF( + "ARF: Too Big To Send (%d >= %d). " + "Sending Abort!\n", + (int)data.type.stream.requestedOctetCount, + (int)octetstring_capacity(&data.fileData[0])); + } + } else if (data.access == FILE_RECORD_ACCESS) { + if (data.type.record.RecordCount > ARRAY_SIZE(data.fileData)) { + DEBUG_PRINTF( + "ARF: RecordCount %u > %u. Sending Reject!\n", + (unsigned)data.type.record.RecordCount, + (unsigned)ARRAY_SIZE(data.fileData)); + error_code = ERROR_CODE_REJECT_PARAMETER_OUT_OF_RANGE; + error = true; + } else if ( + (data.type.record.fileStartRecord < 0) || + (data.type.record.fileStartRecord >= + ARRAY_SIZE(data.fileData))) { + /* If the 'File Start Record' parameter is either less + than 0 or exceeds the actual file size, then the appropriate + error is returned in a 'Result(-)' response.*/ + DEBUG_PRINTF( + "ARF: fileStartRecord %d >= %u. Sending Error!\n", + (int)data.type.record.fileStartRecord, + (unsigned)ARRAY_SIZE(data.fileData)); + error_code = ERROR_CODE_INVALID_FILE_START_POSITION; + error = true; + } else if (bacfile_read_record_data(&data)) { + DEBUG_PRINTF( + "ARF: fileStartRecord %d, %u RecordCount.\n", + (int)data.type.record.fileStartRecord, + (unsigned)data.type.record.RecordCount); + len = arf_ack_encode_apdu( + &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, &data); + pdu_len += len; + } else { + DEBUG_PRINTF("ARF: file_access_denied! Sending Error!"); + error = true; + error_code = ERROR_CODE_FILE_ACCESS_DENIED; + } } else { error = true; error_class = ERROR_CLASS_OBJECT; diff --git a/src/bacnet/basic/service/h_awf.c b/src/bacnet/basic/service/h_awf.c index d332ce15a3..98faa2ae89 100644 --- a/src/bacnet/basic/service/h_awf.c +++ b/src/bacnet/basic/service/h_awf.c @@ -57,18 +57,32 @@ of the BACnet device is a local matter and is not defined by this standard. */ -void handler_atomic_write_file( +/** + * @brief Encode an AtomicWriteFile ACK or Error response into a caller-supplied + * buffer. The public handler_atomic_write_file() wrapper calls this + * function with Handler_Transmit_Buffer so that the encoding logic is + * independently testable. + * @param apdu Output buffer for the encoded PDU (NPDU + APDU), or NULL to + * calculate the required length only. + * @param service_request The raw APDU payload of the received request. + * @param service_len Length of service_request in bytes. + * @param src Source address of the request (used for NPDU encoding). + * @param npdu_data NPDU data structure to populate during encoding. + * @param service_data Confirmed-service metadata from the request. + * @return Total number of bytes written (NPDU + APDU), or 0 on failure. + */ +int handler_atomic_write_file_encode( + uint8_t *apdu, uint8_t *service_request, uint16_t service_len, BACNET_ADDRESS *src, + BACNET_NPDU_DATA *npdu_data, BACNET_CONFIRMED_SERVICE_DATA *service_data) { BACNET_ATOMIC_WRITE_FILE_DATA data; int len = 0; int pdu_len = 0; bool error = false; - int bytes_sent = 0; - BACNET_NPDU_DATA npdu_data; BACNET_ADDRESS my_address; BACNET_ERROR_CLASS error_class = ERROR_CLASS_OBJECT; BACNET_ERROR_CODE error_code = ERROR_CODE_UNKNOWN_OBJECT; @@ -76,57 +90,68 @@ void handler_atomic_write_file( debug_print("Received AtomicWriteFile Request!\n"); /* encode the NPDU portion of the packet */ datalink_get_my_address(&my_address); - npdu_encode_npdu_data(&npdu_data, false, service_data->priority); - pdu_len = npdu_encode_pdu( - &Handler_Transmit_Buffer[0], src, &my_address, &npdu_data); + npdu_encode_npdu_data(npdu_data, false, service_data->priority); + len = npdu_encode_pdu(apdu, src, &my_address, npdu_data); + pdu_len += len; + if (apdu) { + apdu += len; + } if (service_len == 0) { len = reject_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, + apdu, service_data->invoke_id, REJECT_REASON_MISSING_REQUIRED_PARAMETER); debug_print("AWF: Missing Required Parameter. Sending Reject!\n"); - goto AWF_ABORT; + pdu_len += len; + return pdu_len; } else if (service_data->segmented_message) { len = abort_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, + apdu, service_data->invoke_id, ABORT_REASON_SEGMENTATION_NOT_SUPPORTED, true); - debug_print("AWF:Segmented Message. Sending Abort!\n"); - goto AWF_ABORT; + debug_print("AWF: Segmented Message. Sending Abort!\n"); + pdu_len += len; + return pdu_len; } len = awf_decode_service_request(service_request, service_len, &data); /* bad decoding - send an abort */ if (len < 0) { len = abort_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, - ABORT_REASON_OTHER, true); + apdu, service_data->invoke_id, ABORT_REASON_OTHER, true); debug_print("AWF: Bad Encoding. Sending Abort!\n"); - goto AWF_ABORT; + pdu_len += len; + return pdu_len; } if (data.object_type == OBJECT_FILE) { if (!bacfile_valid_instance(data.object_instance)) { error = true; } else if (data.access == FILE_STREAM_ACCESS) { - if (bacfile_write_stream_data(&data)) { + if (data.type.stream.fileStartPosition < -1) { + error = true; + error_class = ERROR_CLASS_SERVICES; + error_code = ERROR_CODE_INVALID_FILE_START_POSITION; + } else if (bacfile_write_stream_data(&data)) { debug_fprintf( stderr, "AWF: Stream offset %d, %d bytes\n", data.type.stream.fileStartPosition, (int)octetstring_length(&data.fileData[0])); - len = awf_ack_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, - &data); + len = awf_ack_encode_apdu(apdu, service_data->invoke_id, &data); + pdu_len += len; } else { error = true; error_class = ERROR_CLASS_OBJECT; error_code = ERROR_CODE_FILE_ACCESS_DENIED; } } else if (data.access == FILE_RECORD_ACCESS) { - if (bacfile_write_record_data(&data)) { + if (data.type.record.fileStartRecord < -1) { + error = true; + error_class = ERROR_CLASS_SERVICES; + error_code = ERROR_CODE_INVALID_FILE_START_POSITION; + } else if (bacfile_write_record_data(&data)) { debug_fprintf( stderr, "AWF: StartRecord %d, RecordCount %u\n", data.type.record.fileStartRecord, data.type.record.returnedRecordCount); - len = awf_ack_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, - &data); + len = awf_ack_encode_apdu(apdu, service_data->invoke_id, &data); + pdu_len += len; } else { error = true; error_class = ERROR_CLASS_OBJECT; @@ -136,7 +161,7 @@ void handler_atomic_write_file( error = true; error_class = ERROR_CLASS_SERVICES; error_code = ERROR_CODE_INVALID_FILE_ACCESS_METHOD; - debug_print("AWF: Record Access Requested. Sending Error!\n"); + debug_print("AWF: Invalid File Access Method. Sending Error!\n"); } } else { error = true; @@ -145,16 +170,38 @@ void handler_atomic_write_file( } if (error) { len = bacerror_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, - SERVICE_CONFIRMED_ATOMIC_WRITE_FILE, error_class, error_code); + apdu, service_data->invoke_id, SERVICE_CONFIRMED_ATOMIC_WRITE_FILE, + error_class, error_code); + pdu_len += len; } -AWF_ABORT: - pdu_len += len; + + return pdu_len; +} + +/** + * @brief Handler for the AtomicWriteFile service. Encodes and sends an ACK or + * Error response based on the provided request and service data. + * @param service_request The APDU portion of the request. + * @param service_len The length of the service_request buffer. + * @param src The source address to send the response to. + * @param service_data The confirmed service data from the request. + */ +void handler_atomic_write_file( + uint8_t *service_request, + uint16_t service_len, + BACNET_ADDRESS *src, + BACNET_CONFIRMED_SERVICE_DATA *service_data) +{ + int pdu_len = 0; + int bytes_sent = 0; + BACNET_NPDU_DATA npdu_data = { 0 }; + + pdu_len = handler_atomic_write_file_encode( + Handler_Transmit_Buffer, service_request, service_len, src, &npdu_data, + service_data); bytes_sent = datalink_send_pdu( src, &npdu_data, &Handler_Transmit_Buffer[0], pdu_len); if (bytes_sent <= 0) { debug_perror("AWF: Failed to send PDU\n"); } - - return; } diff --git a/src/bacnet/basic/service/h_awf.h b/src/bacnet/basic/service/h_awf.h index 142da4f83f..40aeaec47b 100644 --- a/src/bacnet/basic/service/h_awf.h +++ b/src/bacnet/basic/service/h_awf.h @@ -15,11 +15,21 @@ #include "bacnet/bacdef.h" /* BACnet Stack API */ #include "bacnet/apdu.h" +#include "bacnet/npdu.h" #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ +BACNET_STACK_EXPORT +int handler_atomic_write_file_encode( + uint8_t *apdu, + uint8_t *service_request, + uint16_t service_len, + BACNET_ADDRESS *src, + BACNET_NPDU_DATA *npdu_data, + BACNET_CONFIRMED_SERVICE_DATA *service_data); + BACNET_STACK_EXPORT void handler_atomic_write_file( uint8_t *service_request, diff --git a/src/bacnet/basic/sys/bramfs.c b/src/bacnet/basic/sys/bramfs.c index 81efb1eac3..cff4f167ae 100644 --- a/src/bacnet/basic/sys/bramfs.c +++ b/src/bacnet/basic/sys/bramfs.c @@ -150,6 +150,8 @@ bool bacfile_ramfs_file_size_set(const char *pathname, size_t new_size) * @brief Reads stream data from a file * @param pathname - name of the file to read from * @param fileStartPosition - starting position in the file + * If the 'File Start Position' parameter less than 0 + * or exceeds the actual file size, then an error is returned. * @param fileData - data buffer to read into * @param fileDataLen - size of the data buffer * @return number of bytes read, or 0 if not successful @@ -163,8 +165,16 @@ size_t bacfile_ramfs_read_stream_data( struct file_data *pFile; size_t len = 0; + if (fileStartPosition < 0) { + /* invalid file start position */ + return 0; + } pFile = bacfile_ramfs_open(pathname); if (pFile) { + if (fileStartPosition > pFile->size) { + /* invalid file start position */ + return 0; + } if (fileStartPosition + fileDataLen > pFile->size) { /* read only up to the end of the file */ len = pFile->size - fileStartPosition; @@ -180,7 +190,14 @@ size_t bacfile_ramfs_read_stream_data( /** * @brief Writes stream data to a file * @param pathname - name of the file to write to - * @param fileStartPosition - starting position in the file + * @param fileStartPosition - starting position in the file. + * If the 'File Start Position' parameter exceeds the actual file size, + * then the file shall be extended to the size indicated, + * but the contents of any intervening octets or records + * shall be a local matter. + * If this parameter has the special value -1, + * then the write operation shall be treated + * as an append to the current end of file. * @param fileData - data buffer to write from * @param fileDataLen - size of the data buffer * @return number of bytes written, or 0 if not successful @@ -219,7 +236,7 @@ size_t bacfile_ramfs_write_stream_data( memcpy(pFile->data + old_size, fileData, fileDataLen); bytes_written = fileDataLen; } - } else { + } else if (fileStartPosition > 0) { /* open for update */ if (fileStartPosition + fileDataLen > pFile->size) { /* extend the file size */ @@ -296,6 +313,10 @@ static char *record_by_index(char *records, size_t index) * @brief Writes record data to a file * @param pathname - name of the file to write to * @param fileStartRecord - starting record in the file + * If 'File Start Record' parameter has the special + * value -1, then the write operation shall be treated + * as an append to the current end of file, + * and fileIndexRecord can be ignored. * @param fileIndexRecord - index of the record to read * @param fileData - data buffer to read into * @param fileDataLen - size of the data buffer @@ -329,12 +350,15 @@ bool bacfile_ramfs_write_record_data( as an append to the current end of file, and fileIndexRecord can be ignored. */ fileSeekRecord = fileRecordCount; - } else { + } else if (fileStartRecord >= 0) { fileSeekRecord = fileStartRecord + fileIndexRecord; if (fileSeekRecord > fileRecordCount) { /* cannot write more than 1 record beyond the end of the file */ return false; } + } else { + /* invalid file start record */ + return false; } /* sanitize the incoming record; assume from an octetstring */ fileDataStrLen = min(fileDataLen, MAX_OCTET_STRING_BYTES); @@ -386,6 +410,8 @@ bool bacfile_ramfs_write_record_data( * @brief Reads record data from a file * @param pathname - name of the file to read from * @param fileStartRecord - starting record in the file + * If the 'File Start Record' parameter is either less than 0 + * or exceeds the actual file size, then an error is returned. * @param fileIndexRecord - index of the record to read * @param fileData - data buffer to read into * @param fileDataLen - size of the data buffer @@ -404,6 +430,9 @@ bool bacfile_ramfs_read_record_data( char *record; size_t record_len; + if (fileStartRecord < 0) { + return false; /* invalid file start record */ + } pFile = bacfile_ramfs_open(pathname); if (pFile) { fileSeekRecord = fileStartRecord + fileIndexRecord; diff --git a/src/bacnet/basic/sys/bsramfs.c b/src/bacnet/basic/sys/bsramfs.c index 5750c4cec2..6b3a5b3c3a 100644 --- a/src/bacnet/basic/sys/bsramfs.c +++ b/src/bacnet/basic/sys/bsramfs.c @@ -104,6 +104,8 @@ size_t bacfile_sramfs_file_size(const char *pathname) * @brief Reads stream data from a file * @param pathname - name of the file to read from * @param fileStartPosition - starting position in the file + * If the 'File Start Position' parameter less than 0 + * or exceeds the actual file size, then an error is returned. * @param fileData - data buffer to read into * @param fileDataLen - size of the data buffer * @return number of bytes read, or 0 if not successful @@ -117,8 +119,16 @@ size_t bacfile_sramfs_read_stream_data( struct bacnet_file_sramfs_data *pFile; size_t len = 0; + if (fileStartPosition < 0) { + /* invalid file start position */ + return 0; + } pFile = bacfile_sramfs_open(pathname); if (pFile) { + if (fileStartPosition > pFile->size) { + /* invalid file start position */ + return 0; + } if (fileStartPosition + fileDataLen > pFile->size) { /* read only up to the end of the file */ len = pFile->size - fileStartPosition; @@ -162,6 +172,8 @@ static char *record_by_index(char *records, size_t index) * @brief Reads record data from a file * @param pathname - name of the file to read from * @param fileStartRecord - starting record in the file + * If the 'File Start Record' parameter is either less than 0 + * or exceeds the actual file size, then an error is returned. * @param fileIndexRecord - index of the record to read * @param fileData - data buffer to read into * @param fileDataLen - size of the data buffer @@ -180,6 +192,9 @@ bool bacfile_sramfs_read_record_data( char *record; size_t record_len; + if (fileStartRecord < 0) { + return false; /* invalid file start record */ + } pFile = bacfile_sramfs_open(pathname); if (pFile) { fileSeekRecord = fileStartRecord + fileIndexRecord; diff --git a/src/bacnet/version.h b/src/bacnet/version.h index 85e429fb68..9755916d95 100644 --- a/src/bacnet/version.h +++ b/src/bacnet/version.h @@ -15,7 +15,7 @@ #define BACNET_VERSION(x, y, z) (((x) << 16) + ((y) << 8) + (z)) #endif -#define BACNET_VERSION_TEXT "1.5.1-rc2" +#define BACNET_VERSION_TEXT "1.5.1-rc3" #define BACNET_VERSION_CODE BACNET_VERSION(1, 5, 1) #define BACNET_VERSION_MAJOR ((BACNET_VERSION_CODE >> 16) & 0xFF) #define BACNET_VERSION_MINOR ((BACNET_VERSION_CODE >> 8) & 0xFF) diff --git a/test/bacnet/basic/sys/bramfs/src/main.c b/test/bacnet/basic/sys/bramfs/src/main.c index 204ce0e657..68aa48b4ca 100644 --- a/test/bacnet/basic/sys/bramfs/src/main.c +++ b/test/bacnet/basic/sys/bramfs/src/main.c @@ -207,6 +207,99 @@ static void test_BRAMFS_records(void) bacfile_ramfs_deinit(); } +/** + * @brief Unit Test for invalid file start positions in stream access + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bramfs_tests, test_BRAMFS_invalid_stream_positions) +#else +static void test_BRAMFS_invalid_stream_positions(void) +#endif +{ + const char *pathname = "testfile_invalid.txt"; + uint8_t file_data[] = { "ABCDEFGHIJ" }; + uint8_t read_buf[32] = { 0 }; + size_t len = 0; + + bacfile_ramfs_init(); + + /* write initial data starting at position 0 */ + len = bacfile_ramfs_write_stream_data( + pathname, 0, file_data, sizeof(file_data)); + zassert_equal( + len, sizeof(file_data), "Initial write should succeed, len=%zu", len); + + /* read with negative file start position must return 0 */ + len = bacfile_ramfs_read_stream_data( + pathname, -1, read_buf, sizeof(read_buf)); + zassert_equal( + len, 0, "Read with fileStartPosition=-1 must return 0, got %zu", len); + + len = bacfile_ramfs_read_stream_data( + pathname, -100, read_buf, sizeof(read_buf)); + zassert_equal( + len, 0, "Read with fileStartPosition=-100 must return 0, got %zu", len); + + /* read with start position exceeding file size must return 0 */ + len = bacfile_ramfs_read_stream_data( + pathname, (int32_t)sizeof(file_data) + 1, read_buf, sizeof(read_buf)); + zassert_equal( + len, 0, + "Read with fileStartPosition beyond file size must return 0, got %zu", + len); + + /* write with an invalid position (not -1, 0, or >0) must return 0 */ + len = bacfile_ramfs_write_stream_data( + pathname, -2, file_data, sizeof(file_data)); + zassert_equal( + len, 0, "Write with fileStartPosition=-2 must return 0, got %zu", len); + + bacfile_ramfs_deinit(); +} + +/** + * @brief Unit Test for invalid file start records in record access + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bramfs_tests, test_BRAMFS_invalid_record_positions) +#else +static void test_BRAMFS_invalid_record_positions(void) +#endif +{ + const char *pathname = "testfile_rec_invalid.txt"; + char record_1[] = { "First record." }; + uint8_t read_buf[MAX_OCTET_STRING_BYTES] = { 0 }; + size_t record_len = 0; + bool status = false; + + bacfile_ramfs_init(); + + /* write first record so file is non-empty */ + record_len = bacnet_strnlen(record_1, sizeof(record_1)); + status = bacfile_ramfs_write_record_data( + pathname, 0, 0, (const uint8_t *)record_1, record_len); + zassert_true(status, "Initial write_record should succeed"); + + /* write_record with fileStartRecord < -1 must return false */ + status = bacfile_ramfs_write_record_data( + pathname, -2, 0, (const uint8_t *)record_1, record_len); + zassert_false( + status, "write_record_data with fileStartRecord=-2 must return false"); + + /* read_record with negative fileStartRecord must return false */ + status = bacfile_ramfs_read_record_data( + pathname, -1, 0, read_buf, sizeof(read_buf)); + zassert_false( + status, "read_record_data with fileStartRecord=-1 must return false"); + + status = bacfile_ramfs_read_record_data( + pathname, -2, 0, read_buf, sizeof(read_buf)); + zassert_false( + status, "read_record_data with fileStartRecord=-2 must return false"); + + bacfile_ramfs_deinit(); +} + /** * @} */ @@ -218,7 +311,9 @@ void test_main(void) { ztest_test_suite( bramfs_tests, ztest_unit_test(test_BRAMFS_stream), - ztest_unit_test(test_BRAMFS_records)); + ztest_unit_test(test_BRAMFS_records), + ztest_unit_test(test_BRAMFS_invalid_stream_positions), + ztest_unit_test(test_BRAMFS_invalid_record_positions)); ztest_run_test_suite(bramfs_tests); } diff --git a/test/bacnet/basic/sys/bsramfs/src/main.c b/test/bacnet/basic/sys/bsramfs/src/main.c index 556ffeb4c6..645f187062 100644 --- a/test/bacnet/basic/sys/bsramfs/src/main.c +++ b/test/bacnet/basic/sys/bsramfs/src/main.c @@ -125,6 +125,65 @@ static void test_BSRAMFS_records(void) zassert_true(status, "Read record 3 should succeed"); } +/** + * @brief Unit Test for invalid file start positions in BSRAMFS stream/record + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bramfs_tests, test_BSRAMFS_invalid_positions) +#else +static void test_BSRAMFS_invalid_positions(void) +#endif +{ + uint8_t raw_data[] = { "HELLO" }; + /* records: two null-terminated strings */ + char rec_data[] = "First record.\0Second record."; + struct bacnet_file_sramfs_data stream_file = { sizeof(raw_data), + (char *)raw_data, + "invalid_stream.txt", NULL }; + struct bacnet_file_sramfs_data record_file = { sizeof(rec_data), rec_data, + "invalid_record.txt", NULL }; + uint8_t read_buf[64] = { 0 }; + size_t len = 0; + bool status = false; + + bacfile_sramfs_init(); + zassert_true(bacfile_sramfs_add(&stream_file), "Failed to add stream_file"); + zassert_true(bacfile_sramfs_add(&record_file), "Failed to add record_file"); + + /* read_stream with negative start position must return 0 */ + len = bacfile_sramfs_read_stream_data( + stream_file.pathname, -1, read_buf, sizeof(read_buf)); + zassert_equal( + len, 0, "read_stream with fileStartPosition=-1 must return 0, got %zu", + len); + + len = bacfile_sramfs_read_stream_data( + stream_file.pathname, -100, read_buf, sizeof(read_buf)); + zassert_equal( + len, 0, + "read_stream with fileStartPosition=-100 must return 0, got %zu", len); + + /* read_stream with start position exceeding file size must return 0 */ + len = bacfile_sramfs_read_stream_data( + stream_file.pathname, (int32_t)stream_file.size + 1, read_buf, + sizeof(read_buf)); + zassert_equal( + len, 0, + "read_stream with position beyond file size must return 0, got %zu", + len); + + /* read_record with negative fileStartRecord must return false */ + status = bacfile_sramfs_read_record_data( + record_file.pathname, -1, 0, read_buf, sizeof(read_buf)); + zassert_false( + status, "read_record with fileStartRecord=-1 must return false"); + + status = bacfile_sramfs_read_record_data( + record_file.pathname, -2, 0, read_buf, sizeof(read_buf)); + zassert_false( + status, "read_record with fileStartRecord=-2 must return false"); +} + /** * @} */ @@ -136,7 +195,8 @@ void test_main(void) { ztest_test_suite( bramfs_tests, ztest_unit_test(test_BSRAMFS_stream), - ztest_unit_test(test_BSRAMFS_records)); + ztest_unit_test(test_BSRAMFS_records), + ztest_unit_test(test_BSRAMFS_invalid_positions)); ztest_run_test_suite(bramfs_tests); } From b4d2895e47c7c4439ed069972361572862f83871 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 11:22:32 -0500 Subject: [PATCH 24/42] Add .agents to .gitignore to exclude agent files from version control --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ebb160150b..3cf2b20987 100644 --- a/.gitignore +++ b/.gitignore @@ -113,3 +113,4 @@ apps/piface/libpifacedigital/ *.vcxproj.user .venv/ +.agents From 8b3ae153fe54d84f5cc49215a56b86433061c699 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 11:23:19 -0500 Subject: [PATCH 25/42] Update SECURITY.md to include detailed vulnerability records and patch information for CVE-2026 series --- SECURITY.md | 117 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 103 insertions(+), 14 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 1875c7488a..39450ae5ab 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,51 +27,140 @@ cybersecurity vulnerabilities. Here are the published vulnerability records for v1.5.x: -AtomicReadFile/AtomicWriteFile stream fileStartPosition validation flaw causes out-of-bounds read/write in RAM file backends -[GHSA-8759-hx7g-94qx](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8759-hx7g-94qx) +[CVE-2026-49990](https://www.cve.org/CVERecord?id=CVE-2026-49990) - +AtomicReadFile/AtomicWriteFile stream fileStartPosition validation flaw causes out-of-bounds read/write in RAM file backends. +[GHSA-8759-hx7g-94qx](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8759-hx7g-94qx). +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1362](https://github.com/bacnet-stack/bacnet-stack/pull/1362). [CVE-2026-47710](https://www.cve.org/CVERecord?id=CVE-2026-47710) - -Loop reference to long Structured View description causes stack-buffer-overflow -[GHSA-2xm5-gjpc-9m6q](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-2xm5-gjpc-9m6q) +Loop reference to long Structured View description causes stack-buffer-overflow. +[GHSA-2xm5-gjpc-9m6q](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-2xm5-gjpc-9m6q). +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1355](https://github.com/bacnet-stack/bacnet-stack/pull/1355). [CVE-2026-47711](https://www.cve.org/CVERecord?id=CVE-2026-47711) - -Stack buffer overflow in Loop internal ReadProperty path via Life_Safety_Zone accepted-modes property -[GHSA-vrpm-9gm2-x552](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-vrpm-9gm2-x552) +Stack buffer overflow in Loop internal ReadProperty path via Life_Safety_Zone accepted-modes property. +[GHSA-vrpm-9gm2-x552](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-vrpm-9gm2-x552). +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1354](https://github.com/bacnet-stack/bacnet-stack/pull/1354), +[#1355](https://github.com/bacnet-stack/bacnet-stack/pull/1355). [CVE-2026-47259](https://www.cve.org/CVERecord?id=CVE-2026-47259) - -Stack-based buffer overflow in Notification Class RemoveListElement recipient-list decoding -[GHSA-9w9m-w7w5-rrv3](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-9w9m-w7w5-rrv3) +Stack-based buffer overflow in Notification Class RemoveListElement recipient-list decoding. +[GHSA-9w9m-w7w5-rrv3](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-9w9m-w7w5-rrv3). +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1353](https://github.com/bacnet-stack/bacnet-stack/pull/1353). [CVE-2026-47258](https://www.cve.org/CVERecord?id=CVE-2026-47258) - -Stack-based buffer overflow in Notification Class AddListElement recipient-list decoding -[GHSA-rjmv-3mcm-r83j](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-rjmv-3mcm-r83j) +Stack-based buffer overflow in Notification Class AddListElement recipient-list decoding. +[GHSA-rjmv-3mcm-r83j](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-rjmv-3mcm-r83j). +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1353](https://github.com/bacnet-stack/bacnet-stack/pull/1353). [CVE-2026-47257](https://www.cve.org/CVERecord?id=CVE-2026-47257) - -ReinitializeDevice ENDRESTORE can delete existing objects on an empty restore file and still return success -[GHSA-x6pp-3pf3-f87r](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-x6pp-3pf3-f87r) +ReinitializeDevice ENDRESTORE can delete existing objects on an empty restore file and still return success. +[GHSA-x6pp-3pf3-f87r](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-x6pp-3pf3-f87r). +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1352](https://github.com/bacnet-stack/bacnet-stack/pull/1352). -Uncontrolled recursion in Timer object writeback path leads to remote server stack overflow -[GHSA-7r8r-2rj2-5wvr](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-7r8r-2rj2-5wvr) +[CVE-2026-49341](https://www.cve.org/CVERecord?id=CVE-2026-49341) - +Uncontrolled recursion in Timer object writeback path leads to remote server stack overflow. +[GHSA-7r8r-2rj2-5wvr](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-7r8r-2rj2-5wvr). +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1347](https://github.com/bacnet-stack/bacnet-stack/pull/1347) [CVE-2026-47217](https://www.cve.org/CVERecord?id=CVE-2026-47217) - Channel member self-reference causes uncontrolled recursion and stack overflow in default BACnet/IP server [GHSA-wjw5-q9g6-2764](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-wjw5-q9g6-2764) +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1345](https://github.com/bacnet-stack/bacnet-stack/pull/1345). [CVE-2026-46677](https://www.cve.org/CVERecord?id=CVE-2026-46677) - Client-Side Out-of-Bounds Read in AtomicReadFile-ACK Record-Access Handling via RecordCount / fileData[] Mismatch [GHSA-rv5h-cxwq-q3mh](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-rv5h-cxwq-q3mh) +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1344](https://github.com/bacnet-stack/bacnet-stack/pull/1344). [CVE-2026-46676](https://www.cve.org/CVERecord?id=CVE-2026-46676) - Uninitialized Value Use in AtomicReadFile-ACK Record-Access Encoder Causes Response Corruption and Conditional Information Disclosure [GHSA-2fwp-32cj-g3x4](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-2fwp-32cj-g3x4) +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1344](https://github.com/bacnet-stack/bacnet-stack/pull/1344). [CVE-2026-46674](https://www.cve.org/CVERecord?id=CVE-2026-46674) - Out-of-Bounds Read in AtomicWriteFile Record Decoder via Unbounded returnedRecordCount [GHSA-8384-pwhh-cxjh](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8384-pwhh-cxjh) +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1344](https://github.com/bacnet-stack/bacnet-stack/pull/1344). [CVE-2026-45265](https://www.cve.org/CVERecord?id=CVE-2026-45265) - Atomic-Read-File RecordCount Stack-Based Out-of-Bounds Write [GHSA-v3gx-mwrp-xvh5](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-v3gx-mwrp-xvh5) +Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Pull Request: [#1340](https://github.com/bacnet-stack/bacnet-stack/pull/1340). + +[CVE-2026-40279](https://www.cve.org/CVERecord?id=CVE-2026-40279) - +Undefined-behavior signed left shift in `decode_signed32()` +[GHSA-326g-j95f-gmxv](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-326g-j95f-gmxv) +Patched versions: [1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) +Pull Request: [#1300](https://github.com/bacnet-stack/bacnet-stack/pull/1300) + +[CVE-2026-41503](https://www.cve.org/CVERecord?id=CVE-2026-41503) - +Out-of-Bounds Read in ReadPropertyMultiple Property Decoder via Deprecated Tag Parser +[GHSA-5w2v-mwqj-pr2c](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-5w2v-mwqj-pr2c) +Patched versions: +[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) +Pull Request: +[#1244](https://github.com/bacnet-stack/bacnet-stack/pull/1244) + +[CVE-2026-41502](https://www.cve.org/CVERecord?id=CVE-2026-41502) - +Off-by-One Out-of-Bounds Read in ReadPropertyMultiple Object ID Decoder +[GHSA-7545-3fpx-4xw3](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-7545-3fpx-4xw3) +Patched versions: +[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) +Pull Request: +[#1244](https://github.com/bacnet-stack/bacnet-stack/pull/1244) + +[CVE-2026-41475](https://www.cve.org/CVERecord?id=CVE-2026-41475) - +Out-of-Bounds Read in WritePropertyMultiple Decoder via Deprecated Tag Parser +[GHSA-cvv4-v3g6-4jmv](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-cvv4-v3g6-4jmv) +Patched versions: +[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) +Pull Request: +[#1244](https://github.com/bacnet-stack/bacnet-stack/pull/1244) + +[CVE-2026-26264](https://www.cve.org/CVERecord?id=CVE-2026-26264) - +WriteProperty decoding length underflow leads to OOB read and crash +[GHSA-phjh-v45p-gmjj](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-phjh-v45p-gmjj) +Patched versions: +[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) +Pull Request: +[#1231](https://github.com/bacnet-stack/bacnet-stack/pull/1231) + +[CVE-2026-21870](https://www.cve.org/CVERecord?id=CVE-2026-21870) - +Off-by-one Stack-based Buffer Overflow in tokenizer_string +[GHSA-pc83-wp6w-93mx](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-pc83-wp6w-93mx) +Patched versions: +[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) +Pull Request: +[#1196](https://github.com/bacnet-stack/bacnet-stack/pull/1196) + +[CVE-2026-21878](https://www.cve.org/CVERecord?id=CVE-2026-21878) - +Improper Limitation of a Pathname to a Restricted Directory +[GHSA-p8rx-c26w-545j](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-p8rx-c26w-545j) +Patched versions: +[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) +Pull Request: +[#1197](https://github.com/bacnet-stack/bacnet-stack/pull/1197) + +[CVE-2025-66624](https://www.cve.org/CVERecord?id=CVE-2025-66624) - +BACnet-stack MS/TP reply matcher OOB read +[GHSA-8wgw-5h6x-qgqg](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8wgw-5h6x-qgqg) +Patched versions: +[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) +Pull Request: +[#1178](https://github.com/bacnet-stack/bacnet-stack/pull/1178) ## Reporting a Vulnerability From 6f1b10d62866e1a46786fd1089a5095efd4d4906 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 11:31:16 -0500 Subject: [PATCH 26/42] Bugfix/address list encoder buffer overrun (#1363) * Fix buffer overrun in address_list_encode() function by using existing BACnetAddressBinding encoding function and refactoring. Added unit test for validation. --- CHANGELOG.md | 3 + SECURITY.md | 6 + src/bacnet/bacaddr.c | 40 +++-- src/bacnet/bacaddr.h | 3 + src/bacnet/basic/binding/address.c | 81 +++------ .../basic/object/client/device-client.c | 9 +- src/bacnet/basic/object/device.c | 5 + src/bacnet/basic/server/bacnet_device.c | 5 + test/bacnet/basic/binding/address/src/main.c | 164 +++++++++++++++++- 9 files changed, 244 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a90d1c4ac..d83eb0bdbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ The git repositories are hosted at the following sites: ### Security +* Secured address_list_encode() function buffer overrun by using + existing BACnetAddressBinding encoding function for length check + and refactoring. Added unit test for validation.(#1363) * Secured AtomicReadFile/AtomicWriteFile logic and underlying file backends by adding explicit invalid-start-position guards which reject negative and out-of-range positions/records. (#1362) diff --git a/SECURITY.md b/SECURITY.md index 39450ae5ab..8f72693b90 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,6 +27,12 @@ cybersecurity vulnerabilities. Here are the published vulnerability records for v1.5.x: +[CVE-2026-52787](https://www.cve.org/CVERecord?id=CVE-2026-52787) - +Global APDU transmit buffer out-of-bounds write in device.address-binding response encoding via address_list_encode() +[GHSA-4fgg-fghm-jm43](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4fgg-fghm-jm43). +Patched versions: [1.6.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.6.0). +Pull Request: [#1363](https://github.com/bacnet-stack/bacnet-stack/pull/1363). + [CVE-2026-49990](https://www.cve.org/CVERecord?id=CVE-2026-49990) - AtomicReadFile/AtomicWriteFile stream fileStartPosition validation flaw causes out-of-bounds read/write in RAM file backends. [GHSA-8759-hx7g-94qx](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8759-hx7g-94qx). diff --git a/src/bacnet/bacaddr.c b/src/bacnet/bacaddr.c index a48ca86ce7..1da62889aa 100644 --- a/src/bacnet/bacaddr.c +++ b/src/bacnet/bacaddr.c @@ -772,6 +772,32 @@ bool bacnet_vmac_address_set(BACNET_ADDRESS *addr, uint32_t device_id) return status; } +/** + * @brief Encode the BACnetAddressBinding entry + * @param apdu Pointer to the APDU, or NULL for length calculation. + * @param device_id Device ID to encode. + * @param address Pointer to the BACnet address to encode. + * @return Count of encoded bytes. + */ +int bacnet_address_binding_entry_encode( + uint8_t *apdu, uint32_t device_id, const BACNET_ADDRESS *address) +{ + int len = 0, apdu_len = 0; + + if (!address) { + return 0; + } + len = encode_application_object_id(apdu, OBJECT_DEVICE, device_id); + apdu_len += len; + if (apdu) { + apdu += len; + } + len = encode_bacnet_address(apdu, address); + apdu_len += len; + + return apdu_len; +} + /** * @brief Encode a given BACnetAddressBinding * @details @@ -785,21 +811,11 @@ bool bacnet_vmac_address_set(BACNET_ADDRESS *addr, uint32_t device_id) int bacnet_address_binding_type_encode( uint8_t *apdu, const BACNET_ADDRESS_BINDING *value) { - int apdu_len = 0, len = 0; - if (!value) { return 0; } - len = encode_application_object_id( - apdu, OBJECT_DEVICE, value->device_identifier); - apdu_len += len; - if (apdu) { - apdu += len; - } - len = encode_bacnet_address(apdu, &value->device_address); - apdu_len += len; - - return apdu_len; + return bacnet_address_binding_entry_encode( + apdu, value->device_identifier, &value->device_address); } /** diff --git a/src/bacnet/bacaddr.h b/src/bacnet/bacaddr.h index d566f4ff71..ad69dc7a6f 100644 --- a/src/bacnet/bacaddr.h +++ b/src/bacnet/bacaddr.h @@ -109,6 +109,9 @@ int bacnet_vmac_entry_decode( BACNET_STACK_EXPORT bool bacnet_vmac_address_set(BACNET_ADDRESS *addr, uint32_t device_id); +BACNET_STACK_EXPORT +int bacnet_address_binding_entry_encode( + uint8_t *apdu, uint32_t device_id, const BACNET_ADDRESS *address); BACNET_STACK_EXPORT int bacnet_address_binding_type_encode( uint8_t *apdu, const BACNET_ADDRESS_BINDING *value); diff --git a/src/bacnet/basic/binding/address.c b/src/bacnet/basic/binding/address.c index 48ba0d3d68..b031d1308b 100644 --- a/src/bacnet/basic/binding/address.c +++ b/src/bacnet/basic/binding/address.c @@ -753,59 +753,46 @@ unsigned address_count(void) * property. Basically encode the address list to be send out. * * @param apdu Pointer to the APDU - * @param apdu_len Remaining buffer size. + * @param apdu_size Remaining buffer size. * * @return Count of encoded bytes. */ -int address_list_encode(uint8_t *apdu, unsigned apdu_len) +int address_list_encode(uint8_t *apdu, unsigned apdu_size) { - int iLen = 0; + int len = 0, apdu_len = 0; struct Address_Cache_Entry *pMatch; - BACNET_OCTET_STRING MAC_Address; unsigned index; - /* Look for matching address. */ + /* determine the length of the encoded address list */ for (index = 0; index < MAX_ADDRESS_CACHE; index++) { pMatch = &Address_Cache[index]; if ((pMatch->Flags & (BAC_ADDR_IN_USE | BAC_ADDR_BIND_REQ)) == BAC_ADDR_IN_USE) { - iLen += encode_application_object_id( - &apdu[iLen], OBJECT_DEVICE, pMatch->device_id); - iLen += - encode_application_unsigned(&apdu[iLen], pMatch->address.net); - if ((unsigned)iLen >= apdu_len) { - break; - } - - /* pick the appropriate type of entry from the cache */ - - if (pMatch->address.len != 0) { - /* BAC */ - if ((unsigned)(iLen + pMatch->address.len) >= apdu_len) { - break; - } - octetstring_init( - &MAC_Address, pMatch->address.adr, pMatch->address.len); - iLen += - encode_application_octet_string(&apdu[iLen], &MAC_Address); - } else { - /* MAC*/ - if ((unsigned)(iLen + pMatch->address.mac_len) >= apdu_len) { - break; + /* encode matching addresses */ + len = bacnet_address_binding_entry_encode( + NULL, pMatch->device_id, &pMatch->address); + apdu_len += len; + } + } + /* encode the address list if there is enough space */ + if (apdu) { + if (apdu_len > (int)apdu_size) { + apdu_len = BACNET_STATUS_ABORT; + } else { + for (index = 0; index < MAX_ADDRESS_CACHE; index++) { + pMatch = &Address_Cache[index]; + if ((pMatch->Flags & (BAC_ADDR_IN_USE | BAC_ADDR_BIND_REQ)) == + BAC_ADDR_IN_USE) { + /* encode matching addresses */ + len = bacnet_address_binding_entry_encode( + apdu, pMatch->device_id, &pMatch->address); + apdu += len; } - octetstring_init( - &MAC_Address, pMatch->address.mac, pMatch->address.mac_len); - iLen += - encode_application_octet_string(&apdu[iLen], &MAC_Address); - } - /* Any space left? */ - if ((unsigned)iLen >= apdu_len) { - break; } } } - return (iLen); + return apdu_len; } /** @@ -842,7 +829,6 @@ int rr_address_list_encode(uint8_t *apdu, BACNET_READ_RANGE_DATA *pRequest) int iLen = 0; int32_t iTemp = 0; struct Address_Cache_Entry *pMatch = NULL; - BACNET_OCTET_STRING MAC_Address; uint32_t uiTotal = 0; /* Number of bound entries in the cache */ uint32_t uiIndex = 0; /* Current entry number */ uint32_t uiFirst = 0; /* Entry number we started encoding from */ @@ -958,23 +944,8 @@ int rr_address_list_encode(uint8_t *apdu, BACNET_READ_RANGE_DATA *pRequest) &pRequest->ResultFlags, RESULT_FLAG_MORE_ITEMS, true); break; } - iTemp = (int32_t)encode_application_object_id( - &apdu[iLen], OBJECT_DEVICE, pMatch->device_id); - iTemp += encode_application_unsigned( - &apdu[iLen + iTemp], pMatch->address.net); - - /* pick the appropriate type of entry from the cache */ - if (pMatch->address.len != 0) { - octetstring_init( - &MAC_Address, pMatch->address.adr, pMatch->address.len); - iTemp += encode_application_octet_string( - &apdu[iLen + iTemp], &MAC_Address); - } else { - octetstring_init( - &MAC_Address, pMatch->address.mac, pMatch->address.mac_len); - iTemp += encode_application_octet_string( - &apdu[iLen + iTemp], &MAC_Address); - } + iTemp = bacnet_address_binding_entry_encode( + &apdu[iLen], pMatch->device_id, &pMatch->address); /* Reduce the remaining space */ uiRemaining -= iTemp; /* and increase the length consumed */ diff --git a/src/bacnet/basic/object/client/device-client.c b/src/bacnet/basic/object/client/device-client.c index 01b2c31c59..7f18dd1c54 100644 --- a/src/bacnet/basic/object/client/device-client.c +++ b/src/bacnet/basic/object/client/device-client.c @@ -1210,9 +1210,12 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) apdu_len = encode_application_unsigned(&apdu[0], apdu_retries()); break; case PROP_DEVICE_ADDRESS_BINDING: - /* FIXME: the real max apdu remaining should be passed into function - */ - apdu_len = address_list_encode(&apdu[0], MAX_APDU); + apdu_len = address_list_encode(&apdu[0], apdu_size); + if (apdu_len < 0) { + rpdata->error_code = + ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + apdu_len = BACNET_STATUS_ABORT; + } break; case PROP_DATABASE_REVISION: apdu_len = encode_application_unsigned(&apdu[0], Database_Revision); diff --git a/src/bacnet/basic/object/device.c b/src/bacnet/basic/object/device.c index 5d97a24420..03205a6f52 100644 --- a/src/bacnet/basic/object/device.c +++ b/src/bacnet/basic/object/device.c @@ -2887,6 +2887,11 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) break; case PROP_DEVICE_ADDRESS_BINDING: apdu_len = address_list_encode(&apdu[0], apdu_max); + if (apdu_len < 0) { + rpdata->error_code = + ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + apdu_len = BACNET_STATUS_ABORT; + } break; case PROP_DATABASE_REVISION: apdu_len = encode_application_unsigned(&apdu[0], Database_Revision); diff --git a/src/bacnet/basic/server/bacnet_device.c b/src/bacnet/basic/server/bacnet_device.c index 271cf187cd..f496824708 100644 --- a/src/bacnet/basic/server/bacnet_device.c +++ b/src/bacnet/basic/server/bacnet_device.c @@ -2748,6 +2748,11 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) break; case PROP_DEVICE_ADDRESS_BINDING: apdu_len = address_list_encode(&apdu[0], apdu_max); + if (apdu_len < 0) { + rpdata->error_code = + ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; + apdu_len = BACNET_STATUS_ABORT; + } break; case PROP_DATABASE_REVISION: apdu_len = encode_application_unsigned( diff --git a/test/bacnet/basic/binding/address/src/main.c b/test/bacnet/basic/binding/address/src/main.c index 6971f728e4..c89605a965 100644 --- a/test/bacnet/basic/binding/address/src/main.c +++ b/test/bacnet/basic/binding/address/src/main.c @@ -5,6 +5,7 @@ * @date 2004 * @copyright SPDX-License-Identifier: MIT */ +#include #include #include #include @@ -262,6 +263,163 @@ static void testAddress(void) zassert_equal(count, (MAX_ADDRESS_CACHE - i - 1), NULL); } } +/** + * @brief Test address_list_encode for correct return value and no buffer + * overrun. + * + * The function has two phases: + * 1. A length-calculation pass (apdu == NULL) to find the total bytes needed. + * 2. An encoding pass that must write exactly those bytes into the caller's + * buffer without exceeding it. + * + * Bugs guarded against: + * - Writing past the end of a buffer whose size was obtained from the + * NULL-apdu length-calculation call (original overrun). + * - Double-counting encoded bytes in the return value when apdu != NULL + * (second-loop len accumulation bug). + * - Advancing the apdu pointer by the accumulated total rather than by the + * per-entry size, which shifts subsequent entries to wrong offsets and + * can push the write pointer past the buffer end. + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(address_tests, test_address_list_encode) +#else +static void test_address_list_encode(void) +#endif +{ + /* Large backing array; guard region sits immediately after apdu_len bytes + */ + uint8_t apdu[1024]; + BACNET_ADDRESS src = { 0 }; + BACNET_ADDRESS addr = { 0 }; + int len_null = 0; + int len_encoded = 0; + unsigned apdu_len = 0; + unsigned i = 0; + uint32_t device_id = 0; + unsigned max_apdu_size = 0; + + /* Reset the cache. In BACNET_ADDRESS_CACHE_FILE builds address_init() + * re-reads a file left by an earlier test, so drain any loaded entries + * explicitly so the "empty cache" assertions below are reliable. */ + address_init(); + for (i = 0; i < MAX_ADDRESS_CACHE; i++) { + if (address_get_by_index(i, &device_id, &max_apdu_size, &addr)) { + address_remove_device(device_id); + } + } + zassert_equal(address_count(), 0, "Cache must be empty at test start"); + + /* ------------------------------------------------------------------ */ + /* empty cache */ + /* ------------------------------------------------------------------ */ + len_null = address_list_encode(NULL, 0); + zassert_equal(len_null, 0, "Empty cache: length-only call must return 0"); + + len_encoded = address_list_encode(apdu, sizeof(apdu)); + zassert_equal(len_encoded, 0, "Empty cache: encode call must return 0"); + + /* ------------------------------------------------------------------ */ + /* single MAC-layer entry (no routing, net == 0) */ + /* ------------------------------------------------------------------ */ + src.mac_len = 1; + src.mac[0] = 0x05; + src.net = 0; + src.len = 0; + address_add(1001, 480, &src); + zassert_equal(address_count(), 1, NULL); + + /* length-only call must be positive */ + len_null = address_list_encode(NULL, 0); + zassert_true(len_null > 0, "Single entry: length-only call must be > 0"); + + /* encode into a large buffer: return value must equal length-only result */ + memset(apdu, 0, sizeof(apdu)); + len_encoded = address_list_encode(apdu, sizeof(apdu)); + zassert_equal( + len_encoded, len_null, + "Single entry: encoded length must match length-only call"); + + /* too-small buffer: must fail without writing */ + apdu_len = (unsigned)len_null - 1; + memset(apdu, 0xAA, sizeof(apdu)); + len_encoded = address_list_encode(apdu, apdu_len); + zassert_equal( + len_encoded, BACNET_STATUS_ABORT, + "Single entry: too-small buffer must return BACNET_STATUS_ABORT"); + zassert_equal( + apdu[0], 0xAA, "Single entry: too-small buffer must not be written"); + + /* exactly-right-sized buffer: guard bytes after it must survive */ + apdu_len = (unsigned)len_null; + memset(apdu, 0, sizeof(apdu)); + memset(apdu + apdu_len, 0xBB, 8); + len_encoded = address_list_encode(apdu, apdu_len); + zassert_equal( + len_encoded, len_null, + "Single entry: exact-sized buffer must return correct length"); + for (i = 0; i < 8; i++) { + zassert_equal( + apdu[apdu_len + i], 0xBB, + "Single entry: guard bytes must not be overwritten"); + } + + /* ------------------------------------------------------------------ */ + /* two entries: this is the primary regression case for the overrun. */ + /* With the second-loop bug, `apdu += len` advances the write pointer */ + /* by the ever-growing accumulated total instead of by the per-entry */ + /* size. When the buffer is exactly the right size the second entry */ + /* is written past the end of the buffer. */ + /* ------------------------------------------------------------------ */ + src.mac_len = 6; + src.mac[0] = 0xC0; + src.mac[1] = 0xA8; + src.mac[2] = 0x00; + src.mac[3] = 0x18; + src.mac[4] = 0xBA; + src.mac[5] = 0xC0; + src.net = 26001; + src.len = 1; + src.adr[0] = 0x19; + address_add(2002, 480, &src); + zassert_equal(address_count(), 2, NULL); + + /* length-only call with 2 entries */ + len_null = address_list_encode(NULL, 0); + zassert_true(len_null > 0, "Two entries: length-only call must be > 0"); + + /* encode into a large buffer: return value must equal length-only result */ + memset(apdu, 0, sizeof(apdu)); + len_encoded = address_list_encode(apdu, sizeof(apdu)); + zassert_equal( + len_encoded, len_null, + "Two entries: encoded length must match length-only call"); + + /* exactly-right-sized buffer: guard bytes must survive */ + apdu_len = (unsigned)len_null; + memset(apdu, 0, sizeof(apdu)); + memset(apdu + apdu_len, 0xBB, 8); + len_encoded = address_list_encode(apdu, apdu_len); + zassert_equal( + len_encoded, len_null, + "Two entries: exact-sized buffer must return correct length"); + for (i = 0; i < 8; i++) { + zassert_equal( + apdu[apdu_len + i], 0xBB, + "Two entries: guard bytes must not be overwritten"); + } + + /* too-small buffer with 2 entries: must fail without writing */ + apdu_len = (unsigned)len_null - 1; + memset(apdu, 0xAA, sizeof(apdu)); + len_encoded = address_list_encode(apdu, apdu_len); + zassert_equal( + len_encoded, BACNET_STATUS_ABORT, + "Two entries: too-small buffer must return BACNET_STATUS_ABORT"); + zassert_equal( + apdu[0], 0xAA, "Two entries: too-small buffer must not be written"); +} + /** * @} */ @@ -274,13 +432,15 @@ void test_main(void) #ifdef BACNET_ADDRESS_CACHE_FILE ztest_test_suite( address_tests, ztest_unit_test(testAddressFile), - ztest_unit_test(testAddress), ztest_unit_test(test_rr_address)); + ztest_unit_test(testAddress), ztest_unit_test(test_rr_address), + ztest_unit_test(test_address_list_encode)); ztest_run_test_suite(address_tests); #else ztest_test_suite( address_tests, ztest_unit_test(testAddress), - ztest_unit_test(test_rr_address)); + ztest_unit_test(test_rr_address), + ztest_unit_test(test_address_list_encode)); ztest_run_test_suite(address_tests); #endif From 3f82ef857fd8000b1c9ab2d0cd2bf7b3013d7fa0 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 14:29:48 -0500 Subject: [PATCH 27/42] fix: EPICs app property list management by adding append function (#1409) --- CHANGELOG.md | 2 ++ SECURITY.md | 75 +++++++++++++++++++++------------------------ apps/epics/main.c | 78 ++++++++++++++++++++++++++++++++++++----------- 3 files changed, 97 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d83eb0bdbe..35fa36075f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The git repositories are hosted at the following sites: ### Security +* Secured apps/epics by preventing a buffer overflow in ProcessRPMData, + resolving destination slot for property values. (#1366) * Secured address_list_encode() function buffer overrun by using existing BACnetAddressBinding encoding function for length check and refactoring. Added unit test for validation.(#1363) diff --git a/SECURITY.md b/SECURITY.md index 8f72693b90..2cf74bf2e8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,83 +27,90 @@ cybersecurity vulnerabilities. Here are the published vulnerability records for v1.5.x: +[CVE-2026-52786](https://www.cve.org/CVERecord?id=CVE-2026-52786) - +apps/epics: malicious ReadPropertyMultiple-ACK with excessive properties causes global-buffer-overflow in ProcessRPMData(). +[GHSA-c4q6-7827-mfg6](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-c4q6-7827-mfg6). +Patched versions: 1.5.1 +Pull Request: [#1366](https://github.com/bacnet-stack/bacnet-stack/pull/1366), +[#1409](https://github.com/bacnet-stack/bacnet-stack/pull/1409). + [CVE-2026-52787](https://www.cve.org/CVERecord?id=CVE-2026-52787) - Global APDU transmit buffer out-of-bounds write in device.address-binding response encoding via address_list_encode() [GHSA-4fgg-fghm-jm43](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4fgg-fghm-jm43). -Patched versions: [1.6.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.6.0). +Patched versions: 1.5.1 Pull Request: [#1363](https://github.com/bacnet-stack/bacnet-stack/pull/1363). [CVE-2026-49990](https://www.cve.org/CVERecord?id=CVE-2026-49990) - AtomicReadFile/AtomicWriteFile stream fileStartPosition validation flaw causes out-of-bounds read/write in RAM file backends. [GHSA-8759-hx7g-94qx](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8759-hx7g-94qx). -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1362](https://github.com/bacnet-stack/bacnet-stack/pull/1362). [CVE-2026-47710](https://www.cve.org/CVERecord?id=CVE-2026-47710) - Loop reference to long Structured View description causes stack-buffer-overflow. [GHSA-2xm5-gjpc-9m6q](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-2xm5-gjpc-9m6q). -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1355](https://github.com/bacnet-stack/bacnet-stack/pull/1355). [CVE-2026-47711](https://www.cve.org/CVERecord?id=CVE-2026-47711) - Stack buffer overflow in Loop internal ReadProperty path via Life_Safety_Zone accepted-modes property. [GHSA-vrpm-9gm2-x552](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-vrpm-9gm2-x552). -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1354](https://github.com/bacnet-stack/bacnet-stack/pull/1354), [#1355](https://github.com/bacnet-stack/bacnet-stack/pull/1355). [CVE-2026-47259](https://www.cve.org/CVERecord?id=CVE-2026-47259) - Stack-based buffer overflow in Notification Class RemoveListElement recipient-list decoding. [GHSA-9w9m-w7w5-rrv3](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-9w9m-w7w5-rrv3). -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1353](https://github.com/bacnet-stack/bacnet-stack/pull/1353). [CVE-2026-47258](https://www.cve.org/CVERecord?id=CVE-2026-47258) - Stack-based buffer overflow in Notification Class AddListElement recipient-list decoding. [GHSA-rjmv-3mcm-r83j](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-rjmv-3mcm-r83j). -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1353](https://github.com/bacnet-stack/bacnet-stack/pull/1353). [CVE-2026-47257](https://www.cve.org/CVERecord?id=CVE-2026-47257) - ReinitializeDevice ENDRESTORE can delete existing objects on an empty restore file and still return success. [GHSA-x6pp-3pf3-f87r](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-x6pp-3pf3-f87r). -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1352](https://github.com/bacnet-stack/bacnet-stack/pull/1352). [CVE-2026-49341](https://www.cve.org/CVERecord?id=CVE-2026-49341) - Uncontrolled recursion in Timer object writeback path leads to remote server stack overflow. [GHSA-7r8r-2rj2-5wvr](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-7r8r-2rj2-5wvr). -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1347](https://github.com/bacnet-stack/bacnet-stack/pull/1347) [CVE-2026-47217](https://www.cve.org/CVERecord?id=CVE-2026-47217) - Channel member self-reference causes uncontrolled recursion and stack overflow in default BACnet/IP server [GHSA-wjw5-q9g6-2764](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-wjw5-q9g6-2764) -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1345](https://github.com/bacnet-stack/bacnet-stack/pull/1345). [CVE-2026-46677](https://www.cve.org/CVERecord?id=CVE-2026-46677) - Client-Side Out-of-Bounds Read in AtomicReadFile-ACK Record-Access Handling via RecordCount / fileData[] Mismatch [GHSA-rv5h-cxwq-q3mh](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-rv5h-cxwq-q3mh) -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1344](https://github.com/bacnet-stack/bacnet-stack/pull/1344). [CVE-2026-46676](https://www.cve.org/CVERecord?id=CVE-2026-46676) - Uninitialized Value Use in AtomicReadFile-ACK Record-Access Encoder Causes Response Corruption and Conditional Information Disclosure [GHSA-2fwp-32cj-g3x4](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-2fwp-32cj-g3x4) -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1344](https://github.com/bacnet-stack/bacnet-stack/pull/1344). [CVE-2026-46674](https://www.cve.org/CVERecord?id=CVE-2026-46674) - Out-of-Bounds Read in AtomicWriteFile Record Decoder via Unbounded returnedRecordCount [GHSA-8384-pwhh-cxjh](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8384-pwhh-cxjh) -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1344](https://github.com/bacnet-stack/bacnet-stack/pull/1344). [CVE-2026-45265](https://www.cve.org/CVERecord?id=CVE-2026-45265) - Atomic-Read-File RecordCount Stack-Based Out-of-Bounds Write [GHSA-v3gx-mwrp-xvh5](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-v3gx-mwrp-xvh5) -Patched versions: [1.5.1](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.1). +Patched versions: 1.5.1 Pull Request: [#1340](https://github.com/bacnet-stack/bacnet-stack/pull/1340). [CVE-2026-40279](https://www.cve.org/CVERecord?id=CVE-2026-40279) - @@ -115,58 +122,44 @@ Pull Request: [#1300](https://github.com/bacnet-stack/bacnet-stack/pull/1300) [CVE-2026-41503](https://www.cve.org/CVERecord?id=CVE-2026-41503) - Out-of-Bounds Read in ReadPropertyMultiple Property Decoder via Deprecated Tag Parser [GHSA-5w2v-mwqj-pr2c](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-5w2v-mwqj-pr2c) -Patched versions: -[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) -Pull Request: -[#1244](https://github.com/bacnet-stack/bacnet-stack/pull/1244) +Patched versions: 1.5.0 +Pull Request: [#1244](https://github.com/bacnet-stack/bacnet-stack/pull/1244) [CVE-2026-41502](https://www.cve.org/CVERecord?id=CVE-2026-41502) - Off-by-One Out-of-Bounds Read in ReadPropertyMultiple Object ID Decoder [GHSA-7545-3fpx-4xw3](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-7545-3fpx-4xw3) -Patched versions: -[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) -Pull Request: -[#1244](https://github.com/bacnet-stack/bacnet-stack/pull/1244) +Patched versions: 1.5.0 +Pull Request: [#1244](https://github.com/bacnet-stack/bacnet-stack/pull/1244) [CVE-2026-41475](https://www.cve.org/CVERecord?id=CVE-2026-41475) - Out-of-Bounds Read in WritePropertyMultiple Decoder via Deprecated Tag Parser [GHSA-cvv4-v3g6-4jmv](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-cvv4-v3g6-4jmv) -Patched versions: -[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) -Pull Request: -[#1244](https://github.com/bacnet-stack/bacnet-stack/pull/1244) +Patched versions: 1.5.0 +Pull Request: [#1244](https://github.com/bacnet-stack/bacnet-stack/pull/1244) [CVE-2026-26264](https://www.cve.org/CVERecord?id=CVE-2026-26264) - WriteProperty decoding length underflow leads to OOB read and crash [GHSA-phjh-v45p-gmjj](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-phjh-v45p-gmjj) -Patched versions: -[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) -Pull Request: -[#1231](https://github.com/bacnet-stack/bacnet-stack/pull/1231) +Patched versions: 1.5.0 +Pull Request: [#1231](https://github.com/bacnet-stack/bacnet-stack/pull/1231) [CVE-2026-21870](https://www.cve.org/CVERecord?id=CVE-2026-21870) - Off-by-one Stack-based Buffer Overflow in tokenizer_string [GHSA-pc83-wp6w-93mx](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-pc83-wp6w-93mx) -Patched versions: -[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) -Pull Request: -[#1196](https://github.com/bacnet-stack/bacnet-stack/pull/1196) +Patched versions: 1.5.0 +Pull Request: [#1196](https://github.com/bacnet-stack/bacnet-stack/pull/1196) [CVE-2026-21878](https://www.cve.org/CVERecord?id=CVE-2026-21878) - Improper Limitation of a Pathname to a Restricted Directory [GHSA-p8rx-c26w-545j](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-p8rx-c26w-545j) -Patched versions: -[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) -Pull Request: -[#1197](https://github.com/bacnet-stack/bacnet-stack/pull/1197) +Patched versions: 1.5.0 +Pull Request: [#1197](https://github.com/bacnet-stack/bacnet-stack/pull/1197) [CVE-2025-66624](https://www.cve.org/CVERecord?id=CVE-2025-66624) - BACnet-stack MS/TP reply matcher OOB read [GHSA-8wgw-5h6x-qgqg](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8wgw-5h6x-qgqg) -Patched versions: -[1.5.0](https://github.com/bacnet-stack/bacnet-stack/tree/bacnet-stack-1.5.0) -Pull Request: -[#1178](https://github.com/bacnet-stack/bacnet-stack/pull/1178) +Patched versions: 1.5.0 +Pull Request: [#1178](https://github.com/bacnet-stack/bacnet-stack/pull/1178) ## Reporting a Vulnerability diff --git a/apps/epics/main.c b/apps/epics/main.c index 1b512f897c..efbd06f9ea 100644 --- a/apps/epics/main.c +++ b/apps/epics/main.c @@ -112,6 +112,24 @@ static uint32_t Property_List_Length = 0; static uint32_t Property_List_Index = 0; static int32_t Property_List[MAX_PROPS + 2]; +static bool property_list_append(int32_t property_id) +{ + uint32_t capacity = + (uint32_t)(sizeof(Property_List) / sizeof(Property_List[0])); + + /* Keep one slot free for the list terminator and cap to MAX_PROPS. */ + if ((Property_List_Index >= (capacity - 1)) || + (Property_List_Index >= MAX_PROPS)) { + return false; + } + + Property_List[Property_List_Index] = property_id; + Property_List_Index++; + Property_List_Length++; + + return true; +} + struct property_value_list_t { int32_t property_id; BACNET_APPLICATION_DATA_VALUE *value; @@ -826,7 +844,7 @@ ProcessRPMData(BACNET_READ_ACCESS_DATA *rpm_data, EPICS_STATES state) * wait and put these object lists at the end */ bool bHasObjectList = false; bool bHasStructuredViewList = false; - int i = 0; + int slot = 0; while (rpm_data) { rpm_property = rpm_data->listOfProperties; @@ -842,10 +860,11 @@ ProcessRPMData(BACNET_READ_ACCESS_DATA *rpm_data, EPICS_STATES state) bHasStructuredViewList = true; break; default: - Property_List[Property_List_Index] = - rpm_property->propertyIdentifier; - Property_List_Index++; - Property_List_Length++; + if (!property_list_append( + (int32_t)rpm_property->propertyIdentifier)) { + /* Ignore excess properties once local list is full. + */ + } break; } /* Free up the value(s) */ @@ -856,10 +875,29 @@ ProcessRPMData(BACNET_READ_ACCESS_DATA *rpm_data, EPICS_STATES state) free(old_value); } } else if (state == GET_HEADING_RESPONSE) { - Property_Value_List[i++].value = rpm_property->value; - /* copy this pointer. - * On error, the pointer will be null - * We won't free these values; they will free at exit */ + /* Resolve destination slot by property identifier to avoid + * overflow from unexpected, duplicate, or excess properties */ + for (slot = 0; Property_Value_List[slot].property_id != -1; + slot++) { + if (Property_Value_List[slot].property_id == + (int32_t)rpm_property->propertyIdentifier) { + break; + } + } + if ((Property_Value_List[slot].property_id != -1) && + (Property_Value_List[slot].value == NULL)) { + /* Store only in the matching, empty slot. + * We won't free these values; they will free at exit */ + Property_Value_List[slot].value = rpm_property->value; + } else { + /* free unknown, duplicate, or excess property values */ + value = rpm_property->value; + while (value) { + old_value = value; + value = value->next; + free(old_value); + } + } } else { fprintf(stdout, " "); Print_Property_Identifier(rpm_property->propertyIdentifier); @@ -886,18 +924,24 @@ ProcessRPMData(BACNET_READ_ACCESS_DATA *rpm_data, EPICS_STATES state) } else if (bSuccess) { /* and GET_LIST_OF_ALL_RESPONSE */ /* Now append the properties we waited on. */ if (bHasStructuredViewList) { - Property_List[Property_List_Index] = PROP_STRUCTURED_OBJECT_LIST; - Property_List_Index++; - Property_List_Length++; + if (!property_list_append(PROP_STRUCTURED_OBJECT_LIST)) { + /* Ignore when local list is full. */ + } } if (bHasObjectList) { - Property_List[Property_List_Index] = PROP_OBJECT_LIST; - Property_List_Index++; - Property_List_Length++; + if (!property_list_append(PROP_OBJECT_LIST)) { + /* Ignore when local list is full. */ + } } /* Now insert the -1 list terminator, but don't count it. */ - Property_List[Property_List_Index] = -1; - assert(Property_List_Length < MAX_PROPS); + if (Property_List_Index < + (uint32_t)(sizeof(Property_List) / sizeof(Property_List[0]))) { + Property_List[Property_List_Index] = -1; + } else { + Property_List + [(sizeof(Property_List) / sizeof(Property_List[0])) - 1] = -1; + } + assert(Property_List_Length <= MAX_PROPS); Property_List_Index = 0; /* Will start at top of the list */ nextState = GET_PROPERTY_REQUEST; } From 810e41338c5596eeb8d6abe667948976b4d4df5d Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 14:34:51 -0500 Subject: [PATCH 28/42] Fix buffer overflows in bsc_node_parse_urls() (#1365) * Fix buffer overflows in bsc_node_parse_urls() Three bugs in the BACnet/SC Address Resolution ACK URL parser: 1. NUL terminator used absolute string position 'i' as array index instead of relative position 'i - start', writing past the 129-byte utf8_urls buffer for URLs starting past position 128. 2. No bounds check on URL count index 'j', allowing writes past the 10-element utf8_urls array when parsing 11+ URLs, corrupting urls_num, fresh_timer, and adjacent structures. 3. Length check compared absolute position 'i' against the max URI size instead of the actual URL length '(i - start)', incorrectly rejecting valid short URLs at high string offsets. Fixes all three by using relative lengths and adding array bounds checks before every write. --- CHANGELOG.md | 4 +++- SECURITY.md | 6 +++++- src/bacnet/datalink/bsc/bsc-node.c | 15 ++++++++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35fa36075f..b1bd27052f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,12 @@ The git repositories are hosted at the following sites: * * -## [1.5.1-rc3] - 2026-05-27 +## [1.5.1-rc4] - 2026-07-02 ### Security +* Secured bsc_node_parse_urls() by fixing buffer overflows by using relative + lengths and adding array bounds checks before every write. (#1365) * Secured apps/epics by preventing a buffer overflow in ProcessRPMData, resolving destination slot for property values. (#1366) * Secured address_list_encode() function buffer overrun by using diff --git a/SECURITY.md b/SECURITY.md index 2cf74bf2e8..1d688dd902 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -25,7 +25,11 @@ or [GHSA](https://github.com/bacnet-stack/bacnet-stack/security/advisories?state and a record is created to identify, define, and catalog publicly disclosed cybersecurity vulnerabilities. -Here are the published vulnerability records for v1.5.x: +[CVE-2026-52788](https://www.cve.org/CVERecord?id=CVE-2026-52788) - +Buffer overflows in bsc_node_parse_urls() (BACnet/SC Address Resolution ACK URL parser). +[GHSA-rf83-3rr5-v4mj](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-rf83-3rr5-v4mj). +Patched versions: 1.5.1 +Pull Request: [#1365](https://github.com/bacnet-stack/bacnet-stack/pull/1365). [CVE-2026-52786](https://www.cve.org/CVERecord?id=CVE-2026-52786) - apps/epics: malicious ReadPropertyMultiple-ACK with excessive properties causes global-buffer-overflow in ProcessRPMData(). diff --git a/src/bacnet/datalink/bsc/bsc-node.c b/src/bacnet/datalink/bsc/bsc-node.c index 35ee74fce5..86d344f084 100644 --- a/src/bacnet/datalink/bsc/bsc-node.c +++ b/src/bacnet/datalink/bsc/bsc-node.c @@ -405,22 +405,27 @@ static void bsc_node_parse_urls( .utf8_websocket_uri_string_len; i++) { if (url[i] == 0x20) { - if (i > BSC_CONF_NODE_MAX_URI_SIZE_IN_ADDRESS_RESOLUTION_ACK || + if ((i - start) > + BSC_CONF_NODE_MAX_URI_SIZE_IN_ADDRESS_RESOLUTION_ACK || (i - start) == 0) { start = i + 1; continue; - } else { + } else if ( + j < BSC_CONF_NODE_MAX_URIS_NUM_IN_ADDRESS_RESOLUTION_ACK) { memcpy(&r->utf8_urls[j][0], &url[start], i - start); - r->utf8_urls[j][i] = 0; + r->utf8_urls[j][i - start] = 0; j++; start = i + 1; + } else { + break; } } } if (i - start > 0 && - i <= BSC_CONF_NODE_MAX_URI_SIZE_IN_ADDRESS_RESOLUTION_ACK) { + (i - start) <= BSC_CONF_NODE_MAX_URI_SIZE_IN_ADDRESS_RESOLUTION_ACK && + j < BSC_CONF_NODE_MAX_URIS_NUM_IN_ADDRESS_RESOLUTION_ACK) { memcpy(&r->utf8_urls[j][0], &url[start], i - start); - r->utf8_urls[j][i] = 0; + r->utf8_urls[j][i - start] = 0; j++; } r->urls_num = j; From 3c133e499a8cbf363aa7cc8282c8c2db4a86c976 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 14:38:55 -0500 Subject: [PATCH 29/42] Fix DoS vulnerability in rpm_decode_object_property for malformed RPM requests (#1374) --- CHANGELOG.md | 2 ++ SECURITY.md | 6 ++++ src/bacnet/rpm.c | 60 ++++++++++++++++--------------- test/bacnet/rpm/src/main.c | 74 +++++++++++++++++++++++++++++++++++++- 4 files changed, 113 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1bd27052f..5f49f82016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The git repositories are hosted at the following sites: ### Security +* Secured rpm_decode_object_property by fixing a DoS vulnerability + for malformed RPM requests. (#1374) * Secured bsc_node_parse_urls() by fixing buffer overflows by using relative lengths and adding array bounds checks before every write. (#1365) * Secured apps/epics by preventing a buffer overflow in ProcessRPMData, diff --git a/SECURITY.md b/SECURITY.md index 1d688dd902..4b096956f1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -25,6 +25,12 @@ or [GHSA](https://github.com/bacnet-stack/bacnet-stack/security/advisories?state and a record is created to identify, define, and catalog publicly disclosed cybersecurity vulnerabilities. +[CVE-2026-52789](https://www.cve.org/CVERecord?id=CVE-2026-52789) - +Denial of Service (Infinite Loop) in handler_read_property_multiple via malformed RPM requests. +[GHSA-4rf9-4vgq-5gcw](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4rf9-4vgq-5gcw). +Patched versions: 1.5.1 +Pull Request: [#1374](https://github.com/bacnet-stack/bacnet-stack/pull/1374). + [CVE-2026-52788](https://www.cve.org/CVERecord?id=CVE-2026-52788) - Buffer overflows in bsc_node_parse_urls() (BACnet/SC Address Resolution ACK URL parser). [GHSA-rf83-3rr5-v4mj](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-rf83-3rr5-v4mj). diff --git a/src/bacnet/rpm.c b/src/bacnet/rpm.c index 47048a32cf..bd1ccdc6a8 100644 --- a/src/bacnet/rpm.c +++ b/src/bacnet/rpm.c @@ -341,39 +341,43 @@ int rpm_decode_object_property( BACNET_UNSIGNED_INTEGER unsigned_value = 0; /* for decoding */ /* check for valid pointer and minimum size */ - if (apdu && apdu_size) { - /* propertyIdentifier [0] BACnetPropertyIdentifier */ - len = bacnet_enumerated_context_decode( - &apdu[apdu_len], apdu_size - apdu_len, 0, &property); - if (len <= 0) { - if (rpmdata) { - rpmdata->error_code = ERROR_CODE_REJECT_INVALID_TAG; - } - return BACNET_STATUS_REJECT; + if (!apdu || !apdu_size) { + if (rpmdata) { + rpmdata->error_code = ERROR_CODE_REJECT_MISSING_REQUIRED_PARAMETER; } + return BACNET_STATUS_REJECT; + } + /* propertyIdentifier [0] BACnetPropertyIdentifier */ + len = bacnet_enumerated_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 0, &property); + if (len <= 0) { if (rpmdata) { - rpmdata->object_property = (BACNET_PROPERTY_ID)property; + rpmdata->error_code = ERROR_CODE_REJECT_INVALID_TAG; } + return BACNET_STATUS_REJECT; + } + if (rpmdata) { + rpmdata->object_property = (BACNET_PROPERTY_ID)property; + } + apdu_len += len; + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 1, &unsigned_value); + if (len > 0) { + /* propertyArrayIndex [1] Unsigned OPTIONAL */ apdu_len += len; - len = bacnet_unsigned_context_decode( - &apdu[apdu_len], apdu_size - apdu_len, 1, &unsigned_value); - if (len > 0) { - /* propertyArrayIndex [1] Unsigned OPTIONAL */ - apdu_len += len; - if (rpmdata) { - rpmdata->array_index = unsigned_value; - } - } else if (len == 0) { - /* optional - assume ALL array elements */ - if (rpmdata) { - rpmdata->array_index = BACNET_ARRAY_ALL; - } - } else { - if (rpmdata) { - rpmdata->error_code = ERROR_CODE_REJECT_INVALID_TAG; - } - return BACNET_STATUS_REJECT; + if (rpmdata) { + rpmdata->array_index = unsigned_value; + } + } else if (len == 0) { + /* optional - assume ALL array elements */ + if (rpmdata) { + rpmdata->array_index = BACNET_ARRAY_ALL; + } + } else { + if (rpmdata) { + rpmdata->error_code = ERROR_CODE_REJECT_INVALID_TAG; } + return BACNET_STATUS_REJECT; } return apdu_len; diff --git a/test/bacnet/rpm/src/main.c b/test/bacnet/rpm/src/main.c index c1706ead96..f7af0e64d1 100644 --- a/test/bacnet/rpm/src/main.c +++ b/test/bacnet/rpm/src/main.c @@ -628,6 +628,77 @@ static void testReadPropertyMultipleAckProcess(void) Read_Property_Ack_Data[4].error_code, ERROR_CODE_INVALID_TAG, NULL); } +/** + * @brief Regression test for DoS vulnerability: handler_read_property_multiple + * infinite loop on malformed RPM request with missing closing tag. + * + * When rpm_decode_object_property is called with zero remaining bytes + * (exhausted buffer) it must return a negative error code so that the + * handler breaks out of its inner for(;;) loop. Before the fix the + * function returned 0, decode_len never advanced, and the server thread + * spun at 100% CPU indefinitely. + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(rpm_tests, testReadPropertyMultipleMissingClosingTag) +#else +static void testReadPropertyMultipleMissingClosingTag(void) +#endif +{ + uint8_t apdu[480] = { 0 }; + int len = 0; + int test_len = 0; + int apdu_len = 0; + uint8_t invoke_id = 12; + uint8_t test_invoke_id = 0; + uint8_t *service_request = NULL; + unsigned service_request_len = 0; + BACNET_RPM_DATA rpmdata; + + /* Build a valid RPM request that intentionally omits the closing tag + (rpm_encode_apdu_object_end is not called). This reproduces the + malformed/truncated packet described in the DoS report. */ + apdu_len = rpm_encode_apdu_init(&apdu[0], invoke_id); + apdu_len += + rpm_encode_apdu_object_begin(&apdu[apdu_len], OBJECT_DEVICE, 123); + apdu_len += rpm_encode_apdu_object_property( + &apdu[apdu_len], PROP_OBJECT_IDENTIFIER, BACNET_ARRAY_ALL); + /* Closing tag deliberately omitted: + apdu_len += rpm_encode_apdu_object_end(&apdu[apdu_len]); */ + + /* Decode the APDU wrapper to obtain the service-request slice */ + test_len = rpm_decode_apdu( + &apdu[0], apdu_len, &test_invoke_id, &service_request, + &service_request_len); + zassert_true(test_len >= 0, NULL); + zassert_equal(test_invoke_id, invoke_id, NULL); + + /* Decode the object identifier */ + test_len = + rpm_decode_object_id(service_request, service_request_len, &rpmdata); + zassert_true(test_len > 0, NULL); + len += test_len; + + /* Decode the one encoded property - this must succeed */ + test_len = rpm_decode_object_property( + &service_request[len], service_request_len - len, &rpmdata); + zassert_true(test_len > 0, NULL); + zassert_equal(rpmdata.object_property, PROP_OBJECT_IDENTIFIER, NULL); + len += test_len; + + /* The buffer is now exhausted: service_request_len - len == 0. + rpm_decode_object_property must return a negative error code here. + Returning 0 would cause handler_read_property_multiple to spin in + an infinite loop (DoS). */ + zassert_equal((unsigned)len, service_request_len, NULL); + test_len = rpm_decode_object_property( + &service_request[len], service_request_len - len, &rpmdata); + zassert_true( + test_len < 0, + "rpm_decode_object_property returned %d on empty buffer; " + "expected negative error (fix DoS infinite-loop)", + test_len); +} + /** * @} */ @@ -641,7 +712,8 @@ void test_main(void) rpm_tests, ztest_unit_test(testReadPropertyMultiple), ztest_unit_test(testReadPropertyMultipleRequest), ztest_unit_test(testReadPropertyMultipleAck), - ztest_unit_test(testReadPropertyMultipleAckProcess)); + ztest_unit_test(testReadPropertyMultipleAckProcess), + ztest_unit_test(testReadPropertyMultipleMissingClosingTag)); ztest_run_test_suite(rpm_tests); } From 8790e4b1bd7726c736213778aebee5ce62dd9da2 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 16:57:07 -0500 Subject: [PATCH 30/42] security: backport #1375 with changelog/security and code patch --- CHANGELOG.md | 7 +++ SECURITY.md | 12 ++++ src/bacnet/basic/server/bacnet_device.c | 76 ++++++++++++++----------- 3 files changed, 63 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f49f82016..b9bf105aab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,13 @@ The git repositories are hosted at the following sites: ### Security +* Secured the basic device object which had a string use after free. + Added character string buffer stndup and same/diff functions. + Changed all the device object character string handling to use + character string buffers, where the API set calls use static memory + and WriteProperty uses dynamic memory. This improves memory usage + and prevents use after free since the character string buffer tracks + allocated vs non-allocated strings. (#1375) * Secured rpm_decode_object_property by fixing a DoS vulnerability for malformed RPM requests. (#1374) * Secured bsc_node_parse_urls() by fixing buffer overflows by using relative diff --git a/SECURITY.md b/SECURITY.md index 4b096956f1..8311baf033 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -25,6 +25,18 @@ or [GHSA](https://github.com/bacnet-stack/bacnet-stack/security/advisories?state and a record is created to identify, define, and catalog publicly disclosed cybersecurity vulnerabilities. +[CVE-2026-52790](https://www.cve.org/CVERecord?id=CVE-2026-52790) - +bacnet_device.c stack-use-after-return in writable Device string properties +[GHSA-jr7p-rm2x-739x](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-jr7p-rm2x-739x). +Patched versions: 1.5.1 +Pull Request: [#1375](https://github.com/bacnet-stack/bacnet-stack/pull/1375). + +[CVE-2026-45341](https://www.cve.org/CVERecord?id=CVE-2026-45341) - +WriteProperty to Structured View subordinate-list causes NULL pointer dereference +[GHSA-fv2r-c2m2-7qhh](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-fv2r-c2m2-7qhh) +Patched versions: 1.5.1 +Pull Request: [#1321](https://github.com/bacnet-stack/bacnet-stack/pull/1321). + [CVE-2026-52789](https://www.cve.org/CVERecord?id=CVE-2026-52789) - Denial of Service (Infinite Loop) in handler_read_property_multiple via malformed RPM requests. [GHSA-4rf9-4vgq-5gcw](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4rf9-4vgq-5gcw). diff --git a/src/bacnet/basic/server/bacnet_device.c b/src/bacnet/basic/server/bacnet_device.c index f496824708..a6f5e7464d 100644 --- a/src/bacnet/basic/server/bacnet_device.c +++ b/src/bacnet/basic/server/bacnet_device.c @@ -1210,6 +1210,7 @@ static const int32_t Device_Properties_Optional[] = { PROP_LOCAL_DATE, PROP_DAYLIGHT_SAVINGS_STATUS, PROP_LOCATION, + PROP_DEVICE_UUID, #if (BACNET_COV_SUBSCRIPTIONS_SIZE > 0) PROP_ACTIVE_COV_SUBSCRIPTIONS, #endif @@ -1350,15 +1351,18 @@ bool Device_Objects_Property_List_Member( /* local data */ static uint32_t Object_Instance_Number = BACNET_MAX_INSTANCE; -static BACNET_CHARACTER_STRING My_Object_Name; +static BACNET_CHARACTER_STRING Object_Name_String; static BACNET_DEVICE_STATUS System_Status = STATUS_OPERATIONAL; static const char *Device_Name_Default = BACNET_DEVICE_OBJECT_NAME; static const char *Device_Vendor_Name_Default = BACNET_VENDOR_NAME; static uint16_t Vendor_Identifier = BACNET_VENDOR_ID; -static const char *Model_Name = BACNET_DEVICE_MODEL_NAME; +static BACNET_CHARACTER_STRING Device_Model_Name_String; +static const char *Device_Model_Name_Default = BACNET_DEVICE_MODEL_NAME; static const char *Application_Software_Version = BACNET_DEVICE_VERSION; static const char *Firmware_Revision = BACNET_VERSION_TEXT; +static BACNET_CHARACTER_STRING Device_Location_String; static const char *Device_Location_Default = BACNET_DEVICE_LOCATION_NAME; +static BACNET_CHARACTER_STRING Device_Description_String; static const char *Device_Description_Default = BACNET_DEVICE_DESCRIPTION; static uint32_t Database_Revision; static BACNET_REINITIALIZED_STATE Reinitialize_State = BACNET_REINIT_IDLE; @@ -1654,7 +1658,7 @@ bool Device_Object_Name( bool status = false; if (object_instance == Object_Instance_Number) { - status = characterstring_copy(object_name, &My_Object_Name); + status = characterstring_copy(object_name, &Object_Name_String); } return status; @@ -1664,9 +1668,9 @@ bool Device_Set_Object_Name(const BACNET_CHARACTER_STRING *object_name) { bool status = false; /*return value */ - if (!characterstring_same(&My_Object_Name, object_name)) { + if (!characterstring_same(&Object_Name_String, object_name)) { /* Make the change and update the database revision */ - status = characterstring_copy(&My_Object_Name, object_name); + status = characterstring_copy(&Object_Name_String, object_name); Device_Inc_Database_Revision(); } @@ -1680,7 +1684,7 @@ bool Device_Set_Object_Name(const BACNET_CHARACTER_STRING *object_name) */ bool Device_Object_Name_ANSI_Init(const char *value) { - return characterstring_init_ansi(&My_Object_Name, value); + return characterstring_init_ansi(&Object_Name_String, value); } /** @@ -1689,7 +1693,7 @@ bool Device_Object_Name_ANSI_Init(const char *value) */ char *Device_Object_Name_ANSI(void) { - return (char *)characterstring_value(&My_Object_Name); + return (char *)characterstring_value(&Object_Name_String); } /** @@ -1848,13 +1852,14 @@ void Device_Set_Vendor_Identifier(uint16_t vendor_id) const char *Device_Model_Name(void) { - return Model_Name; + return (char *)characterstring_value(&Device_Model_Name_String); } bool Device_Set_Model_Name(const char *name, size_t length) { (void)length; - Model_Name = name ? name : BACNET_DEVICE_MODEL_NAME; + characterstring_init_ansi( + &Device_Model_Name_String, name ? name : Device_Model_Name_Default); return true; } @@ -1887,26 +1892,28 @@ bool Device_Set_Application_Software_Version(const char *name, size_t length) const char *Device_Description(void) { - return Device_Description_Default; + return (char *)characterstring_value(&Device_Description_String); } bool Device_Set_Description(const char *name, size_t length) { (void)length; - Device_Description_Default = name ? name : BACNET_DEVICE_DESCRIPTION; + characterstring_init_ansi( + &Device_Description_String, name ? name : Device_Description_Default); return true; } const char *Device_Location(void) { - return Device_Location_Default; + return (char *)characterstring_value(&Device_Location_String); } bool Device_Set_Location(const char *name, size_t length) { (void)length; - Device_Location_Default = name ? name : BACNET_DEVICE_LOCATION_NAME; + characterstring_init_ansi( + &Device_Location_String, name ? name : Device_Location_Default); return true; } @@ -2598,6 +2605,7 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) int apdu_len = 0; /* return value */ BACNET_BIT_STRING bit_string = { 0 }; BACNET_CHARACTER_STRING char_string = { 0 }; + BACNET_OCTET_STRING octet_string = { 0 }; uint32_t i = 0; uint32_t count = 0; uint8_t *apdu = NULL; @@ -2616,8 +2624,8 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) &apdu[0], OBJECT_DEVICE, Object_Instance_Number); break; case PROP_OBJECT_NAME: - apdu_len = - encode_application_character_string(&apdu[0], &My_Object_Name); + apdu_len = encode_application_character_string( + &apdu[0], &Object_Name_String); break; case PROP_OBJECT_TYPE: apdu_len = encode_application_enumerated(&apdu[0], OBJECT_DEVICE); @@ -2640,9 +2648,8 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) apdu_len = encode_application_unsigned(&apdu[0], Vendor_Identifier); break; case PROP_MODEL_NAME: - characterstring_init_ansi(&char_string, Model_Name); - apdu_len = - encode_application_character_string(&apdu[0], &char_string); + apdu_len = encode_application_character_string( + &apdu[0], &Device_Model_Name_String); break; case PROP_FIRMWARE_REVISION: characterstring_init_ansi(&char_string, Firmware_Revision); @@ -2656,9 +2663,8 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) encode_application_character_string(&apdu[0], &char_string); break; case PROP_LOCATION: - characterstring_init_ansi(&char_string, Device_Location_Default); - apdu_len = - encode_application_character_string(&apdu[0], &char_string); + apdu_len = encode_application_character_string( + &apdu[0], &Device_Location_String); break; case PROP_LOCAL_TIME: Update_Current_Time(); @@ -2844,6 +2850,10 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) apdu_len = encode_application_character_string(&apdu[0], &char_string); break; + case PROP_DEVICE_UUID: + octetstring_init(&octet_string, Device_UUID, sizeof(Device_UUID)); + apdu_len = encode_application_octet_string(&apdu[0], &octet_string); + break; case PROP_TIME_OF_DEVICE_RESTART: apdu_len = bacapp_encode_timestamp(&apdu[0], &Time_Of_Device_Restart); @@ -3036,7 +3046,7 @@ bool Device_Write_Property_Local(BACNET_WRITE_PROPERTY_DATA *wp_data) break; case PROP_OBJECT_NAME: status = write_property_string_valid( - wp_data, &value, characterstring_capacity(&My_Object_Name)); + wp_data, &value, characterstring_capacity(&Object_Name_String)); if (status) { /* All the object names in a device must be unique */ if (Device_Valid_Object_Name( @@ -3060,9 +3070,8 @@ bool Device_Write_Property_Local(BACNET_WRITE_PROPERTY_DATA *wp_data) status = write_property_empty_string_valid( wp_data, &value, MAX_DEV_LOC_LEN); if (status) { - Device_Set_Location( - characterstring_value(&value.type.Character_String), - characterstring_length(&value.type.Character_String)); + characterstring_copy( + &Device_Location_String, &value.type.Character_String); } break; @@ -3070,18 +3079,16 @@ bool Device_Write_Property_Local(BACNET_WRITE_PROPERTY_DATA *wp_data) status = write_property_empty_string_valid( wp_data, &value, MAX_DEV_DESC_LEN); if (status) { - Device_Set_Description( - characterstring_value(&value.type.Character_String), - characterstring_length(&value.type.Character_String)); + characterstring_copy( + &Device_Description_String, &value.type.Character_String); } break; case PROP_MODEL_NAME: status = write_property_empty_string_valid( wp_data, &value, MAX_DEV_MOD_LEN); if (status) { - Device_Set_Model_Name( - characterstring_value(&value.type.Character_String), - characterstring_length(&value.type.Character_String)); + characterstring_copy( + &Device_Model_Name_String, &value.type.Character_String); } break; #if defined(BACNET_TIME_MASTER) @@ -3904,7 +3911,12 @@ void Device_Init(object_functions_t *object_table) if (Object_Instance_Number > BACNET_MAX_INSTANCE) { Object_Instance_Number = BACNET_MAX_INSTANCE; } - characterstring_init_ansi(&My_Object_Name, Device_Name_Default); + characterstring_init_ansi(&Object_Name_String, Device_Name_Default); + characterstring_init_ansi( + &Device_Model_Name_String, Device_Model_Name_Default); + characterstring_init_ansi( + &Device_Description_String, Device_Description_Default); + characterstring_init_ansi(&Device_Location_String, Device_Location_Default); #if (BACNET_PROTOCOL_REVISION >= 14) #ifdef CONFIG_BACNET_BASIC_OBJECT_CHANNEL /* link WriteProperty to Channel object for references */ From 80a5e5ac7d5129c63e703b57e9e3751e8f8fc758 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 16:59:03 -0500 Subject: [PATCH 31/42] security: backport #1386/#1387 with changelog/security and code patch --- CHANGELOG.md | 3 + SECURITY.md | 6 ++ apps/router-ipv6/main.c | 47 ++--------- apps/router-mstp/main.c | 44 ++--------- ports/linux/dlmstp.c | 2 +- src/bacnet/npdu.c | 105 +++++++++++++++++++++++++ src/bacnet/npdu.h | 18 +++++ test/bacnet/npdu/src/main.c | 151 +++++++++++++++++++++++++++++++++++- 8 files changed, 294 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9bf105aab..7f1242ecd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ The git repositories are hosted at the following sites: and WriteProperty uses dynamic memory. This improves memory usage and prevents use after free since the character string buffer tracks allocated vs non-allocated strings. (#1375) +* Secured xy_color_decode() by adjusting apdu_size calculation to prevent + out-of-bounds read, and secured network control handler offset calculation + in router applications to prevent buffer overrun. (#1386, #1387) * Secured rpm_decode_object_property by fixing a DoS vulnerability for malformed RPM requests. (#1374) * Secured bsc_node_parse_urls() by fixing buffer overflows by using relative diff --git a/SECURITY.md b/SECURITY.md index 8311baf033..47a2bdec47 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -37,6 +37,12 @@ WriteProperty to Structured View subordinate-list causes NULL pointer dereferenc Patched versions: 1.5.1 Pull Request: [#1321](https://github.com/bacnet-stack/bacnet-stack/pull/1321). +Pre-auth OOB read in xy_color_decode (BACnetXYColor) via WriteGroup/WriteProperty +[GHSA-mmg6-p4pr-cj6h](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-mmg6-p4pr-cj6h). +Patched versions: 1.5.1 +Pull Request: [#1386](https://github.com/bacnet-stack/bacnet-stack/pull/1386), +[#1387](https://github.com/bacnet-stack/bacnet-stack/pull/1387). + [CVE-2026-52789](https://www.cve.org/CVERecord?id=CVE-2026-52789) - Denial of Service (Infinite Loop) in handler_read_property_multiple via malformed RPM requests. [GHSA-4rf9-4vgq-5gcw](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4rf9-4vgq-5gcw). diff --git a/apps/router-ipv6/main.c b/apps/router-ipv6/main.c index 6d764951dc..1487d69e32 100644 --- a/apps/router-ipv6/main.c +++ b/apps/router-ipv6/main.c @@ -638,9 +638,7 @@ static void network_control_handler( uint8_t *npdu, uint16_t npdu_len) { - uint16_t npdu_offset = 0; uint16_t dnet = 0; - uint16_t len = 0; const char *msg_name = NULL; msg_name = bactext_network_layer_msg_name(npdu_data->network_message_type); @@ -652,19 +650,8 @@ static void network_control_handler( break; case NETWORK_MESSAGE_I_AM_ROUTER_TO_NETWORK: /* add its DNETs to our routing table */ - fprintf(stderr, "for Networks: "); - len = 2; - while (npdu_len >= len) { - len = decode_unsigned16(&npdu[npdu_offset], &dnet); - fprintf(stderr, "%hu", dnet); - dnet_add(snet, dnet, src); - npdu_len -= len; - npdu_offset += len; - if (npdu_len) { - fprintf(stderr, ", "); - } - } - fprintf(stderr, ".\n"); + npdu_i_am_router_to_network_process( + snet, src, npdu, npdu_len, dnet_add); break; case NETWORK_MESSAGE_I_COULD_BE_ROUTER_TO_NETWORK: /* Do nothing, same as previous case. */ @@ -709,32 +696,10 @@ static void network_control_handler( case NETWORK_MESSAGE_INIT_RT_TABLE: /* If sent with Number of Ports == 0, we respond with * NETWORK_MESSAGE_INIT_RT_TABLE_ACK and a list of all our - * reachable networks. - */ - if (npdu_len > 0) { - /* If Number of Ports is 0, broadcast our "full" table */ - if (npdu[0] == 0) { - send_initialize_routing_table_ack(snet, NULL); - } else { - /* they sent us a list */ - int net_count = npdu[0]; - while (net_count--) { - int i = 1; - /* DNET */ - decode_unsigned16(&npdu[i], &dnet); - /* update routing table */ - dnet_add(snet, dnet, src); - if (npdu[i + 3] > 0) { - /* find next NET value */ - i = npdu[i + 3] + 4; - } else { - i += 4; - } - } - send_initialize_routing_table_ack(snet, NULL); - } - break; - } + * reachable networks. */ + npdu_init_routing_table_process( + snet, src, npdu, npdu_len, dnet_add); + send_initialize_routing_table_ack(snet, NULL); break; case NETWORK_MESSAGE_INIT_RT_TABLE_ACK: /* Do nothing with the routing table info, since don't support diff --git a/apps/router-mstp/main.c b/apps/router-mstp/main.c index f8d49c9a2f..ca6f7f048e 100644 --- a/apps/router-mstp/main.c +++ b/apps/router-mstp/main.c @@ -659,9 +659,7 @@ static void network_control_handler( uint8_t *npdu, uint16_t npdu_len) { - uint16_t npdu_offset = 0; uint16_t dnet = 0; - uint16_t len = 0; const char *msg_name = NULL; (void)src; @@ -675,19 +673,8 @@ static void network_control_handler( break; case NETWORK_MESSAGE_I_AM_ROUTER_TO_NETWORK: /* add its DNETs to our routing table */ - fprintf(stderr, "for Networks: "); - len = 2; - while (npdu_len >= len) { - len = decode_unsigned16(&npdu[npdu_offset], &dnet); - fprintf(stderr, "%hu", dnet); - dnet_add(snet, dnet, src); - npdu_len -= len; - npdu_offset += len; - if (npdu_len) { - fprintf(stderr, ", "); - } - } - fprintf(stderr, ".\n"); + npdu_i_am_router_to_network_process( + snet, src, npdu, npdu_len, dnet_add); break; case NETWORK_MESSAGE_I_COULD_BE_ROUTER_TO_NETWORK: /* Do nothing, same as previous case. */ @@ -734,30 +721,9 @@ static void network_control_handler( * NETWORK_MESSAGE_INIT_RT_TABLE_ACK and a list of all our * reachable networks. */ - if (npdu_len > 0) { - /* If Number of Ports is 0, broadcast our "full" table */ - if (npdu[0] == 0) { - send_initialize_routing_table_ack(snet, NULL); - } else { - /* they sent us a list */ - int net_count = npdu[0]; - while (net_count--) { - int i = 1; - /* DNET */ - decode_unsigned16(&npdu[i], &dnet); - /* update routing table */ - dnet_add(snet, dnet, src); - if (npdu[i + 3] > 0) { - /* find next NET value */ - i = npdu[i + 3] + 4; - } else { - i += 4; - } - } - send_initialize_routing_table_ack(snet, NULL); - } - break; - } + npdu_init_routing_table_process( + snet, src, npdu, npdu_len, dnet_add); + send_initialize_routing_table_ack(snet, NULL); break; case NETWORK_MESSAGE_INIT_RT_TABLE_ACK: /* Do nothing with the routing table info, since don't support diff --git a/ports/linux/dlmstp.c b/ports/linux/dlmstp.c index 1cf85ad92d..9fe0d2fb92 100644 --- a/ports/linux/dlmstp.c +++ b/ports/linux/dlmstp.c @@ -926,7 +926,7 @@ bool dlmstp_init(char *ifname) RS485_Set_Interface(ifname); debug_fprintf(stderr, "MS/TP Interface: %s\n", ifname); } else { - ifname = (char *)RS485_Interface(); + ifname = RS485_Interface(); } pthread_condattr_init(&attr); if ((rv = pthread_condattr_setclock(&attr, CLOCK_MONOTONIC)) != 0) { diff --git a/src/bacnet/npdu.c b/src/bacnet/npdu.c index 45fc2e42e2..af27bc6af6 100644 --- a/src/bacnet/npdu.c +++ b/src/bacnet/npdu.c @@ -891,3 +891,108 @@ bool npdu_is_data_expecting_reply( request_pdu, request_pdu_len, &request_address, reply_pdu, reply_pdu_len, &reply_address); } + +/** + * @brief Process the NPDU portion of an I-Am-Router-To-Network message, which + * contains a list of BACnet network numbers that the router is connected to. + * @param snet [in] The source network number of the I-Am-Router-To + * Network message, which is the network number of the router sending the + * message. + * @param src [in] The source address of the I-Am-Router-To-Network message, + * which is the address of the router sending the message. + * @param npdu [in] The buffer containing the NPDU portion of the + * I-Am-Router-To-Network message, which contains the list of BACnet network + * numbers that the router is connected to. + * @param npdu_size [in] The size of the npdu buffer in bytes. + * @param dnet_add [in] A callback function that will be called for each BACnet + * network number (DNET) + */ +void npdu_i_am_router_to_network_process( + uint16_t snet, + const BACNET_ADDRESS *src, + const uint8_t *npdu, + uint16_t npdu_size, + npdu_dnet_add_callback_t dnet_add) +{ + int len = 2; + uint16_t dnet = 0; + uint16_t npdu_offset = 0; + uint16_t npdu_len = npdu_size; + + while (npdu_len >= len) { + len = decode_unsigned16(&npdu[npdu_offset], &dnet); + if (dnet_add) { + dnet_add(snet, dnet, src); + } + npdu_len -= len; + npdu_offset += len; + } +} + +/** + * @brief Process the NPDU portion of an Initialize-Routing-Table message, which + * contains a list of BACnet network numbers (DNETs) and per-port information. + * @param snet [in] The source network number of the Initialize-Routing-Table + * message, which is the network number of the router sending the message. + * @param src [in] The source address of the I-Have-Router-To-Network message, + * which is the address of the router sending the message. + * @param npdu [in] The buffer containing the NPDU portion of the + * I-Have-Router-To-Network message, which contains the list of BACnet network + * numbers that the router is connected to, along with port information. + * @param npdu_size [in] The size of the npdu buffer in bytes. + * @param dnet_add [in] Optional callback invoked for each decoded DNET value. + * The callback receives the source network, decoded DNET, and source address. + */ +void npdu_init_routing_table_process( + uint16_t snet, + const BACNET_ADDRESS *src, + const uint8_t *npdu, + uint16_t npdu_size, + npdu_dnet_add_callback_t dnet_add) +{ + int len = 2; + uint16_t dnet = 0; + uint16_t npdu_offset = 0; + uint8_t port_id = 0; + uint8_t port_info_len = 0; + uint8_t net_count; + uint16_t npdu_len = npdu_size; + + if (npdu_len <= 1) { + /* malformed message */ + return; + } + net_count = npdu[npdu_offset]; + npdu_offset += 1; + npdu_len -= 1; + if (net_count == 0) { + /* no networks, nothing to do */ + return; + } + /* DNET(2) + PortID(1) + PortInfoLen(1) = 4 bytes */ + while ((npdu_len >= 4) && (net_count--)) { + /* DNET */ + len = decode_unsigned16(&npdu[npdu_offset], &dnet); + npdu_offset += len; + npdu_len -= len; + /* update routing table */ + if (dnet_add) { + dnet_add(snet, dnet, src); + } + /* skip port_id & port_info */ + port_id = npdu[npdu_offset]; + npdu_offset += 1; + npdu_len -= 1; + port_info_len = npdu[npdu_offset]; + npdu_offset += 1; + npdu_len -= 1; + if (npdu_len >= port_info_len) { + npdu_offset += port_info_len; + npdu_len -= port_info_len; + } else { + /* malformed message */ + break; + } + (void)port_id; + } +} diff --git a/src/bacnet/npdu.h b/src/bacnet/npdu.h index e8d2327a4b..fe63a316e6 100644 --- a/src/bacnet/npdu.h +++ b/src/bacnet/npdu.h @@ -54,6 +54,9 @@ typedef struct router_port_t { struct router_port_t *next; /**< Point to next in linked list */ } BACNET_ROUTER_PORT; +typedef void (*npdu_dnet_add_callback_t)( + uint16_t snet, uint16_t net, const BACNET_ADDRESS *addr); + #define NETWORK_NUMBER_LEARNED 0 #define NETWORK_NUMBER_CONFIGURED 1 @@ -123,6 +126,7 @@ bool npdu_is_expected_reply( const uint8_t *reply_pdu, uint16_t reply_pdu_len, BACNET_ADDRESS *reply_address); +BACNET_STACK_EXPORT bool npdu_is_data_expecting_reply( const uint8_t *request_pdu, uint16_t request_pdu_len, @@ -130,6 +134,20 @@ bool npdu_is_data_expecting_reply( const uint8_t *reply_pdu, uint16_t reply_pdu_len, uint8_t reply_mac); +BACNET_STACK_EXPORT +void npdu_i_am_router_to_network_process( + uint16_t snet, + const BACNET_ADDRESS *src, + const uint8_t *npdu, + uint16_t npdu_size, + npdu_dnet_add_callback_t dnet_add); +BACNET_STACK_EXPORT +void npdu_init_routing_table_process( + uint16_t snet, + const BACNET_ADDRESS *src, + const uint8_t *npdu, + uint16_t npdu_size, + npdu_dnet_add_callback_t dnet_add); #ifdef __cplusplus } diff --git a/test/bacnet/npdu/src/main.c b/test/bacnet/npdu/src/main.c index 43cc4a57d8..f53bead348 100644 --- a/test/bacnet/npdu/src/main.c +++ b/test/bacnet/npdu/src/main.c @@ -5,6 +5,7 @@ * @date 2012 * @copyright SPDX-License-Identifier: MIT */ +#include #include #include #include @@ -538,6 +539,152 @@ static void test_NPDU_Data_Expecting_Reply(void) reply_pdu, reply_pdu_len, &test_address, npdu_len + 3); } +/** + * @brief Test npdu_i_am_router_to_network_process + */ +struct test_dnet_add_data { + uint16_t snet; + uint16_t net[16]; + unsigned count; +}; + +static void +test_dnet_add_callback(uint16_t snet, uint16_t net, const BACNET_ADDRESS *addr) +{ + struct test_dnet_add_data *data = + (struct test_dnet_add_data *)(uintptr_t)(const void *)addr; + data->snet = snet; + if (data->count < 16) { + data->net[data->count++] = net; + } +} + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(npdu_tests, test_NPDU_I_Am_Router_To_Network_Process) +#else +static void test_NPDU_I_Am_Router_To_Network_Process(void) +#endif +{ + struct test_dnet_add_data cb_data = { 0 }; + uint8_t npdu[16] = { 0 }; + uint16_t npdu_size = 0; + uint16_t snet = 42; + + /* encode two network numbers: 100 and 200 */ + npdu[0] = 0x00; + npdu[1] = 100; + npdu[2] = 0x00; + npdu[3] = 200; + npdu_size = 4; + + npdu_i_am_router_to_network_process( + snet, (BACNET_ADDRESS *)&cb_data, npdu, npdu_size, + test_dnet_add_callback); + + zassert_equal(cb_data.count, 2, NULL); + zassert_equal(cb_data.snet, snet, NULL); + zassert_equal(cb_data.net[0], 100, NULL); + zassert_equal(cb_data.net[1], 200, NULL); + + /* empty buffer - nothing should be added */ + memset(&cb_data, 0, sizeof(cb_data)); + npdu_i_am_router_to_network_process( + snet, (BACNET_ADDRESS *)&cb_data, npdu, 1, test_dnet_add_callback); + zassert_equal(cb_data.count, 0, NULL); + + /* NULL callback - no crash */ + npdu_i_am_router_to_network_process( + snet, (BACNET_ADDRESS *)&cb_data, npdu, npdu_size, NULL); + + /* single network number */ + memset(&cb_data, 0, sizeof(cb_data)); + npdu[0] = 0x00; + npdu[1] = 0xFF; + npdu_size = 2; + npdu_i_am_router_to_network_process( + snet, (BACNET_ADDRESS *)&cb_data, npdu, npdu_size, + test_dnet_add_callback); + zassert_equal(cb_data.count, 1, NULL); + zassert_equal(cb_data.net[0], 255, NULL); +} + +/** + * @brief Test npdu_init_routing_table_process + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(npdu_tests, test_NPDU_Init_Routing_Table_Process) +#else +static void test_NPDU_Init_Routing_Table_Process(void) +#endif +{ + struct test_dnet_add_data cb_data = { 0 }; + uint8_t npdu[64] = { 0 }; + uint16_t npdu_size = 0; + uint16_t snet = 10; + uint8_t *p = npdu; + + /* encode 2 entries: + * net_count = 2 + * Entry 1: DNET=300, PortID=1, PortInfoLen=0 + * Entry 2: DNET=400, PortID=2, PortInfoLen=3, PortInfo={0xAA,0xBB,0xCC} */ + *p++ = 2; /* net_count */ + /* entry 1 */ + *p++ = 0x01; /* DNET high */ + *p++ = 0x2C; /* DNET low: 300 */ + *p++ = 1; /* PortID */ + *p++ = 0; /* PortInfoLen */ + /* entry 2 */ + *p++ = 0x01; /* DNET high */ + *p++ = 0x90; /* DNET low: 400 */ + *p++ = 2; /* PortID */ + *p++ = 3; /* PortInfoLen */ + *p++ = 0xAA; + *p++ = 0xBB; + *p++ = 0xCC; + npdu_size = (uint16_t)(p - npdu); + + npdu_init_routing_table_process( + snet, (BACNET_ADDRESS *)&cb_data, npdu, npdu_size, + test_dnet_add_callback); + + zassert_equal(cb_data.count, 2, NULL); + zassert_equal(cb_data.snet, snet, NULL); + zassert_equal(cb_data.net[0], 300, NULL); + zassert_equal(cb_data.net[1], 400, NULL); + + /* malformed: too short (<=1 byte) */ + memset(&cb_data, 0, sizeof(cb_data)); + npdu_init_routing_table_process( + snet, (BACNET_ADDRESS *)&cb_data, npdu, 1, test_dnet_add_callback); + zassert_equal(cb_data.count, 0, NULL); + + /* net_count == 0 */ + memset(&cb_data, 0, sizeof(cb_data)); + npdu[0] = 0; + npdu_init_routing_table_process( + snet, (BACNET_ADDRESS *)&cb_data, npdu, npdu_size, + test_dnet_add_callback); + zassert_equal(cb_data.count, 0, NULL); + + /* NULL callback - no crash */ + npdu[0] = 2; + npdu_init_routing_table_process( + snet, (BACNET_ADDRESS *)&cb_data, npdu, npdu_size, NULL); + + /* truncated buffer - only enough bytes for first entry's + * DNET+PortID+PortInfoLen but not enough for second entry's 4-byte minimum; + * first entry should be added */ + memset(&cb_data, 0, sizeof(cb_data)); + npdu[0] = 2; + /* npdu_size=7: net_count(1)+DNET(2)+PortID(1)+PortInfoLen(1)+2bytes of + * entry2 After processing entry 1: npdu_len=3, which is < 4 so loop exits + */ + npdu_init_routing_table_process( + snet, (BACNET_ADDRESS *)&cb_data, npdu, 7, test_dnet_add_callback); + zassert_equal(cb_data.count, 1, NULL); + zassert_equal(cb_data.net[0], 300, NULL); +} + #if defined(CONFIG_ZTEST_NEW_API) ZTEST_SUITE(npdu_tests, NULL, NULL, NULL, NULL, NULL); #else @@ -548,7 +695,9 @@ void test_main(void) ztest_unit_test(test_NPDU_Network), ztest_unit_test(test_NPDU_Copy), ztest_unit_test(test_NPDU_Confirmed_Service), ztest_unit_test(test_NPDU_Segmented_Complex_Ack_Reply), - ztest_unit_test(test_NPDU_Data_Expecting_Reply)); + ztest_unit_test(test_NPDU_Data_Expecting_Reply), + ztest_unit_test(test_NPDU_I_Am_Router_To_Network_Process), + ztest_unit_test(test_NPDU_Init_Routing_Table_Process)); ztest_run_test_suite(npdu_tests); } From d12731f302a9adad09444a433f5d86c8f847e5b0 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 17:01:00 -0500 Subject: [PATCH 32/42] security: backport #1392 with changelog/security and code patch --- CHANGELOG.md | 2 + SECURITY.md | 5 + apps/router-ipv6/main.c | 240 +++++++++++++++++++++++---------- apps/router-mstp/main.c | 289 +++++++++++++++++++++++++++------------- 4 files changed, 373 insertions(+), 163 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f1242ecd4..c2e328001c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ The git repositories are hosted at the following sites: * Secured xy_color_decode() by adjusting apdu_size calculation to prevent out-of-bounds read, and secured network control handler offset calculation in router applications to prevent buffer overrun. (#1386, #1387) +* Secured apps/router-mstp and apps/router-ipv6 routing by introducing + routed_npdu_apdu_encode() with explicit oversized-PDU drop checks. (#1392) * Secured rpm_decode_object_property by fixing a DoS vulnerability for malformed RPM requests. (#1374) * Secured bsc_node_parse_urls() by fixing buffer overflows by using relative diff --git a/SECURITY.md b/SECURITY.md index 47a2bdec47..5f3b28768d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -43,6 +43,11 @@ Patched versions: 1.5.1 Pull Request: [#1386](https://github.com/bacnet-stack/bacnet-stack/pull/1386), [#1387](https://github.com/bacnet-stack/bacnet-stack/pull/1387). +Remote global-buffer-overflow in apps/router-ipv6/main.c routed APDU forwarding path +[GHSA-4p4w-m434-jrhj](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4p4w-m434-jrhj). +Patched versions: 1.5.1 +Pull Request: [#1392](https://github.com/bacnet-stack/bacnet-stack/pull/1392). + [CVE-2026-52789](https://www.cve.org/CVERecord?id=CVE-2026-52789) - Denial of Service (Infinite Loop) in handler_read_property_multiple via malformed RPM requests. [GHSA-4rf9-4vgq-5gcw](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4rf9-4vgq-5gcw). diff --git a/apps/router-ipv6/main.c b/apps/router-ipv6/main.c index 1487d69e32..05c35575e5 100644 --- a/apps/router-ipv6/main.c +++ b/apps/router-ipv6/main.c @@ -38,6 +38,22 @@ #include "bacnet/datalink/bvlc.h" #include "bacnet/basic/bbmd/h_bbmd.h" +#ifndef DEBUG_LOG_DEBUG +#define DEBUG_LOG_DISABLED 0 +#define DEBUG_LOG_ERROR 1 +#define DEBUG_LOG_INFO 2 +#define DEBUG_LOG_DEBUG 3 +#endif + +#ifndef debug_log_fprintf +#define debug_log_fprintf(level, stream, ...) \ + debug_fprintf((stream), __VA_ARGS__) +#endif + +#ifndef debug_log_severity_set +#define debug_log_severity_set(level) ((void)(level)) +#endif + /* current version of the BACnet stack */ static const char *BACnet_Version = BACNET_VERSION_TEXT; @@ -269,7 +285,8 @@ static void dnet_cleanup(DNET *dnets) { DNET *dnet = dnets; while (dnet != NULL) { - debug_printf("DNET %u removed\n", (unsigned)dnet->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "DNET %u removed\n", (unsigned)dnet->net); dnet = dnet->next; free(dnets); dnets = dnet; @@ -313,14 +330,20 @@ static int datalink_send_pdu( int bytes_sent = 0; if (snet == 0) { - debug_printf("BVLC/BVLC6 Send to DNET %u\n", (unsigned)dest->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "BVLC/BVLC6 Send to DNET %u\n", + (unsigned)dest->net); bytes_sent = bip_send_pdu(dest, npdu_data, pdu, pdu_len); bytes_sent = bip6_send_pdu(dest, npdu_data, pdu, pdu_len); } else if (snet == BIP_Net) { - debug_printf("BVLC Send to DNET %u\n", (unsigned)dest->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "BVLC Send to DNET %u\n", + (unsigned)dest->net); bytes_sent = bip_send_pdu(dest, npdu_data, pdu, pdu_len); } else if (snet == BIP6_Net) { - debug_printf("BVLC6 Send to DNET %u\n", (unsigned)dest->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "BVLC6 Send to DNET %u\n", + (unsigned)dest->net); bytes_sent = bip6_send_pdu(dest, npdu_data, pdu, pdu_len); } @@ -358,7 +381,7 @@ static void send_i_am_router_to_network(uint16_t snet, uint16_t net) len = encode_unsigned16(&Tx_Buffer[pdu_len], net); pdu_len += len; } else { - debug_printf("I-Am-Router-To-Network "); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "I-Am-Router-To-Network "); /* Each router shall broadcast out each port an I-Am-Router-To-Network message containing the network numbers of each accessible network except the networks @@ -369,12 +392,12 @@ static void send_i_am_router_to_network(uint16_t snet, uint16_t net) port = Router_Table_Head; while (port != NULL) { if (port->net != snet) { - debug_printf("%u,", port->net); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "%u,", port->net); len = encode_unsigned16(&Tx_Buffer[pdu_len], port->net); pdu_len += len; dnet = port->dnets; while (dnet != NULL) { - debug_printf("%u,", dnet->net); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "%u,", dnet->net); len = encode_unsigned16(&Tx_Buffer[pdu_len], dnet->net); pdu_len += len; dnet = dnet->next; @@ -382,7 +405,7 @@ static void send_i_am_router_to_network(uint16_t snet, uint16_t net) } port = port->next; } - debug_printf("from %u\n", snet); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "from %u\n", snet); } datalink_send_pdu(snet, &dest, &npdu_data, &Tx_Buffer[0], pdu_len); } @@ -642,7 +665,7 @@ static void network_control_handler( const char *msg_name = NULL; msg_name = bactext_network_layer_msg_name(npdu_data->network_message_type); - fprintf(stderr, "Received %s\n", msg_name); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "Received %s\n", msg_name); switch (npdu_data->network_message_type) { case NETWORK_MESSAGE_WHO_IS_ROUTER_TO_NETWORK: who_is_router_to_network_handler( @@ -659,32 +682,47 @@ static void network_control_handler( case NETWORK_MESSAGE_REJECT_MESSAGE_TO_NETWORK: if (npdu_len >= 3) { decode_unsigned16(&npdu[1], &dnet); - fprintf(stderr, "for Network:%hu\n", dnet); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, "for Network:%hu\n", dnet); switch (npdu[0]) { case 0: - fprintf(stderr, "Reason: Other Error.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, "Reason: Other Error.\n"); break; case 1: - fprintf(stderr, "Reason: Network unreachable.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Network unreachable.\n"); break; case 2: - fprintf(stderr, "Reason: Network is busy.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Network is busy.\n"); break; case 3: - fprintf( - stderr, "Reason: Unknown network message type.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Unknown network message type.\n"); break; case 4: - fprintf(stderr, "Reason: Message too long.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Message too long.\n"); break; case 5: - fprintf(stderr, "Reason: Security Error.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Security Error.\n"); break; case 6: - fprintf(stderr, "Reason: Invalid address length.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Invalid address length.\n"); break; default: - fprintf(stderr, "Reason: %u\n", (unsigned int)npdu[0]); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, "Reason: %u\n", + (unsigned int)npdu[0]); break; } } @@ -754,6 +792,46 @@ static void routed_src_address( } } +/** + * @brief Encode a routed APDU into the provided PDU buffer + * @param pdu[out] The buffer to hold the encoded PDU + * @param dest[in] The destination BACNET_ADDRESS + * @param src[in] The source BACNET_ADDRESS + * @param npdu[in] The NPDU data to encode + * @param apdu[in] The APDU data to encode + * @param apdu_len[in] The length of the APDU data + * @return The total length of the encoded PDU, or 0 on error + */ +static int routed_npdu_apdu_encode( + uint8_t *pdu, + BACNET_ADDRESS *dest, + BACNET_ADDRESS *src, + const BACNET_NPDU_DATA *npdu, + uint8_t *apdu, + uint16_t apdu_len) +{ + int npdu_len = 0; + + npdu_len = npdu_encode_pdu(pdu, dest, src, npdu); + if (npdu_len <= 0) { + return 0; + } + if ((npdu_len + apdu_len) > sizeof(Tx_Buffer)) { + /* can't send, message too big */ + debug_log_fprintf( + DEBUG_LOG_ERROR, stderr, + "Dropping oversized routed APDU: " + "npdu_len=%d apdu_len=%u tx=%lu\n", + npdu_len, (unsigned)apdu_len, (unsigned long)sizeof(Tx_Buffer)); + return 0; + } + if (apdu && (apdu_len > 0)) { + memmove(&pdu[npdu_len], apdu, apdu_len); + } + + return npdu_len + apdu_len; +} + /** * If a BACnet NPDU is received with NPCI indicating that the message * should be relayed by virtue of the presence of a non-broadcast @@ -785,7 +863,7 @@ static void routed_apdu_handler( BACNET_ADDRESS local_dest; BACNET_ADDRESS remote_dest; BACNET_ADDRESS router_src; - int npdu_len = 0; + int pdu_len = 0; /* for broadcast messages no search is needed */ if (dest->net == BACNET_BROADCAST_NETWORK) { @@ -805,19 +883,21 @@ static void routed_apdu_handler( npdu->hop_count--; routed_src_address(&router_src, snet, src); /* encode both source and destination for broadcast */ - npdu_len = - npdu_encode_pdu(&Tx_Buffer[0], &local_dest, &router_src, npdu); - memmove(&Tx_Buffer[npdu_len], apdu, apdu_len); - /* send to my other ports */ - debug_printf("Routing a BROADCAST from %u\n", (unsigned)snet); - port = Router_Table_Head; - while (port != NULL) { - if (port->net != snet) { - datalink_send_pdu( - port->net, &local_dest, npdu, &Tx_Buffer[0], - npdu_len + apdu_len); + pdu_len = routed_npdu_apdu_encode( + &Tx_Buffer[0], &local_dest, &router_src, npdu, apdu, apdu_len); + if (pdu_len > 0) { + /* send to my other ports */ + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "Routing a BROADCAST from %u\n", + (unsigned)snet); + port = Router_Table_Head; + while (port != NULL) { + if (port->net != snet) { + datalink_send_pdu( + port->net, &local_dest, npdu, &Tx_Buffer[0], pdu_len); + } + port = port->next; } - port = port->next; } return; } @@ -825,7 +905,9 @@ static void routed_apdu_handler( port = dnet_find(dest->net, &remote_dest); if (port) { if (port->net == dest->net) { - debug_printf("Routing to Port %u\n", (unsigned)dest->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "Routing to Port %u\n", + (unsigned)dest->net); /* Case 1: the router is directly connected to the network referred to by DNET. */ /* In the first case, DNET, DADR, and Hop @@ -838,15 +920,16 @@ static void routed_apdu_handler( local_dest.net = 0; npdu->hop_count--; routed_src_address(&router_src, snet, src); - npdu_len = - npdu_encode_pdu(&Tx_Buffer[0], &local_dest, &router_src, npdu); - memmove(&Tx_Buffer[npdu_len], apdu, apdu_len); - datalink_send_pdu( - port->net, &local_dest, npdu, &Tx_Buffer[0], - npdu_len + apdu_len); + pdu_len = routed_npdu_apdu_encode( + &Tx_Buffer[0], &local_dest, &router_src, npdu, apdu, apdu_len); + if (pdu_len > 0) { + datalink_send_pdu( + port->net, &local_dest, npdu, &Tx_Buffer[0], pdu_len); + } } else { - debug_printf( - "Routing to another Router %u\n", (unsigned)remote_dest.net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "Routing to another Router %u\n", + (unsigned)remote_dest.net); /* Case 2: the message must be relayed to another router for further transmission */ /* In the second case, if the Hop Count is greater than zero, @@ -856,30 +939,34 @@ static void routed_apdu_handler( discarded. */ npdu->hop_count--; routed_src_address(&router_src, snet, src); - npdu_len = - npdu_encode_pdu(&Tx_Buffer[0], &remote_dest, &router_src, npdu); - memmove(&Tx_Buffer[npdu_len], apdu, apdu_len); - datalink_send_pdu( - port->net, &remote_dest, npdu, &Tx_Buffer[0], - npdu_len + apdu_len); + pdu_len = routed_npdu_apdu_encode( + &Tx_Buffer[0], &remote_dest, &router_src, npdu, apdu, apdu_len); + if (pdu_len > 0) { + datalink_send_pdu( + port->net, &remote_dest, npdu, &Tx_Buffer[0], pdu_len); + } } } else if (dest->net) { - debug_printf("Routing to Unknown Route %u\n", (unsigned)dest->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "Routing to Unknown Route %u\n", + (unsigned)dest->net); /* Case 3: a global broadcast is required. */ dest->mac_len = 0; npdu->hop_count--; /* encode both source and destination */ routed_src_address(&router_src, snet, src); - npdu_len = npdu_encode_pdu(&Tx_Buffer[0], dest, &router_src, npdu); - memmove(&Tx_Buffer[npdu_len], apdu, apdu_len); - /* send to all other ports */ - port = Router_Table_Head; - while (port != NULL) { - if (port->net != snet) { - datalink_send_pdu( - port->net, dest, npdu, &Tx_Buffer[0], npdu_len + apdu_len); + pdu_len = routed_npdu_apdu_encode( + &Tx_Buffer[0], dest, &router_src, npdu, apdu, apdu_len); + if (pdu_len > 0) { + /* send to all other ports */ + port = Router_Table_Head; + while (port != NULL) { + if (port->net != snet) { + datalink_send_pdu( + port->net, dest, npdu, &Tx_Buffer[0], pdu_len); + } + port = port->next; } - port = port->next; } /* If the next router is unknown, an attempt shall be made to identify it using a Who-Is-Router-To-Network message. */ @@ -912,13 +999,18 @@ static void my_routing_npdu_handler( if (!pdu) { /* no packet */ - } else { - protocol_version = pdu[0]; + return; } + if (pdu_len == 0) { + /* empty packet */ + return; + } + protocol_version = pdu[0]; if (protocol_version == BACNET_PROTOCOL_VERSION) { apdu_offset = bacnet_npdu_decode(pdu, pdu_len, &dest, src, &npdu_data); if (apdu_offset <= 0) { - fprintf(stderr, "NPDU: Decoding failed; Discarded!\n"); + debug_log_fprintf( + DEBUG_LOG_ERROR, stderr, "NPDU: Decoding failed; Discarded!\n"); } else if (npdu_data.network_layer_message) { if ((dest.net == 0) || (dest.net == BACNET_BROADCAST_NETWORK)) { network_control_handler( @@ -954,13 +1046,15 @@ static void my_routing_npdu_handler( } } } else { - fprintf( - stderr, "NPDU: DNET=%u. Discarded!\n", (unsigned)dest.net); + debug_log_fprintf( + DEBUG_LOG_ERROR, stderr, "NPDU: DNET=%u. Discarded!\n", + (unsigned)dest.net); } } } else { - fprintf( - stderr, "NPDU: unsupported protocol version %u. Discarded!\n", + debug_log_fprintf( + DEBUG_LOG_ERROR, stderr, + "NPDU: unsupported protocol version %u. Discarded!\n", protocol_version); } @@ -975,8 +1069,14 @@ static void datalink_init(void) char *pEnv = NULL; BACNET_ADDRESS my_address = { 0 }; + pEnv = getenv("BACNET_ROUTER_DEBUG"); + if (pEnv) { + bip_debug_enable(); + fprintf(stderr, "Debug=enabled\n"); + } else { + fprintf(stderr, "Debug=disabled\n"); + } /* BACnet/IP Initialization */ - bip_debug_enable(); pEnv = getenv("BACNET_IP_PORT"); if (pEnv) { bip_set_port((uint16_t)strtol(pEnv, NULL, 0)); @@ -1043,7 +1143,7 @@ static void cleanup(void) { DNET *port = NULL; - fprintf(stderr, "Cleaning up...\n"); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "Cleaning up...\n"); /* clean up the remote networks */ port = Router_Table_Head; while (port != NULL) { @@ -1095,6 +1195,7 @@ static void control_c_hooks(void) } #endif +#ifndef FUZZING /** * Main function of simple router demo. * @@ -1134,7 +1235,8 @@ int main(int argc, char *argv[]) bip_receive(&src, &BIP_Rx_Buffer[0], sizeof(BIP_Rx_Buffer), 5); /* process */ if (pdu_len) { - debug_printf("BACnet/IP Received packet\n"); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "BACnet/IP Received packet\n"); my_routing_npdu_handler(BIP_Net, &src, &BIP_Rx_Buffer[0], pdu_len); } /* returns 0 bytes on timeout */ @@ -1142,7 +1244,8 @@ int main(int argc, char *argv[]) bip6_receive(&src, &BIP6_Rx_Buffer[0], sizeof(BIP6_Rx_Buffer), 5); /* process */ if (pdu_len) { - debug_printf("BACnet/IPv6 Received packet\n"); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "BACnet/IPv6 Received packet\n"); my_routing_npdu_handler( BIP6_Net, &src, &BIP6_Rx_Buffer[0], pdu_len); } @@ -1162,3 +1265,4 @@ int main(int argc, char *argv[]) return 0; } +#endif diff --git a/apps/router-mstp/main.c b/apps/router-mstp/main.c index ca6f7f048e..68fb4ae8b2 100644 --- a/apps/router-mstp/main.c +++ b/apps/router-mstp/main.c @@ -36,6 +36,22 @@ #include "bacnet/datalink/bvlc.h" #include "bacnet/basic/bbmd/h_bbmd.h" +#ifndef DEBUG_LOG_DEBUG +#define DEBUG_LOG_DISABLED 0 +#define DEBUG_LOG_ERROR 1 +#define DEBUG_LOG_INFO 2 +#define DEBUG_LOG_DEBUG 3 +#endif + +#ifndef debug_log_fprintf +#define debug_log_fprintf(level, stream, ...) \ + debug_fprintf((stream), __VA_ARGS__) +#endif + +#ifndef debug_log_severity_set +#define debug_log_severity_set(level) ((void)(level)) +#endif + /* current version of the BACnet stack */ static const char *BACnet_Version = BACNET_VERSION_TEXT; @@ -80,29 +96,6 @@ static uint8_t MSTP_Rx_Buffer[DLMSTP_MPDU_MAX]; static uint8_t Tx_Buffer[MAX(DLMSTP_MPDU_MAX, BIP_MPDU_MAX)]; /* main loop exit control */ static bool Exit_Requested; -/* debugging info */ -static bool Debug_Enabled; - -/** - * @brief print debug info if debug is enabled - * @param format - printf format string - * @param ... variable arguments - * @return number of bytes printed - */ -static int log_printf(const char *format, ...) -{ - int length = 0; - va_list ap; - - if (Debug_Enabled) { - va_start(ap, format); - length = vfprintf(stdout, format, ap); - va_end(ap); - fflush(stdout); - } - - return length; -} /** * Search the router table to find a matching DNET entry @@ -290,7 +283,8 @@ static void dnet_cleanup(DNET *dnets) { DNET *dnet = dnets; while (dnet != NULL) { - log_printf("DNET %u removed\n", (unsigned)dnet->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "DNET %u removed\n", (unsigned)dnet->net); dnet = dnet->next; free(dnets); dnets = dnet; @@ -334,14 +328,20 @@ static int datalink_send_pdu( int bytes_sent = 0; if (snet == 0) { - log_printf("BVLC & MS/TP Send to DNET %u\n", (unsigned)dest->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "BVLC & MS/TP Send to DNET %u\n", + (unsigned)dest->net); bytes_sent = bip_send_pdu(dest, npdu_data, pdu, pdu_len); bytes_sent = dlmstp_send_pdu(dest, npdu_data, pdu, pdu_len); } else if (snet == BIP_Net) { - log_printf("BVLC Send to DNET %u\n", (unsigned)dest->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "BVLC Send to DNET %u\n", + (unsigned)dest->net); bytes_sent = bip_send_pdu(dest, npdu_data, pdu, pdu_len); } else if (snet == MSTP_Net) { - log_printf("MS/TP Send to DNET %u\n", (unsigned)dest->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "MS/TP Send to DNET %u\n", + (unsigned)dest->net); bytes_sent = dlmstp_send_pdu(dest, npdu_data, pdu, pdu_len); } @@ -379,7 +379,7 @@ static void send_i_am_router_to_network(uint16_t snet, uint16_t net) len = encode_unsigned16(&Tx_Buffer[pdu_len], net); pdu_len += len; } else { - log_printf("I-Am-Router-To-Network "); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "I-Am-Router-To-Network "); /* Each router shall broadcast out each port an I-Am-Router-To-Network message containing the network numbers of each accessible network except the networks @@ -390,12 +390,12 @@ static void send_i_am_router_to_network(uint16_t snet, uint16_t net) port = Router_Table_Head; while (port != NULL) { if (port->net != snet) { - log_printf("%u,", port->net); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "%u,", port->net); len = encode_unsigned16(&Tx_Buffer[pdu_len], port->net); pdu_len += len; dnet = port->dnets; while (dnet != NULL) { - log_printf("%u,", dnet->net); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "%u,", dnet->net); len = encode_unsigned16(&Tx_Buffer[pdu_len], dnet->net); pdu_len += len; dnet = dnet->next; @@ -403,7 +403,7 @@ static void send_i_am_router_to_network(uint16_t snet, uint16_t net) } port = port->next; } - log_printf("from %u\n", snet); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "from %u\n", snet); } datalink_send_pdu(snet, &dest, &npdu_data, &Tx_Buffer[0], pdu_len); } @@ -662,10 +662,8 @@ static void network_control_handler( uint16_t dnet = 0; const char *msg_name = NULL; - (void)src; - (void)npdu_data; msg_name = bactext_network_layer_msg_name(npdu_data->network_message_type); - fprintf(stderr, "Received %s\n", msg_name); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "Received %s\n", msg_name); switch (npdu_data->network_message_type) { case NETWORK_MESSAGE_WHO_IS_ROUTER_TO_NETWORK: who_is_router_to_network_handler( @@ -682,32 +680,47 @@ static void network_control_handler( case NETWORK_MESSAGE_REJECT_MESSAGE_TO_NETWORK: if (npdu_len >= 3) { decode_unsigned16(&npdu[1], &dnet); - fprintf(stderr, "for Network:%hu\n", dnet); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, "for Network:%hu\n", dnet); switch (npdu[0]) { case 0: - fprintf(stderr, "Reason: Other Error.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, "Reason: Other Error.\n"); break; case 1: - fprintf(stderr, "Reason: Network unreachable.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Network unreachable.\n"); break; case 2: - fprintf(stderr, "Reason: Network is busy.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Network is busy.\n"); break; case 3: - fprintf( - stderr, "Reason: Unknown network message type.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Unknown network message type.\n"); break; case 4: - fprintf(stderr, "Reason: Message too long.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Message too long.\n"); break; case 5: - fprintf(stderr, "Reason: Security Error.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Security Error.\n"); break; case 6: - fprintf(stderr, "Reason: Invalid address length.\n"); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, + "Reason: Invalid address length.\n"); break; default: - fprintf(stderr, "Reason: %u\n", (unsigned int)npdu[0]); + debug_log_fprintf( + DEBUG_LOG_INFO, stderr, "Reason: %u\n", + (unsigned int)npdu[0]); break; } } @@ -721,9 +734,30 @@ static void network_control_handler( * NETWORK_MESSAGE_INIT_RT_TABLE_ACK and a list of all our * reachable networks. */ - npdu_init_routing_table_process( - snet, src, npdu, npdu_len, dnet_add); - send_initialize_routing_table_ack(snet, NULL); + if (npdu_len > 0) { + /* If Number of Ports is 0, broadcast our "full" table */ + if (npdu[0] == 0) { + send_initialize_routing_table_ack(snet, NULL); + } else { + /* they sent us a list */ + int net_count = npdu[0]; + while (net_count--) { + int i = 1; + /* DNET */ + decode_unsigned16(&npdu[i], &dnet); + /* update routing table */ + dnet_add(snet, dnet, src); + if (npdu[i + 3] > 0) { + /* find next NET value */ + i = npdu[i + 3] + 4; + } else { + i += 4; + } + } + send_initialize_routing_table_ack(snet, NULL); + } + break; + } break; case NETWORK_MESSAGE_INIT_RT_TABLE_ACK: /* Do nothing with the routing table info, since don't support @@ -778,6 +812,46 @@ static void routed_src_address( } } +/** + * @brief Encode a routed APDU into the provided PDU buffer. + * @param pdu [out] The buffer to hold the encoded PDU. + * @param dest [in] The destination BACNET_ADDRESS. + * @param src [in] The source BACNET_ADDRESS. + * @param npdu [in] The NPDU data to encode. + * @param apdu [in] The APDU data to encode. + * @param apdu_len [in] The length of the APDU data. + * @return The total length of the encoded PDU, or 0 on error. + */ +static int routed_npdu_apdu_encode( + uint8_t *pdu, + BACNET_ADDRESS *dest, + BACNET_ADDRESS *src, + const BACNET_NPDU_DATA *npdu, + uint8_t *apdu, + uint16_t apdu_len) +{ + int npdu_len = 0; + + npdu_len = npdu_encode_pdu(pdu, dest, src, npdu); + if (npdu_len <= 0) { + return 0; + } + if ((npdu_len + apdu_len) > sizeof(Tx_Buffer)) { + /* can't send, message too big */ + debug_log_fprintf( + DEBUG_LOG_ERROR, stderr, + "Dropping oversized routed APDU: " + "npdu_len=%d apdu_len=%u tx=%lu\n", + npdu_len, (unsigned)apdu_len, (unsigned long)sizeof(Tx_Buffer)); + return 0; + } + if (apdu && (apdu_len > 0)) { + memmove(&pdu[npdu_len], apdu, apdu_len); + } + + return npdu_len + apdu_len; +} + /** * If a BACnet NPDU is received with NPCI indicating that the message * should be relayed by virtue of the presence of a non-broadcast @@ -809,7 +883,7 @@ static void routed_apdu_handler( BACNET_ADDRESS local_dest; BACNET_ADDRESS remote_dest; BACNET_ADDRESS router_src; - int npdu_len = 0; + int pdu_len = 0; /* for broadcast messages no search is needed */ if (dest->net == BACNET_BROADCAST_NETWORK) { @@ -829,19 +903,21 @@ static void routed_apdu_handler( npdu->hop_count--; routed_src_address(&router_src, snet, src); /* encode both source and destination for broadcast */ - npdu_len = - npdu_encode_pdu(&Tx_Buffer[0], &local_dest, &router_src, npdu); - memmove(&Tx_Buffer[npdu_len], apdu, apdu_len); - /* send to my other ports */ - log_printf("Routing a BROADCAST from %u\n", (unsigned)snet); - port = Router_Table_Head; - while (port != NULL) { - if (port->net != snet) { - datalink_send_pdu( - port->net, &local_dest, npdu, &Tx_Buffer[0], - npdu_len + apdu_len); + pdu_len = routed_npdu_apdu_encode( + &Tx_Buffer[0], &local_dest, &router_src, npdu, apdu, apdu_len); + if (pdu_len > 0) { + /* send to my other ports */ + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "Routing a BROADCAST from %u\n", + (unsigned)snet); + port = Router_Table_Head; + while (port != NULL) { + if (port->net != snet) { + datalink_send_pdu( + port->net, &local_dest, npdu, &Tx_Buffer[0], pdu_len); + } + port = port->next; } - port = port->next; } return; } @@ -849,7 +925,9 @@ static void routed_apdu_handler( port = dnet_find(dest->net, &remote_dest); if (port) { if (port->net == dest->net) { - log_printf("Routing to Port %u\n", (unsigned)dest->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "Routing to Port %u\n", + (unsigned)dest->net); /* Case 1: the router is directly connected to the network referred to by DNET. */ /* In the first case, DNET, DADR, and Hop @@ -862,15 +940,16 @@ static void routed_apdu_handler( local_dest.net = 0; npdu->hop_count--; routed_src_address(&router_src, snet, src); - npdu_len = - npdu_encode_pdu(&Tx_Buffer[0], &local_dest, &router_src, npdu); - memmove(&Tx_Buffer[npdu_len], apdu, apdu_len); - datalink_send_pdu( - port->net, &local_dest, npdu, &Tx_Buffer[0], - npdu_len + apdu_len); + pdu_len = routed_npdu_apdu_encode( + &Tx_Buffer[0], &local_dest, &router_src, npdu, apdu, apdu_len); + if (pdu_len > 0) { + datalink_send_pdu( + port->net, &local_dest, npdu, &Tx_Buffer[0], pdu_len); + } } else { - log_printf( - "Routing to another Router %u\n", (unsigned)remote_dest.net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "Routing to another Router %u\n", + (unsigned)remote_dest.net); /* Case 2: the message must be relayed to another router for further transmission */ /* In the second case, if the Hop Count is greater than zero, @@ -880,30 +959,34 @@ static void routed_apdu_handler( discarded. */ npdu->hop_count--; routed_src_address(&router_src, snet, src); - npdu_len = - npdu_encode_pdu(&Tx_Buffer[0], &remote_dest, &router_src, npdu); - memmove(&Tx_Buffer[npdu_len], apdu, apdu_len); - datalink_send_pdu( - port->net, &remote_dest, npdu, &Tx_Buffer[0], - npdu_len + apdu_len); + pdu_len = routed_npdu_apdu_encode( + &Tx_Buffer[0], &remote_dest, &router_src, npdu, apdu, apdu_len); + if (pdu_len > 0) { + datalink_send_pdu( + port->net, &remote_dest, npdu, &Tx_Buffer[0], pdu_len); + } } } else if (dest->net) { - log_printf("Routing to Unknown Route %u\n", (unsigned)dest->net); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "Routing to Unknown Route %u\n", + (unsigned)dest->net); /* Case 3: a global broadcast is required. */ dest->mac_len = 0; npdu->hop_count--; /* encode both source and destination */ routed_src_address(&router_src, snet, src); - npdu_len = npdu_encode_pdu(&Tx_Buffer[0], dest, &router_src, npdu); - memmove(&Tx_Buffer[npdu_len], apdu, apdu_len); - /* send to all other ports */ - port = Router_Table_Head; - while (port != NULL) { - if (port->net != snet) { - datalink_send_pdu( - port->net, dest, npdu, &Tx_Buffer[0], npdu_len + apdu_len); + pdu_len = routed_npdu_apdu_encode( + &Tx_Buffer[0], dest, &router_src, npdu, apdu, apdu_len); + if (pdu_len > 0) { + /* send to all other ports */ + port = Router_Table_Head; + while (port != NULL) { + if (port->net != snet) { + datalink_send_pdu( + port->net, dest, npdu, &Tx_Buffer[0], pdu_len); + } + port = port->next; } - port = port->next; } /* If the next router is unknown, an attempt shall be made to identify it using a Who-Is-Router-To-Network message. */ @@ -930,15 +1013,24 @@ static void my_routing_npdu_handler( uint16_t snet, BACNET_ADDRESS *src, uint8_t *pdu, uint16_t pdu_len) { int apdu_offset = 0; + unsigned protocol_version = 0; BACNET_ADDRESS dest = { 0 }; BACNET_NPDU_DATA npdu_data = { 0 }; if (!pdu) { /* no packet */ - } else if (pdu[0] == BACNET_PROTOCOL_VERSION) { + return; + } + if (pdu_len == 0) { + /* empty packet */ + return; + } + protocol_version = pdu[0]; + if (protocol_version == BACNET_PROTOCOL_VERSION) { apdu_offset = bacnet_npdu_decode(pdu, pdu_len, &dest, src, &npdu_data); if (apdu_offset <= 0) { - fprintf(stderr, "NPDU: Decoding failed; Discarded!\n"); + debug_log_fprintf( + DEBUG_LOG_ERROR, stderr, "NPDU: Decoding failed; Discarded!\n"); } else if (npdu_data.network_layer_message) { if ((dest.net == 0) || (dest.net == BACNET_BROADCAST_NETWORK)) { network_control_handler( @@ -974,12 +1066,16 @@ static void my_routing_npdu_handler( } } } else { - fprintf( - stderr, "NPDU: DNET=%u. Discarded!\n", (unsigned)dest.net); + debug_log_fprintf( + DEBUG_LOG_ERROR, stderr, "NPDU: DNET=%u. Discarded!\n", + (unsigned)dest.net); } } } else { - /* unsupported protocol version */ + debug_log_fprintf( + DEBUG_LOG_ERROR, stderr, + "NPDU: unsupported protocol version %u. Discarded!\n", + protocol_version); } return; @@ -996,8 +1092,7 @@ static void datalink_init(void) pEnv = getenv("BACNET_ROUTER_DEBUG"); if (pEnv) { bip_debug_enable(); - Debug_Enabled = true; - log_printf("Debug=enabled\n"); + fprintf(stderr, "Debug=enabled\n"); } else { fprintf(stderr, "Debug=disabled\n"); } @@ -1079,7 +1174,7 @@ static void cleanup(void) { DNET *port = NULL; - fprintf(stderr, "Cleaning up...\n"); + debug_log_fprintf(DEBUG_LOG_INFO, stderr, "Cleaning up...\n"); /* clean up the remote networks */ port = Router_Table_Head; while (port != NULL) { @@ -1101,6 +1196,8 @@ static BOOL WINAPI CtrlCHandler(DWORD dwCtrlType) Sleep(100); } exit(0); + + return TRUE; } static void control_c_hooks(void) @@ -1168,7 +1265,8 @@ int main(int argc, char *argv[]) bip_receive(&src, &BIP_Rx_Buffer[0], sizeof(BIP_Rx_Buffer), 5); /* process */ if (pdu_len) { - log_printf("BACnet/IP Received packet\n"); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "BACnet/IP Received packet\n"); my_routing_npdu_handler(BIP_Net, &src, &BIP_Rx_Buffer[0], pdu_len); } /* returns 0 bytes on timeout */ @@ -1176,7 +1274,8 @@ int main(int argc, char *argv[]) dlmstp_receive(&src, &MSTP_Rx_Buffer[0], sizeof(MSTP_Rx_Buffer), 5); /* process */ if (pdu_len) { - log_printf("BACnet MS/TP Received packet\n"); + debug_log_fprintf( + DEBUG_LOG_DEBUG, stderr, "BACnet MS/TP Received packet\n"); my_routing_npdu_handler( MSTP_Net, &src, &MSTP_Rx_Buffer[0], pdu_len); } From 1538542476c5fedab818bb56a25c8f5fb0ed1d51 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 17:02:59 -0500 Subject: [PATCH 33/42] security: backport #1395 with changelog/security and code patch --- CHANGELOG.md | 2 + SECURITY.md | 5 ++ src/bacnet/basic/service/h_rpm_a.c | 37 +++++---- test/CMakeLists.txt | 2 + .../basic/service/h_rpm_a/CMakeLists.txt | 79 +++++++++++++++++++ test/bacnet/basic/service/h_rpm_a/src/main.c | 74 +++++++++++++++++ 6 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 test/bacnet/basic/service/h_rpm_a/CMakeLists.txt create mode 100644 test/bacnet/basic/service/h_rpm_a/src/main.c diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e328001c..2b6e28bf16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ The git repositories are hosted at the following sites: in router applications to prevent buffer overrun. (#1386, #1387) * Secured apps/router-mstp and apps/router-ipv6 routing by introducing routed_npdu_apdu_encode() with explicit oversized-PDU drop checks. (#1392) +* Secured rpm_ack_decode_service_request buffer overflow by validating data + length and remaining bytes. Added decoder-path unit tests. (#1395) * Secured rpm_decode_object_property by fixing a DoS vulnerability for malformed RPM requests. (#1374) * Secured bsc_node_parse_urls() by fixing buffer overflows by using relative diff --git a/SECURITY.md b/SECURITY.md index 5f3b28768d..beb9cbd39b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -48,6 +48,11 @@ Remote global-buffer-overflow in apps/router-ipv6/main.c routed APDU forwarding Patched versions: 1.5.1 Pull Request: [#1392](https://github.com/bacnet-stack/bacnet-stack/pull/1392). +Remote Global-Buffer-Overflow Read in readpropm via Malformed ReadPropertyMultiple-ACK +[GHSA-3xxw-jfwm-rq9c](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-3xxw-jfwm-rq9c). +Patched versions: 1.5.1 +Pull Request: [#1395](https://github.com/bacnet-stack/bacnet-stack/pull/1395). + [CVE-2026-52789](https://www.cve.org/CVERecord?id=CVE-2026-52789) - Denial of Service (Infinite Loop) in handler_read_property_multiple via malformed RPM requests. [GHSA-4rf9-4vgq-5gcw](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4rf9-4vgq-5gcw). diff --git a/src/bacnet/basic/service/h_rpm_a.c b/src/bacnet/basic/service/h_rpm_a.c index 1b39e3ad2f..f90a4eeca5 100644 --- a/src/bacnet/basic/service/h_rpm_a.c +++ b/src/bacnet/basic/service/h_rpm_a.c @@ -46,6 +46,7 @@ int rpm_ack_decode_service_request( int len = 0; /* number of bytes returned from decoding */ uint8_t tag_number = 0; /* decoded tag number */ int data_len = 0; /* data blob length */ + int data_remaining = 0; /* bytes left in the current data blob */ int tag_len = 0; /* length of the tag portion of the data */ BACNET_READ_ACCESS_DATA *rpm_object; BACNET_READ_ACCESS_DATA *old_rpm_object; @@ -98,10 +99,15 @@ int rpm_ack_decode_service_request( if (apdu_len && bacnet_is_opening_tag_number(apdu, apdu_len, 4, &tag_len)) { data_len = bacnet_enclosed_data_length(apdu, apdu_len); + if (data_len < 0) { + PERROR("RPM Ack: invalid enclosed property value length\n"); + return BACNET_STATUS_ERROR; + } /* propertyValue */ decoded_len += tag_len; apdu_len -= tag_len; apdu += tag_len; + data_remaining = data_len; value = calloc(1, sizeof(BACNET_APPLICATION_DATA_VALUE)); rpm_property->value = value; if (apdu_len && @@ -126,27 +132,26 @@ int rpm_ack_decode_service_request( * OK. */ if (len < 0) { /* problem decoding */ - if (data_len >= 0) { - /* valid data that we'll skip over */ - len = data_len; - bacapp_value_list_init(value, 1); - } else { - PERROR( - "RPM Ack: unable to decode! %s:%s\n", - bactext_object_type_name( - rpm_object->object_type), - bactext_property_name( - rpm_property->propertyIdentifier)); - /* note: caller will free the memory */ - return BACNET_STATUS_ERROR; - } + len = data_remaining; + bacapp_value_list_init(value, 1); + } + if (len > data_remaining) { + PERROR("RPM Ack: decoded length exceeds property " + "value length\n"); + return BACNET_STATUS_ERROR; } decoded_len += len; apdu_len -= len; apdu += len; - if (apdu_len && + data_remaining -= len; + if ((apdu_len < 0) || (data_remaining < 0)) { + PERROR("RPM Ack: invalid remaining length while " + "decoding property value\n"); + return BACNET_STATUS_ERROR; + } + if (apdu_len > 0 && bacnet_is_closing_tag_number( - apdu, apdu_len, 4, &tag_len)) { + apdu, (unsigned)apdu_len, 4, &tag_len)) { decoded_len += tag_len; apdu_len -= tag_len; apdu += tag_len; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4cae33a85a..30f0b5ccfa 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -193,6 +193,8 @@ list(APPEND testdirs bacnet/basic/object/trendlog # basic/program bacnet/basic/program/ubasic + # basic/service + bacnet/basic/service/h_rpm_a # basic/server bacnet/basic/server/bacnet_device # basic/sys diff --git a/test/bacnet/basic/service/h_rpm_a/CMakeLists.txt b/test/bacnet/basic/service/h_rpm_a/CMakeLists.txt new file mode 100644 index 0000000000..f2f86c4655 --- /dev/null +++ b/test/bacnet/basic/service/h_rpm_a/CMakeLists.txt @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: MIT + +cmake_minimum_required(VERSION 3.10 FATAL_ERROR) + +get_filename_component(basename ${CMAKE_CURRENT_SOURCE_DIR} NAME) +project(test_${basename} + VERSION 1.0.0 + LANGUAGES C) + +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z0-9_/-]*$" + "/src" + SRC_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z0-9_/-]*$" + "/test" + TST_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) + +set(ZTST_DIR "${TST_DIR}/ztest/src") + +add_compile_definitions( + BACNET_BIG_ENDIAN=0 + CONFIG_ZTEST=1 + BACDL_NONE=1 + BACAPP_ALL +) + +include_directories( + ${SRC_DIR} + ${TST_DIR}/ztest/include +) + +add_executable(${PROJECT_NAME} + # File(s) under test + ${SRC_DIR}/bacnet/basic/service/h_rpm_a.c + # Support files and stubs (pathname alphabetical) + ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/authentication_factor.c + ${SRC_DIR}/bacnet/authentication_factor_format.c + ${SRC_DIR}/bacnet/bacaction.c + ${SRC_DIR}/bacnet/bacaddr.c + ${SRC_DIR}/bacnet/bacapp.c + ${SRC_DIR}/bacnet/bacdcode.c + ${SRC_DIR}/bacnet/bacdest.c + ${SRC_DIR}/bacnet/bacdevobjpropref.c + ${SRC_DIR}/bacnet/abort.c + ${SRC_DIR}/bacnet/bacerror.c + ${SRC_DIR}/bacnet/reject.c + ${SRC_DIR}/bacnet/bacint.c + ${SRC_DIR}/bacnet/baclog.c + ${SRC_DIR}/bacnet/bacreal.c + ${SRC_DIR}/bacnet/bacstr.c + ${SRC_DIR}/bacnet/bactext.c + ${SRC_DIR}/bacnet/basic/sys/bigend.c + ${SRC_DIR}/bacnet/basic/sys/debug.c + ${SRC_DIR}/bacnet/datetime.c + ${SRC_DIR}/bacnet/basic/sys/days.c + ${SRC_DIR}/bacnet/indtext.c + ${SRC_DIR}/bacnet/hostnport.c + ${SRC_DIR}/bacnet/lighting.c + ${SRC_DIR}/bacnet/shed_level.c + ${SRC_DIR}/bacnet/timer_value.c + ${SRC_DIR}/bacnet/timestamp.c + ${SRC_DIR}/bacnet/memcopy.c + ${SRC_DIR}/bacnet/weeklyschedule.c + ${SRC_DIR}/bacnet/bactimevalue.c + ${SRC_DIR}/bacnet/dailyschedule.c + ${SRC_DIR}/bacnet/calendar_entry.c + ${SRC_DIR}/bacnet/special_event.c + ${SRC_DIR}/bacnet/channel_value.c + ${SRC_DIR}/bacnet/secure_connect.c + ${SRC_DIR}/bacnet/rpm.c + # Test and test library files + ./src/main.c + ${ZTST_DIR}/ztest_mock.c + ${ZTST_DIR}/ztest.c +) diff --git a/test/bacnet/basic/service/h_rpm_a/src/main.c b/test/bacnet/basic/service/h_rpm_a/src/main.c new file mode 100644 index 0000000000..660f08533c --- /dev/null +++ b/test/bacnet/basic/service/h_rpm_a/src/main.c @@ -0,0 +1,74 @@ +/** + * @file + * @brief Unit tests for handler_read_property_multiple_ack decoder paths + * @copyright SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(h_rpm_a_tests, testMalformedTag4PayloadReturnsError) +#else +static void testMalformedTag4PayloadReturnsError(void) +#endif +{ + uint8_t service_request[480] = { 0 }; + uint8_t value_buffer[16] = { 0 }; + BACNET_RPM_DATA rpmdata = { 0 }; + BACNET_READ_ACCESS_DATA *read_access_data = NULL; + int len = 0; + int value_len = 0; + int test_len = 0; + + rpmdata.object_type = OBJECT_DEVICE; + rpmdata.object_instance = 123; + len += rpm_ack_encode_apdu_object_begin(&service_request[len], &rpmdata); + len += rpm_ack_encode_apdu_object_property( + &service_request[len], PROP_OBJECT_LIST, BACNET_ARRAY_ALL); + + len += encode_opening_tag(&service_request[len], 4); + + value_len = encode_application_object_id( + &value_buffer[0], OBJECT_DEVICE, rpmdata.object_instance); + zassert_true(value_len > 0, NULL); + memcpy(&service_request[len], &value_buffer[0], (size_t)value_len); + len += value_len; + + /* Truncated second value in same tag-4 payload to force partial decode. */ + value_len = encode_application_real(&value_buffer[0], 1.0f); + zassert_true(value_len >= 5, NULL); + memcpy(&service_request[len], &value_buffer[0], 2); + len += 2; + + len += encode_closing_tag(&service_request[len], 4); + len += rpm_ack_encode_apdu_object_end(&service_request[len]); + + read_access_data = calloc(1, sizeof(BACNET_READ_ACCESS_DATA)); + zassert_not_null(read_access_data, NULL); + + test_len = + rpm_ack_decode_service_request(service_request, len, read_access_data); + zassert_equal( + test_len, BACNET_STATUS_ERROR, + "rpm_ack_decode_service_request returned %d, expected %d", test_len, + BACNET_STATUS_ERROR); + + while (read_access_data) { + read_access_data = rpm_data_free(read_access_data); + } +} + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST_SUITE(h_rpm_a_tests, NULL, NULL, NULL, NULL, NULL); +#else +void test_main(void) +{ + ztest_test_suite( + h_rpm_a_tests, ztest_unit_test(testMalformedTag4PayloadReturnsError)); + ztest_run_test_suite(h_rpm_a_tests); +} +#endif From df5d537c61638da3b08505b72d3df607cd9e699c Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 17:04:57 -0500 Subject: [PATCH 34/42] security: backport #1408 with changelog/security and code patch --- CHANGELOG.md | 4 + SECURITY.md | 10 ++ src/bacnet/basic/sys/bramfs.c | 98 ++++++++++++------ test/bacnet/basic/sys/bramfs/src/main.c | 126 +++++++++++++++++++++++- 4 files changed, 205 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b6e28bf16..9ee00e81ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ The git repositories are hosted at the following sites: routed_npdu_apdu_encode() with explicit oversized-PDU drop checks. (#1392) * Secured rpm_ack_decode_service_request buffer overflow by validating data length and remaining bytes. Added decoder-path unit tests. (#1395) +* Secured the basic RAMFS to prevent heap out-of-bounds read during + record replacement in AtomicWriteFile record-access handling. (#1408) +* Secured the basic RAMFS to prevent buffer overrun during consecutive + AtomicWriteFile record appends. (#1408) * Secured rpm_decode_object_property by fixing a DoS vulnerability for malformed RPM requests. (#1374) * Secured bsc_node_parse_urls() by fixing buffer overflows by using relative diff --git a/SECURITY.md b/SECURITY.md index beb9cbd39b..79d3a532c2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -53,6 +53,16 @@ Remote Global-Buffer-Overflow Read in readpropm via Malformed ReadPropertyMultip Patched versions: 1.5.1 Pull Request: [#1395](https://github.com/bacnet-stack/bacnet-stack/pull/1395). +Replacing a RAMFS record with a shorter one via AtomicWriteFile(record-access) can trigger a heap out-of-bounds read +[GHSA-cf8g-hp9m-9fvv](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-cf8g-hp9m-9fvv). +Patched versions: 1.5.1 +Pull Request: [#1408](https://github.com/bacnet-stack/bacnet-stack/pull/1408). + +Consecutive AtomicWriteFile(record-access) appends can trigger a heap out-of-bounds read in the RAMFS file backend +[GHSA-32jj-x86x-w98w](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-32jj-x86x-w98w). +Patched versions: 1.5.1 +Pull Request: [#1408](https://github.com/bacnet-stack/bacnet-stack/pull/1408). + [CVE-2026-52789](https://www.cve.org/CVERecord?id=CVE-2026-52789) - Denial of Service (Infinite Loop) in handler_read_property_multiple via malformed RPM requests. [GHSA-4rf9-4vgq-5gcw](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4rf9-4vgq-5gcw). diff --git a/src/bacnet/basic/sys/bramfs.c b/src/bacnet/basic/sys/bramfs.c index cff4f167ae..e01011a3b1 100644 --- a/src/bacnet/basic/sys/bramfs.c +++ b/src/bacnet/basic/sys/bramfs.c @@ -262,22 +262,30 @@ size_t bacfile_ramfs_write_stream_data( /** * @brief Count the number of records in a file * @param records - string of null-terminated records + * @param size - total buffer size in bytes * @return number of records */ -static size_t record_count(const char *records) +static size_t record_count(const char *records, size_t size) { size_t count = 0; int len = 0; + const char *end; - if (records) { - do { - len = bacnet_strnlen(records, MAX_OCTET_STRING_BYTES); - if (len > 0) { - count++; - records = records + len + 1; - } - } while (len > 0); + if (!records) { + return 0; } + end = records + size; + + do { + if (records >= end) { + break; + } + len = bacnet_strnlen(records, MAX_OCTET_STRING_BYTES); + if (len > 0) { + count++; + records = records + len + 1; + } + } while (len > 0); return count; } @@ -286,25 +294,33 @@ static size_t record_count(const char *records) * @brief Get the specific record at index 0..N * @param records - string of null-terminated records * @param record_index - record index number 0..N of the records + * @param size - total buffer size in bytes * @return record, or NULL */ -static char *record_by_index(char *records, size_t index) +static char *record_by_index(char *records, size_t index, size_t size) { size_t count = 0; int len = 0; + const char *end; - if (records) { - do { - len = bacnet_strnlen(records, MAX_OCTET_STRING_BYTES); - if (len > 0) { - if (index == count) { - return records; - } - count++; - records = records + len + 1; - } - } while (len > 0); + if (!records) { + return NULL; } + end = records + size; + + do { + if (records >= end) { + break; + } + len = bacnet_strnlen(records, MAX_OCTET_STRING_BYTES); + if (len > 0) { + if (index == count) { + return records; + } + count++; + records = records + len + 1; + } + } while (len > 0); return NULL; } @@ -336,6 +352,9 @@ bool bacfile_ramfs_write_record_data( char *record; size_t record_len; size_t tail_record_len; + char *tail_data = NULL; + size_t tail_data_len = 0; + size_t new_size = 0; char fileDataStr[MAX_OCTET_STRING_BYTES + 1] = { 0 }; /* +1 for null terminator */ @@ -343,7 +362,7 @@ bool bacfile_ramfs_write_record_data( pFile = bacfile_ramfs_open(pathname); if (pFile) { - fileRecordCount = record_count(pFile->data); + fileRecordCount = record_count(pFile->data, pFile->size); if (fileStartRecord == -1) { /* If 'File Start Record' parameter has the special value -1, then the write operation shall be treated @@ -369,23 +388,38 @@ bool bacfile_ramfs_write_record_data( } if (fileSeekRecord < fileRecordCount) { /* find the old record length */ - record = record_by_index(pFile->data, fileSeekRecord); + record = record_by_index(pFile->data, fileSeekRecord, pFile->size); record_len = bacnet_strnlen(record, MAX_OCTET_STRING_BYTES); tail_record_len = pFile->size - (record - pFile->data) - record_len; + /* save tail data (excluding old record's null terminator) before + realloc (may be lost if buffer shrinks) */ + tail_data = NULL; + tail_data_len = 0; + if (tail_record_len > 1) { + tail_data_len = tail_record_len - 1; + tail_data = malloc(tail_data_len); + if (tail_data) { + memcpy(tail_data, record + record_len + 1, tail_data_len); + } else { + return false; /* out of memory */ + } + } /* reallocate file to make room for new record */ - record = realloc( - pFile->data, pFile->size - record_len + fileDataStrLen + 1); + new_size = pFile->size - record_len + fileDataStrLen + 1; + record = realloc(pFile->data, new_size); if (!record) { + free(tail_data); return false; /* out of memory */ } pFile->data = record; + pFile->size = new_size; /* find the old record position after a realloc */ - record = record_by_index(pFile->data, fileSeekRecord); - /* move all existing records after the inserted record */ - if (tail_record_len > 0) { - memmove( - record + fileDataStrLen, record + record_len, - tail_record_len); + record = record_by_index(pFile->data, fileSeekRecord, pFile->size); + /* restore tail data to new position (after new record + null + terminator) */ + if (tail_data && tail_data_len > 0) { + memmove(record + fileDataStrLen + 1, tail_data, tail_data_len); + free(tail_data); } } else { /* extend the file by this one record */ @@ -437,7 +471,7 @@ bool bacfile_ramfs_read_record_data( if (pFile) { fileSeekRecord = fileStartRecord + fileIndexRecord; /* seek to the start record */ - record = record_by_index(pFile->data, fileSeekRecord); + record = record_by_index(pFile->data, fileSeekRecord, pFile->size); if (record) { record_len = bacnet_strnlen(record, MAX_OCTET_STRING_BYTES); if ((record_len > 0) && (record_len <= fileDataLen)) { diff --git a/test/bacnet/basic/sys/bramfs/src/main.c b/test/bacnet/basic/sys/bramfs/src/main.c index 68aa48b4ca..a3c2e8e8d0 100644 --- a/test/bacnet/basic/sys/bramfs/src/main.c +++ b/test/bacnet/basic/sys/bramfs/src/main.c @@ -300,6 +300,128 @@ static void test_BRAMFS_invalid_record_positions(void) bacfile_ramfs_deinit(); } +/** + * @brief Unit Test for consecutive record appends (regression test for buffer + * overrun) + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bramfs_tests, test_BRAMFS_consecutive_appends) +#else +static void test_BRAMFS_consecutive_appends(void) +#endif +{ + const char *pathname = "testfile_consecutive.txt"; + bool status = false; + char record_1[] = { "First appended record." }; + char record_2[] = { "Second appended record." }; + char read_buf[MAX_OCTET_STRING_BYTES] = { 0 }; + size_t record_len = 0; + + bacfile_ramfs_init(); + + /* Append first record with fileStartRecord = -1 */ + record_len = bacnet_strnlen(record_1, sizeof(record_1)); + status = bacfile_ramfs_write_record_data( + pathname, -1, 0, (const uint8_t *)record_1, record_len); + zassert_true(status, "First append should succeed"); + + /* Append second record consecutively with fileStartRecord = -1 */ + record_len = bacnet_strnlen(record_2, sizeof(record_2)); + status = bacfile_ramfs_write_record_data( + pathname, -1, 0, (const uint8_t *)record_2, record_len); + zassert_true( + status, + "Second consecutive append should succeed (buffer overrun bug)"); + + /* Verify both records readable */ + record_len = bacnet_strnlen(record_1, sizeof(record_1)); + status = bacfile_ramfs_read_record_data( + pathname, 0, 0, (uint8_t *)read_buf, record_len); + zassert_true(status, "Read first record should succeed"); + zassert_true( + memcmp(read_buf, record_1, record_len) == 0, "First record data match"); + + record_len = bacnet_strnlen(record_2, sizeof(record_2)); + status = bacfile_ramfs_read_record_data( + pathname, 0, 1, (uint8_t *)read_buf, record_len); + zassert_true(status, "Read second record should succeed"); + zassert_true( + memcmp(read_buf, record_2, record_len) == 0, + "Second record data match"); + + bacfile_ramfs_deinit(); +} + +/** + * @brief Unit Test for record replacement with shorter data (regression test + * for stale memmove length causing heap OOB read) + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bramfs_tests, test_BRAMFS_record_replace_shorter) +#else +static void test_BRAMFS_record_replace_shorter(void) +#endif +{ + const char *pathname = "testfile_replace.txt"; + bool status = false; + char record_1[] = { "Original long record that is quite lengthy." }; + char record_2[] = { "Keep." }; + char record_3[] = { "Third record after replacement." }; + char read_buf[MAX_OCTET_STRING_BYTES] = { 0 }; + size_t record_len = 0; + + bacfile_ramfs_init(); + + /* Write three records */ + record_len = bacnet_strnlen(record_1, sizeof(record_1)); + status = bacfile_ramfs_write_record_data( + pathname, 0, 0, (const uint8_t *)record_1, record_len); + zassert_true(status, "Write record 1 should succeed"); + + record_len = bacnet_strnlen(record_2, sizeof(record_2)); + status = bacfile_ramfs_write_record_data( + pathname, -1, 0, (const uint8_t *)record_2, record_len); + zassert_true(status, "Write record 2 should succeed"); + + record_len = bacnet_strnlen(record_3, sizeof(record_3)); + status = bacfile_ramfs_write_record_data( + pathname, -1, 0, (const uint8_t *)record_3, record_len); + zassert_true(status, "Write record 3 should succeed"); + + /* Replace first record with much shorter data; tests stale length in + * memmove */ + char short_record[] = { "X" }; + record_len = bacnet_strnlen(short_record, sizeof(short_record)); + status = bacfile_ramfs_write_record_data( + pathname, 0, 0, (const uint8_t *)short_record, record_len); + zassert_true(status, "Replace record 1 with shorter data should succeed"); + + /* Verify all records still readable and correct */ + record_len = bacnet_strnlen(short_record, sizeof(short_record)); + status = bacfile_ramfs_read_record_data( + pathname, 0, 0, (uint8_t *)read_buf, record_len); + zassert_true(status, "Read replaced record should succeed"); + zassert_true( + memcmp(read_buf, short_record, record_len) == 0, + "Replaced record data match"); + + record_len = bacnet_strnlen(record_2, sizeof(record_2)); + status = bacfile_ramfs_read_record_data( + pathname, 0, 1, (uint8_t *)read_buf, record_len); + zassert_true(status, "Read record 2 should succeed after replace"); + zassert_true( + memcmp(read_buf, record_2, record_len) == 0, "Record 2 data match"); + + record_len = bacnet_strnlen(record_3, sizeof(record_3)); + status = bacfile_ramfs_read_record_data( + pathname, 0, 2, (uint8_t *)read_buf, record_len); + zassert_true(status, "Read record 3 should succeed after replace"); + zassert_true( + memcmp(read_buf, record_3, record_len) == 0, "Record 3 data match"); + + bacfile_ramfs_deinit(); +} + /** * @} */ @@ -313,7 +435,9 @@ void test_main(void) bramfs_tests, ztest_unit_test(test_BRAMFS_stream), ztest_unit_test(test_BRAMFS_records), ztest_unit_test(test_BRAMFS_invalid_stream_positions), - ztest_unit_test(test_BRAMFS_invalid_record_positions)); + ztest_unit_test(test_BRAMFS_invalid_record_positions), + ztest_unit_test(test_BRAMFS_consecutive_appends), + ztest_unit_test(test_BRAMFS_record_replace_shorter)); ztest_run_test_suite(bramfs_tests); } From 15e004575c3f1cc0732e8dea3d5cb8fc37a22db5 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 17:06:54 -0500 Subject: [PATCH 35/42] security: backport #1410 with changelog/security and code patch --- CHANGELOG.md | 1 + SECURITY.md | 5 + src/bacnet/basic/object/lsz.c | 98 +++++++++-- test/bacnet/basic/object/lsz/src/main.c | 206 +++++++++++++++++++++++- 4 files changed, 293 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee00e81ad..a320aed89b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ The git repositories are hosted at the following sites: record replacement in AtomicWriteFile record-access handling. (#1408) * Secured the basic RAMFS to prevent buffer overrun during consecutive AtomicWriteFile record appends. (#1408) +* Secured Life Safety Zone member handling for write property. (#1410) * Secured rpm_decode_object_property by fixing a DoS vulnerability for malformed RPM requests. (#1374) * Secured bsc_node_parse_urls() by fixing buffer overflows by using relative diff --git a/SECURITY.md b/SECURITY.md index 79d3a532c2..e0a03856e4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -63,6 +63,11 @@ Consecutive AtomicWriteFile(record-access) appends can trigger a heap out-of-bou Patched versions: 1.5.1 Pull Request: [#1408](https://github.com/bacnet-stack/bacnet-stack/pull/1408). +Remote unauthenticated DoS in Life_Safety_Zone PROP_ZONE_MEMBERS WriteProperty parsing +[GHSA-2c8x-f46r-8phh](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-2c8x-f46r-8phh). +Patched versions: 1.5.1 +Pull Request: [#1410](https://github.com/bacnet-stack/bacnet-stack/pull/1410). + [CVE-2026-52789](https://www.cve.org/CVERecord?id=CVE-2026-52789) - Denial of Service (Infinite Loop) in handler_read_property_multiple via malformed RPM requests. [GHSA-4rf9-4vgq-5gcw](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4rf9-4vgq-5gcw). diff --git a/src/bacnet/basic/object/lsz.c b/src/bacnet/basic/object/lsz.c index d01fd5430e..bb3728a051 100644 --- a/src/bacnet/basic/object/lsz.c +++ b/src/bacnet/basic/object/lsz.c @@ -551,6 +551,7 @@ bool Life_Safety_Zone_Members_Add( const BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *data) { bool status = false; + int index = -1; BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *entry; struct object_data *pObject; @@ -563,12 +564,29 @@ bool Life_Safety_Zone_Members_Add( return false; } memcpy(entry, data, sizeof(BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE)); - status = Keylist_Data_Add( + index = Keylist_Data_Add( pObject->Zone_Members, Keylist_Count(pObject->Zone_Members), entry); + if (index >= 0) { + status = true; + } else { + free(entry); + } return status; } +/** + * @brief Free a list and all of its element data + * @param list - keylist to free + */ +static void Life_Safety_Zone_Members_List_Delete(OS_Keylist list) +{ + if (list) { + Keylist_Data_Free(list); + Keylist_Delete(list); + } +} + /** * @brief Remove all members from the Zone Members list * @param object_instance - object-instance number of the object @@ -594,26 +612,72 @@ static bool Life_Safety_Zone_Members_Write(BACNET_WRITE_PROPERTY_DATA *wp_data) int len = 0, apdu_len = 0, apdu_size = 0; const uint8_t *apdu = NULL; BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE data = { 0 }; + struct object_data *pObject; + OS_Keylist members = NULL; + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *entry = NULL; + int index = -1; if (wp_data == NULL) { return false; } - /* empty the list */ - Life_Safety_Zone_Members_Clear(wp_data->object_instance); + pObject = Keylist_Data(Object_List, wp_data->object_instance); + if (!pObject) { + wp_data->error_class = ERROR_CLASS_OBJECT; + wp_data->error_code = ERROR_CODE_UNKNOWN_OBJECT; + return false; + } + members = Keylist_Create(); + if (!members) { + wp_data->error_class = ERROR_CLASS_RESOURCES; + wp_data->error_code = ERROR_CODE_NO_SPACE_TO_WRITE_PROPERTY; + return false; + } apdu = wp_data->application_data; apdu_size = wp_data->application_data_len; - /* decode all packed */ + /* First pass: validate full payload and enforce forward progress. */ while (apdu_len < apdu_size) { len = bacnet_device_object_property_reference_decode( - apdu, apdu_size - apdu_len, &data); - if (len < 0) { + &apdu[apdu_len], apdu_size - apdu_len, NULL); + if (len <= 0) { wp_data->error_class = ERROR_CLASS_PROPERTY; wp_data->error_code = ERROR_CODE_INVALID_DATA_TYPE; + Life_Safety_Zone_Members_List_Delete(members); return false; } - Life_Safety_Zone_Members_Add(wp_data->object_instance, &data); apdu_len += len; } + /* Second pass: decode and stage members in temporary list. */ + apdu_len = 0; + while (apdu_len < apdu_size) { + len = bacnet_device_object_property_reference_decode( + &apdu[apdu_len], apdu_size - apdu_len, &data); + if (len <= 0) { + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_INVALID_DATA_TYPE; + Life_Safety_Zone_Members_List_Delete(members); + return false; + } + entry = calloc(1, sizeof(BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE)); + if (!entry) { + wp_data->error_class = ERROR_CLASS_RESOURCES; + wp_data->error_code = ERROR_CODE_NO_SPACE_TO_WRITE_PROPERTY; + Life_Safety_Zone_Members_List_Delete(members); + return false; + } + memcpy(entry, &data, sizeof(BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE)); + index = Keylist_Data_Add(members, Keylist_Count(members), entry); + if (index < 0) { + free(entry); + wp_data->error_class = ERROR_CLASS_RESOURCES; + wp_data->error_code = ERROR_CODE_NO_SPACE_TO_WRITE_PROPERTY; + Life_Safety_Zone_Members_List_Delete(members); + return false; + } + apdu_len += len; + } + /* Commit new list only after full payload validates and stages. */ + Life_Safety_Zone_Members_List_Delete(pObject->Zone_Members); + pObject->Zone_Members = members; return true; } @@ -793,15 +857,17 @@ bool Life_Safety_Zone_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) if (wp_data == NULL) { return false; } - /* decode the some of the request */ - len = bacapp_decode_application_data( - wp_data->application_data, wp_data->application_data_len, &value); - /* FIXME: len < application_data_len: more data? */ - if (len < 0) { - /* error while decoding - a value larger than we can handle */ - wp_data->error_class = ERROR_CLASS_PROPERTY; - wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; - return false; + if (wp_data->object_property != PROP_ZONE_MEMBERS) { + /* decode the some of the request */ + len = bacapp_decode_application_data( + wp_data->application_data, wp_data->application_data_len, &value); + /* FIXME: len < application_data_len: more data? */ + if (len < 0) { + /* error while decoding - a value larger than we can handle */ + wp_data->error_class = ERROR_CLASS_PROPERTY; + wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; + return false; + } } switch (wp_data->object_property) { case PROP_MODE: diff --git a/test/bacnet/basic/object/lsz/src/main.c b/test/bacnet/basic/object/lsz/src/main.c index ba78ee0971..812c70f57e 100644 --- a/test/bacnet/basic/object/lsz/src/main.c +++ b/test/bacnet/basic/object/lsz/src/main.c @@ -5,11 +5,84 @@ * @date 2024 * @copyright SPDX-License-Identifier: MIT */ +#include #include #include +#include #include #include +static int lsz_zone_members_count( + uint32_t object_instance, + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *first, + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE *second) +{ + BACNET_READ_PROPERTY_DATA rpdata = { 0 }; + uint8_t apdu[MAX_APDU] = { 0 }; + int apdu_len = 0; + int len = 0; + int count = 0; + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE member = { 0 }; + + rpdata.object_type = OBJECT_LIFE_SAFETY_ZONE; + rpdata.object_instance = object_instance; + rpdata.object_property = PROP_ZONE_MEMBERS; + rpdata.array_index = BACNET_ARRAY_ALL; + rpdata.application_data = &apdu[0]; + rpdata.application_data_len = sizeof(apdu); + + apdu_len = Life_Safety_Zone_Read_Property(&rpdata); + zassert_true(apdu_len >= 0, NULL); + while (len < apdu_len) { + int decoded_len = bacnet_device_object_property_reference_decode( + &apdu[len], apdu_len - len, &member); + zassert_true(decoded_len > 0, NULL); + len += decoded_len; + if ((count == 0) && first) { + *first = member; + } else if ((count == 1) && second) { + *second = member; + } + count++; + } + zassert_equal(len, apdu_len, NULL); + + return count; +} + +static bool lsz_zone_members_write( + uint32_t object_instance, + const uint8_t *payload, + int payload_len, + BACNET_ERROR_CLASS *error_class, + BACNET_ERROR_CODE *error_code) +{ + BACNET_WRITE_PROPERTY_DATA wp_data = { 0 }; + bool status = false; + + wp_data.object_type = OBJECT_LIFE_SAFETY_ZONE; + wp_data.object_instance = object_instance; + wp_data.object_property = PROP_ZONE_MEMBERS; + wp_data.array_index = BACNET_ARRAY_ALL; + wp_data.priority = BACNET_NO_PRIORITY; + wp_data.application_data_len = payload_len; + if (payload && (payload_len > 0) && + (payload_len <= sizeof(wp_data.application_data))) { + memcpy(wp_data.application_data, payload, payload_len); + } else { + return false; + } + status = Life_Safety_Zone_Write_Property(&wp_data); + if (error_class) { + *error_class = wp_data.error_class; + } + if (error_code) { + *error_code = wp_data.error_code; + } + + return status; +} + /** * @addtogroup bacnet_tests * @{ @@ -53,6 +126,135 @@ static void testLifeSafetyZone(void) /* cleanup */ status = Life_Safety_Zone_Delete(object_instance); } + +/** + * @brief Regression tests for zone-members WriteProperty decode handling + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(testsLifeSafetyZone, testLifeSafetyZoneZoneMembersWriteProperty) +#else +static void testLifeSafetyZoneZoneMembersWriteProperty(void) +#endif +{ + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE member = { 0 }; + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE member2 = { 0 }; + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE test_member = { 0 }; + BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE test_member2 = { 0 }; + BACNET_ERROR_CLASS error_class = ERROR_CLASS_DEVICE; + BACNET_ERROR_CODE error_code = ERROR_CODE_OTHER; + uint8_t apdu[MAX_APDU] = { 0 }; + int len = 0; + int len2 = 0; + uint32_t object_instance; + bool status; + + Life_Safety_Zone_Init(); + object_instance = Life_Safety_Zone_Create(BACNET_MAX_INSTANCE); + zassert_not_equal(object_instance, BACNET_MAX_INSTANCE, NULL); + + member.objectIdentifier.type = OBJECT_ANALOG_INPUT; + member.objectIdentifier.instance = 1; + member.propertyIdentifier = PROP_PRESENT_VALUE; + member.arrayIndex = BACNET_ARRAY_ALL; + member.deviceIdentifier.type = BACNET_NO_DEV_TYPE; + member.deviceIdentifier.instance = BACNET_NO_DEV_ID; + + member2.objectIdentifier.type = OBJECT_BINARY_INPUT; + member2.objectIdentifier.instance = 2; + member2.propertyIdentifier = PROP_PRESENT_VALUE; + member2.arrayIndex = BACNET_ARRAY_ALL; + member2.deviceIdentifier.type = BACNET_NO_DEV_TYPE; + member2.deviceIdentifier.instance = BACNET_NO_DEV_ID; + + len = bacapp_encode_device_obj_property_ref(&apdu[0], &member); + zassert_true(len > 0, NULL); + status = lsz_zone_members_write( + object_instance, &apdu[0], len, &error_class, &error_code); + zassert_true(status, NULL); + zassert_equal( + lsz_zone_members_count(object_instance, &test_member, NULL), 1, NULL); + zassert_true( + bacnet_device_object_property_reference_same(&test_member, &member), + NULL); + + /* zero-length decode (first tag mismatch): opening[3], null, closing[3] */ + apdu[0] = 0x3E; + apdu[1] = 0x00; + apdu[2] = 0x3F; + status = lsz_zone_members_write( + object_instance, &apdu[0], 3, &error_class, &error_code); + zassert_false(status, NULL); + zassert_equal(error_class, ERROR_CLASS_PROPERTY, NULL); + zassert_equal(error_code, ERROR_CODE_INVALID_DATA_TYPE, NULL); + zassert_equal( + lsz_zone_members_count(object_instance, &test_member, NULL), 1, NULL); + zassert_true( + bacnet_device_object_property_reference_same(&test_member, &member), + NULL); + + /* zero-length decode with non-empty payload and wrong first element tag */ + apdu[0] = 0x00; + status = lsz_zone_members_write( + object_instance, &apdu[0], 1, &error_class, &error_code); + zassert_false(status, NULL); + zassert_equal(error_class, ERROR_CLASS_PROPERTY, NULL); + zassert_equal(error_code, ERROR_CODE_INVALID_DATA_TYPE, NULL); + zassert_equal( + lsz_zone_members_count(object_instance, &test_member, NULL), 1, NULL); + zassert_true( + bacnet_device_object_property_reference_same(&test_member, &member), + NULL); + + /* negative decode: payload starts like ref, but truncated */ + len = bacapp_encode_device_obj_property_ref(&apdu[0], &member); + zassert_true(len > 1, NULL); + status = lsz_zone_members_write( + object_instance, &apdu[0], 1, &error_class, &error_code); + zassert_false(status, NULL); + zassert_equal(error_class, ERROR_CLASS_PROPERTY, NULL); + zassert_equal(error_code, ERROR_CODE_INVALID_DATA_TYPE, NULL); + zassert_equal( + lsz_zone_members_count(object_instance, &test_member, NULL), 1, NULL); + zassert_true( + bacnet_device_object_property_reference_same(&test_member, &member), + NULL); + + /* malformed tail after valid first element must not partially commit */ + len = bacapp_encode_device_obj_property_ref(&apdu[0], &member2); + zassert_true(len > 0, NULL); + apdu[len] = 0x00; + status = lsz_zone_members_write( + object_instance, &apdu[0], len + 1, &error_class, &error_code); + zassert_false(status, NULL); + zassert_equal(error_class, ERROR_CLASS_PROPERTY, NULL); + zassert_equal(error_code, ERROR_CODE_INVALID_DATA_TYPE, NULL); + zassert_equal( + lsz_zone_members_count(object_instance, &test_member, NULL), 1, NULL); + zassert_true( + bacnet_device_object_property_reference_same(&test_member, &member), + NULL); + + /* positive decode + multi-element list + cursor advancement */ + len = bacapp_encode_device_obj_property_ref(&apdu[0], &member); + zassert_true(len > 0, NULL); + len2 = bacapp_encode_device_obj_property_ref(&apdu[len], &member2); + zassert_true(len2 > 0, NULL); + status = lsz_zone_members_write( + object_instance, &apdu[0], len + len2, &error_class, &error_code); + zassert_true(status, NULL); + zassert_equal( + lsz_zone_members_count(object_instance, &test_member, &test_member2), 2, + NULL); + zassert_true( + bacnet_device_object_property_reference_same(&test_member, &member), + NULL); + zassert_true( + bacnet_device_object_property_reference_same(&test_member2, &member2), + NULL); + + status = Life_Safety_Zone_Delete(object_instance); + zassert_true(status, NULL); +} /** * @} */ @@ -62,7 +264,9 @@ ZTEST_SUITE(testsLifeSafetyZone, NULL, NULL, NULL, NULL, NULL); #else void test_main(void) { - ztest_test_suite(testsLifeSafetyZone, ztest_unit_test(testLifeSafetyZone)); + ztest_test_suite( + testsLifeSafetyZone, ztest_unit_test(testLifeSafetyZone), + ztest_unit_test(testLifeSafetyZoneZoneMembersWriteProperty)); ztest_run_test_suite(testsLifeSafetyZone); } From ec831a5195e9a24886f50b7cbb14aaac8d466cf3 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 17:08:54 -0500 Subject: [PATCH 36/42] security: backport #1411 with changelog/security and code patch --- CHANGELOG.md | 2 ++ SECURITY.md | 5 +++++ src/bacnet/basic/object/bacfile.c | 8 +++++--- src/bacnet/basic/sys/bramfs.c | 4 ++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a320aed89b..b82f7dd6fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ The git repositories are hosted at the following sites: * Secured the basic RAMFS to prevent buffer overrun during consecutive AtomicWriteFile record appends. (#1408) * Secured Life Safety Zone member handling for write property. (#1410) +* Secured RAMFS by checking read-only property for file size setting + and zero new memory during realloc to prevent data leaking. (#1411) * Secured rpm_decode_object_property by fixing a DoS vulnerability for malformed RPM requests. (#1374) * Secured bsc_node_parse_urls() by fixing buffer overflows by using relative diff --git a/SECURITY.md b/SECURITY.md index e0a03856e4..73f9f3b9d0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -68,6 +68,11 @@ Remote unauthenticated DoS in Life_Safety_Zone PROP_ZONE_MEMBERS WriteProperty p Patched versions: 1.5.1 Pull Request: [#1410](https://github.com/bacnet-stack/bacnet-stack/pull/1410). +WriteProperty(File_Size) can bypass read-only protection and expose uninitialized RAMFS tail bytes through AtomicReadFile +[GHSA-mwj7-2v5r-v934](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-mwj7-2v5r-v934). +Patched versions: 1.5.1 +Pull Request: [#1411](https://github.com/bacnet-stack/bacnet-stack/pull/1411). + [CVE-2026-52789](https://www.cve.org/CVERecord?id=CVE-2026-52789) - Denial of Service (Infinite Loop) in handler_read_property_multiple via malformed RPM requests. [GHSA-4rf9-4vgq-5gcw](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4rf9-4vgq-5gcw). diff --git a/src/bacnet/basic/object/bacfile.c b/src/bacnet/basic/object/bacfile.c index 47706f7b6d..bae1abab4b 100644 --- a/src/bacnet/basic/object/bacfile.c +++ b/src/bacnet/basic/object/bacfile.c @@ -649,9 +649,11 @@ bool bacfile_file_size_set( pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - if (pObject->File_Access_Stream) { - status = - bacfile_file_size_set_callback(pObject->Pathname, file_size); + if (!pObject->Read_Only) { + if (pObject->File_Access_Stream) { + status = bacfile_file_size_set_callback( + pObject->Pathname, file_size); + } } } diff --git a/src/bacnet/basic/sys/bramfs.c b/src/bacnet/basic/sys/bramfs.c index e01011a3b1..5a3865e88c 100644 --- a/src/bacnet/basic/sys/bramfs.c +++ b/src/bacnet/basic/sys/bramfs.c @@ -130,6 +130,10 @@ bool bacfile_ramfs_file_size_set(const char *pathname, size_t new_size) if (new_size > 0) { new_data = realloc(pFile->data, new_size); if (new_data) { + /* zero the new memory to avoid leaking sensitive data */ + if (new_size > pFile->size) { + memset(new_data + pFile->size, 0, new_size - pFile->size); + } pFile->data = new_data; pFile->size = new_size; status = true; From c20d61c1a347b75c866793de6c568d16fea22a1f Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 2 Jul 2026 17:10:52 -0500 Subject: [PATCH 37/42] security: backport #1412 with changelog/security and code patch --- CHANGELOG.md | 2 + SECURITY.md | 5 + src/bacnet/lighting.c | 174 +++++++++++++++++--------------- test/bacnet/lighting/src/main.c | 66 +++++++++++- 4 files changed, 164 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b82f7dd6fb..acd278c708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ The git repositories are hosted at the following sites: * Secured Life Safety Zone member handling for write property. (#1410) * Secured RAMFS by checking read-only property for file size setting and zero new memory during realloc to prevent data leaking. (#1411) +* Secured lighting_command_decode out-of-bounds read, and enforce the + FADE_TO and RAMP_TO required levels. (#1412) * Secured rpm_decode_object_property by fixing a DoS vulnerability for malformed RPM requests. (#1374) * Secured bsc_node_parse_urls() by fixing buffer overflows by using relative diff --git a/SECURITY.md b/SECURITY.md index 73f9f3b9d0..6723e69126 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -73,6 +73,11 @@ WriteProperty(File_Size) can bypass read-only protection and expose uninitialize Patched versions: 1.5.1 Pull Request: [#1411](https://github.com/bacnet-stack/bacnet-stack/pull/1411). +Out-of-bounds read in lighting_command_decode (full APDU size passed to nested tag decoders) +[GHSA-9hq3-w3pc-8385](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-9hq3-w3pc-8385). +Patched versions: 1.5.1 +Pull Request: [#1412](https://github.com/bacnet-stack/bacnet-stack/pull/1412). + [CVE-2026-52789](https://www.cve.org/CVERecord?id=CVE-2026-52789) - Denial of Service (Infinite Loop) in handler_read_property_multiple via malformed RPM requests. [GHSA-4rf9-4vgq-5gcw](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-4rf9-4vgq-5gcw). diff --git a/src/bacnet/lighting.c b/src/bacnet/lighting.c index 382730f802..1a0b7cde2f 100644 --- a/src/bacnet/lighting.c +++ b/src/bacnet/lighting.c @@ -192,129 +192,139 @@ int lighting_command_decode( case BACNET_LIGHTS_NONE: break; case BACNET_LIGHTS_FADE_TO: - if ((apdu_size - apdu_len) == 0) { - return BACNET_STATUS_REJECT; - } - /* target-level [1] REAL (0.0..100.0) OPTIONAL */ + /* target-level [1] REAL (0.0..100.0) */ len = bacnet_real_context_decode( - &apdu[apdu_len], apdu_size, 1, &real_value); + &apdu[apdu_len], apdu_size - apdu_len, 1, &real_value); if (len > 0) { apdu_len += len; if (data) { data->target_level = real_value; data->use_target_level = true; } + } else { + return BACNET_STATUS_ERROR; } - if ((apdu_size - apdu_len) > 0) { - /* Tag 4: fade-time - OPTIONAL */ - len = bacnet_unsigned_context_decode( - &apdu[apdu_len], apdu_size, 4, &unsigned_value); - if (len > 0) { - apdu_len += len; - if (data) { - data->fade_time = (uint32_t)unsigned_value; - data->use_fade_time = true; - } - } else { - return BACNET_STATUS_ERROR; + /* Tag 4: fade-time - OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 4, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->fade_time = (uint32_t)unsigned_value; + data->use_fade_time = true; } + } else if (len == 0) { + /* fade-time is optional, so do nothing */ + } else { + return BACNET_STATUS_ERROR; } - if ((apdu_size - apdu_len) > 0) { - /* priority [5] Unsigned (1..16) OPTIONAL */ - len = bacnet_unsigned_context_decode( - &apdu[apdu_len], apdu_size, 5, &unsigned_value); - if (len > 0) { - apdu_len += len; - if (data) { - data->priority = (uint8_t)unsigned_value; - data->use_priority = true; - } + /* priority [5] Unsigned (1..16) OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 5, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->priority = (uint8_t)unsigned_value; + data->use_priority = true; } + } else if (len == 0) { + /* priority is optional, so do nothing */ + } else { + return BACNET_STATUS_ERROR; } break; case BACNET_LIGHTS_RAMP_TO: - if ((apdu_size - apdu_len) == 0) { - return BACNET_STATUS_REJECT; - } - /* target-level [1] REAL (0.0..100.0) OPTIONAL */ + /* target-level [1] REAL (0.0..100.0) */ len = bacnet_real_context_decode( - &apdu[apdu_len], apdu_size, 1, &real_value); + &apdu[apdu_len], apdu_size - apdu_len, 1, &real_value); if (len > 0) { apdu_len += len; if (data) { data->target_level = real_value; data->use_target_level = true; } + } else { + return BACNET_STATUS_ERROR; } - if ((apdu_size - apdu_len) > 0) { - /* ramp-rate [2] REAL (0.1..100.0) OPTIONAL */ - len = bacnet_real_context_decode( - &apdu[apdu_len], apdu_size, 2, &real_value); - if (len > 0) { - apdu_len += len; - if (data) { - data->ramp_rate = real_value; - data->use_ramp_rate = true; - } + /* ramp-rate [2] REAL (0.1..100.0) OPTIONAL */ + len = bacnet_real_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 2, &real_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->ramp_rate = real_value; + data->use_ramp_rate = true; } + } else if (len == 0) { + /* ramp-rate is optional, so do nothing */ + } else { + return BACNET_STATUS_ERROR; } - if ((apdu_size - apdu_len) > 0) { - /* priority [5] Unsigned (1..16) OPTIONAL */ - len = bacnet_unsigned_context_decode( - &apdu[apdu_len], apdu_size, 5, &unsigned_value); - if (len > 0) { - apdu_len += len; - if (data) { - data->priority = (uint8_t)unsigned_value; - data->use_priority = true; - } + /* priority [5] Unsigned (1..16) OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 5, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->priority = (uint8_t)unsigned_value; + data->use_priority = true; } + } else if (len == 0) { + /* priority is optional, so do nothing */ + } else { + return BACNET_STATUS_ERROR; } break; case BACNET_LIGHTS_STEP_UP: case BACNET_LIGHTS_STEP_DOWN: case BACNET_LIGHTS_STEP_ON: case BACNET_LIGHTS_STEP_OFF: - if ((apdu_size - apdu_len) > 0) { - /* step-increment [3] REAL (0.1..100.0) OPTIONAL */ - len = bacnet_real_context_decode( - &apdu[apdu_len], apdu_size, 3, &real_value); - if (len > 0) { - apdu_len += len; - if (data) { - data->step_increment = real_value; - data->use_step_increment = true; - } + /* step-increment [3] REAL (0.1..100.0) OPTIONAL */ + len = bacnet_real_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 3, &real_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->step_increment = real_value; + data->use_step_increment = true; } + } else if (len == 0) { + /* step-increment is optional, so do nothing */ + } else { + return BACNET_STATUS_ERROR; } - if ((apdu_size - apdu_len) > 0) { - /* priority [5] Unsigned (1..16) OPTIONAL */ - len = bacnet_unsigned_context_decode( - &apdu[apdu_len], apdu_size, 5, &unsigned_value); - if (len > 0) { - apdu_len += len; - if (data) { - data->priority = (uint8_t)unsigned_value; - data->use_priority = true; - } + /* priority [5] Unsigned (1..16) OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 5, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->priority = (uint8_t)unsigned_value; + data->use_priority = true; } + } else if (len == 0) { + /* priority is optional, so do nothing */ + } else { + return BACNET_STATUS_ERROR; } break; case BACNET_LIGHTS_WARN: case BACNET_LIGHTS_WARN_OFF: case BACNET_LIGHTS_WARN_RELINQUISH: case BACNET_LIGHTS_STOP: - if ((apdu_size - apdu_len) > 0) { - /* priority [5] Unsigned (1..16) OPTIONAL */ - len = bacnet_unsigned_context_decode( - &apdu[apdu_len], apdu_size, 5, &unsigned_value); - if (len > 0) { - apdu_len += len; - if (data) { - data->priority = (uint8_t)unsigned_value; - data->use_priority = true; - } + /* priority [5] Unsigned (1..16) OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 5, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->priority = (uint8_t)unsigned_value; + data->use_priority = true; } + } else if (len == 0) { + /* priority is optional, so do nothing */ + } else { + return BACNET_STATUS_ERROR; } break; default: diff --git a/test/bacnet/lighting/src/main.c b/test/bacnet/lighting/src/main.c index 515b68c459..8b1cc9d829 100644 --- a/test/bacnet/lighting/src/main.c +++ b/test/bacnet/lighting/src/main.c @@ -8,6 +8,7 @@ * @brief test BACnet integer encode/decode APIs */ +#include #include #include #include @@ -264,6 +265,68 @@ static void testBACnetXYColor(void) } } +/** + * @brief Test bounds checking for lighting_command_decode + * Validates fix for CWE-125 out-of-bounds read. + * Old code passed apdu_size instead of (apdu_size - apdu_len) to nested + * decoders, causing them to read past buffer when operation field advanced the + * pointer. + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(lighting_tests, testBACnetLightingCommandBoundsCheck) +#else +static void testBACnetLightingCommandBoundsCheck(void) +#endif +{ + BACNET_LIGHTING_COMMAND test_cases[] = { + /* FADE_TO with optional fade_time and priority */ + { BACNET_LIGHTS_FADE_TO, true, false, false, true, true, 50.0, 0.0, 0.0, + 5000, 8 }, + /* RAMP_TO with optional ramp_rate and priority */ + { BACNET_LIGHTS_RAMP_TO, true, true, false, false, true, 25.0, 10.0, + 0.0, 0, 5 }, + /* STEP_UP with optional step_increment and priority */ + { BACNET_LIGHTS_STEP_UP, false, false, true, false, true, 0.0, 0.0, 5.0, + 0, 12 }, + /* WARN with optional priority */ + { BACNET_LIGHTS_WARN, false, false, false, false, true, 0.0, 0.0, 0.0, + 0, 1 }, + }; + unsigned i, trunc_pos; + int enc_len; + uint8_t apdu_full[MAX_APDU] = { 0 }; + uint8_t apdu_trunc[MAX_APDU] = { 0 }; + int decode_result; + BACNET_LIGHTING_COMMAND decoded = { 0 }; + + for (i = 0; i < ARRAY_SIZE(test_cases); i++) { + /* Encode full valid message */ + enc_len = lighting_command_encode(apdu_full, &test_cases[i]); + zassert_true( + enc_len > 0, "Failed encode operation %u", test_cases[i].operation); + + /* Copy full buffer */ + memcpy(apdu_trunc, apdu_full, (size_t)enc_len); + + /* Test: Progressively truncate buffer, should return error */ + for (trunc_pos = 1; trunc_pos < enc_len; trunc_pos++) { + decode_result = + lighting_command_decode(apdu_trunc, trunc_pos, &decoded); + /* Decoder must not report consuming more bytes than provided */ + zassert_true( + decode_result <= 0 || decode_result <= (int)trunc_pos, + "Op %u: truncated to %u bytes returned decode_result %d", + test_cases[i].operation, trunc_pos, decode_result); + } + + /* Valid decode with full buffer */ + decode_result = lighting_command_decode(apdu_full, enc_len, &decoded); + zassert_equal( + decode_result, enc_len, "Op %u: full buffer decode failed", + test_cases[i].operation); + } +} + #if defined(CONFIG_ZTEST_NEW_API) ZTEST_SUITE(lighting_tests, NULL, NULL, NULL, NULL, NULL); #else @@ -272,7 +335,8 @@ void test_main(void) ztest_test_suite( lighting_tests, ztest_unit_test(testBACnetLightingCommandAll), ztest_unit_test(testBACnetColorCommandAll), - ztest_unit_test(testBACnetXYColor)); + ztest_unit_test(testBACnetXYColor), + ztest_unit_test(testBACnetLightingCommandBoundsCheck)); ztest_run_test_suite(lighting_tests); } From 390ec05fd6266942681fc706c220d0c0e1d8d6fb Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Sat, 4 Jul 2026 20:32:35 -0500 Subject: [PATCH 38/42] security: update branch filters to allow all branches for CI workflows --- .github/workflows/bsc-tests-linux.yml | 4 ++-- .github/workflows/gcc.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/main.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/bsc-tests-linux.yml b/.github/workflows/bsc-tests-linux.yml index 2d6fc4cb0a..f51a459b18 100644 --- a/.github/workflows/bsc-tests-linux.yml +++ b/.github/workflows/bsc-tests-linux.yml @@ -3,7 +3,7 @@ name: BACnet/SC linux tests on: push: branches: - - master + - '*' pull_request: branches: - '*' @@ -89,4 +89,4 @@ jobs: cd build cmake .. make - ./test_bsc-datalink \ No newline at end of file + ./test_bsc-datalink diff --git a/.github/workflows/gcc.yml b/.github/workflows/gcc.yml index 91e6643b30..33a16cdcd9 100644 --- a/.github/workflows/gcc.yml +++ b/.github/workflows/gcc.yml @@ -3,7 +3,7 @@ name: GCC on: push: branches: - - master + - '*' pull_request: branches: - '*' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 729a960bbb..746de13214 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,7 +3,7 @@ name: Quality on: push: branches: - - master + - '*' pull_request: branches: - '*' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e5dcf22819..8eccf51997 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ name: CMake on: push: branches: - - master + - '*' pull_request: branches: - '*' From d5c11f77366526cc660d81f222c415d1566f228c Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Sat, 4 Jul 2026 20:32:51 -0500 Subject: [PATCH 39/42] fix pre-commit style --- src/bacnet/basic/service/h_arf.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bacnet/basic/service/h_arf.c b/src/bacnet/basic/service/h_arf.c index 69d98c76cf..d8d4c1f402 100644 --- a/src/bacnet/basic/service/h_arf.c +++ b/src/bacnet/basic/service/h_arf.c @@ -147,7 +147,8 @@ void handler_atomic_read_file( (int)data.type.stream.fileStartPosition, (int)data.type.stream.requestedOctetCount); len = arf_ack_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, &data); + &Handler_Transmit_Buffer[pdu_len], + service_data->invoke_id, &data); pdu_len += len; } else { error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; @@ -185,7 +186,8 @@ void handler_atomic_read_file( (int)data.type.record.fileStartRecord, (unsigned)data.type.record.RecordCount); len = arf_ack_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, &data); + &Handler_Transmit_Buffer[pdu_len], + service_data->invoke_id, &data); pdu_len += len; } else { DEBUG_PRINTF("ARF: file_access_denied! Sending Error!"); From 3a74c74a8139526e34da32c6cc41ed7c71aaded5 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Sat, 4 Jul 2026 20:41:58 -0500 Subject: [PATCH 40/42] release: update version to 1.5.1 and modify changelog --- CHANGELOG.md | 2 +- release.sh | 7 +++---- src/bacnet/version.h | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd278c708..83efbee75d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ The git repositories are hosted at the following sites: * * -## [1.5.1-rc4] - 2026-07-02 +## [1.5.1] - 2026-07-04 ### Security diff --git a/release.sh b/release.sh index d4ff61db98..4949f452b9 100755 --- a/release.sh +++ b/release.sh @@ -6,11 +6,10 @@ # # Prior to running this script, be sure to: # a) update CHANGELOG, version.h and CMakeLists.txt with new version number -# b) commit changes into master branch +# b) commit those changes into this long term branch bacnet-stack-1.5 # After running this script, be sure to: -# c) create long term branch as bacnet-stack-x.y if needed -# d) push tags and branch to github -# e) mirror github to sourceforge using mirror.sh script +# c) push tags and branch to github +# d) mirror github to sourceforge using mirror.sh USERNAME='skarg' diff --git a/src/bacnet/version.h b/src/bacnet/version.h index 9755916d95..ca51888a99 100644 --- a/src/bacnet/version.h +++ b/src/bacnet/version.h @@ -15,7 +15,7 @@ #define BACNET_VERSION(x, y, z) (((x) << 16) + ((y) << 8) + (z)) #endif -#define BACNET_VERSION_TEXT "1.5.1-rc3" +#define BACNET_VERSION_TEXT "1.5.1" #define BACNET_VERSION_CODE BACNET_VERSION(1, 5, 1) #define BACNET_VERSION_MAJOR ((BACNET_VERSION_CODE >> 16) & 0xFF) #define BACNET_VERSION_MINOR ((BACNET_VERSION_CODE >> 8) & 0xFF) From 81fef566e8295893ea73c85c436f4ada3d3f5ebc Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 16 Jul 2026 09:13:10 -0500 Subject: [PATCH 41/42] release: update version to 1.5.2 and modify changelog --- CHANGELOG.md | 8 +++++++- CMakeLists.txt | 2 +- src/bacnet/version.h | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83efbee75d..0cee6b4163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ The git repositories are hosted at the following sites: * * +## [1.5.2] - Unreleased + +### Security + +### Fixed + ## [1.5.1] - 2026-07-04 ### Security @@ -26,7 +32,7 @@ The git repositories are hosted at the following sites: allocated vs non-allocated strings. (#1375) * Secured xy_color_decode() by adjusting apdu_size calculation to prevent out-of-bounds read, and secured network control handler offset calculation - in router applications to prevent buffer overrun. (#1386, #1387) + in router applications to prevent buffer overrun. (#1386) (#1387) * Secured apps/router-mstp and apps/router-ipv6 routing by introducing routed_npdu_apdu_encode() with explicit oversized-PDU drop checks. (#1392) * Secured rpm_ack_decode_service_request buffer overflow by validating data diff --git a/CMakeLists.txt b/CMakeLists.txt index bcaef5b2d4..d783beb9af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project( bacnet-stack - VERSION 1.5.1 + VERSION 1.5.2 LANGUAGES C) # diff --git a/src/bacnet/version.h b/src/bacnet/version.h index ca51888a99..734103d3d8 100644 --- a/src/bacnet/version.h +++ b/src/bacnet/version.h @@ -15,8 +15,8 @@ #define BACNET_VERSION(x, y, z) (((x) << 16) + ((y) << 8) + (z)) #endif -#define BACNET_VERSION_TEXT "1.5.1" -#define BACNET_VERSION_CODE BACNET_VERSION(1, 5, 1) +#define BACNET_VERSION_TEXT "1.5.2-rc1" +#define BACNET_VERSION_CODE BACNET_VERSION(1, 5, 2) #define BACNET_VERSION_MAJOR ((BACNET_VERSION_CODE >> 16) & 0xFF) #define BACNET_VERSION_MINOR ((BACNET_VERSION_CODE >> 8) & 0xFF) #define BACNET_VERSION_MAINTENANCE (BACNET_VERSION_CODE & 0xFF) From ac0a40962130151d6c9f318d599bf39123b92100 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 16 Jul 2026 09:15:19 -0500 Subject: [PATCH 42/42] fix: resolve buffer overflow in COBS frame decoding and add unit test for tight buffer handling (#1425) --- CHANGELOG.md | 3 ++ SECURITY.md | 5 +++ src/bacnet/datalink/mstp.c | 4 +- test/bacnet/datalink/mstp/src/main.c | 61 ++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cee6b4163..4dbf58bd3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ The git repositories are hosted at the following sites: ### Security +* Secured an MS/TP implementation COBS frame decoding buffer overflow, + and added unit test for tight buffer handling. (#1425) + ### Fixed ## [1.5.1] - 2026-07-04 diff --git a/SECURITY.md b/SECURITY.md index 6723e69126..a4df1ca15d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -25,6 +25,11 @@ or [GHSA](https://github.com/bacnet-stack/bacnet-stack/security/advisories?state and a record is created to identify, define, and catalog publicly disclosed cybersecurity vulnerabilities. +MS/TP COBS decode overflow +[GHSA-8456-m9x4-j6mc](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-8456-m9x4-j6mc). +Patched versions: 1.4.6, 1.5.2, 1.6.1, 1.7.0 +Pull Request: [#1425](https://github.com/bacnet-stack/bacnet-stack/pull/1425). + [CVE-2026-52790](https://www.cve.org/CVERecord?id=CVE-2026-52790) - bacnet_device.c stack-use-after-return in writable Device string properties [GHSA-jr7p-rm2x-739x](https://github.com/bacnet-stack/bacnet-stack/security/advisories/GHSA-jr7p-rm2x-739x). diff --git a/src/bacnet/datalink/mstp.c b/src/bacnet/datalink/mstp.c index b83cda2215..51b705883d 100644 --- a/src/bacnet/datalink/mstp.c +++ b/src/bacnet/datalink/mstp.c @@ -561,8 +561,8 @@ void MSTP_Receive_Frame_FSM(struct mstp_port_struct_t *mstp_port) (mstp_port->FrameType <= Nmax_COBS_type)) { mstp_port->DataLength = cobs_frame_decode( &mstp_port->InputBuffer[mstp_port->Index + 1], - mstp_port->InputBufferSize, mstp_port->InputBuffer, - mstp_port->Index + 1); + mstp_port->InputBufferSize - (mstp_port->Index + 1), + mstp_port->InputBuffer, mstp_port->Index + 1); if (mstp_port->DataLength > 0) { /* GoodCRC */ if (mstp_port->receive_state == diff --git a/test/bacnet/datalink/mstp/src/main.c b/test/bacnet/datalink/mstp/src/main.c index 362fa00123..a61cb5267b 100644 --- a/test/bacnet/datalink/mstp/src/main.c +++ b/test/bacnet/datalink/mstp/src/main.c @@ -646,6 +646,66 @@ static void testMasterNodeFSM(void) /* FIXME: write a unit test for the Master Node State Machine */ } +static void testReceiveNodeFSM_COBS_Decode_TightBuffer(void) +{ + struct mstp_port_struct_t mstp_port = { 0 }; /* port data */ + uint8_t my_mac = 0x05; /* local MAC address */ + uint8_t frame[MAX_MPDU] = { 0 }; + uint8_t rx_tight[MAX_MPDU] = { 0 }; + uint8_t payload[64] = { 0 }; + unsigned len; + unsigned cobs_len; + unsigned tight_size; + unsigned guard_start; + unsigned i; + + for (i = 0; i < sizeof(payload); i++) { + payload[i] = (uint8_t)(i + 1); + } + /* Include zeros to force multiple COBS blocks and real decode writes. */ + payload[3] = 0; + payload[17] = 0; + + len = MSTP_Create_Frame( + frame, sizeof(frame), FRAME_TYPE_BACNET_EXTENDED_DATA_EXPECTING_REPLY, + my_mac, my_mac, payload, sizeof(payload)); + zassert_true(len > 0, NULL); + + cobs_len = (((unsigned)frame[5]) << 8) | frame[6]; + cobs_len += 2; + tight_size = cobs_len + 1; + zassert_true(tight_size < sizeof(rx_tight), NULL); + + mstp_port.InputBuffer = &rx_tight[0]; + mstp_port.InputBufferSize = tight_size; + mstp_port.OutputBuffer = &TxBuffer[0]; + mstp_port.OutputBufferSize = sizeof(TxBuffer); + mstp_port.SilenceTimer = Timer_Silence; + mstp_port.SilenceTimerReset = Timer_Silence_Reset; + mstp_port.This_Station = my_mac; + mstp_port.Nmax_info_frames = 1; + mstp_port.Nmax_master = 127; + MSTP_Init(&mstp_port); + + guard_start = tight_size; + for (i = guard_start; i < (guard_start + 8); i++) { + rx_tight[i] = 0xA5; + } + + Load_Input_Buffer(frame, len); + for (i = 0; i < len; i++) { + RS485_Check_UART_Data(&mstp_port); + MSTP_Receive_Frame_FSM(&mstp_port); + } + + zassert_true(mstp_port.ReceivedInvalidFrame == true, NULL); + zassert_true(mstp_port.ReceivedValidFrame == false, NULL); + zassert_true(mstp_port.receive_state == MSTP_RECEIVE_STATE_IDLE, NULL); + for (i = guard_start; i < (guard_start + 8); i++) { + zassert_true(rx_tight[i] == 0xA5, NULL); + } +} + static void testSlaveNodeFSM(void) { struct mstp_port_struct_t MSTP_Port = { 0 }; /* port data */ @@ -1367,6 +1427,7 @@ void test_main(void) { ztest_test_suite( crc_tests, ztest_unit_test(testReceiveNodeFSM), + ztest_unit_test(testReceiveNodeFSM_COBS_Decode_TightBuffer), ztest_unit_test(testMasterNodeFSM), ztest_unit_test(testSlaveNodeFSM), ztest_unit_test(testZeroConfigNodeFSM), ztest_unit_test(testAutoBaudNodeFSM));