Skip to content

`WriteProperty(File_Size)` can bypass read-only protection and expose uninitialized RAMFS tail bytes through `AtomicReadFile`

Moderate
skarg published GHSA-mwj7-2v5r-v934 Jul 16, 2026

Package

bacnet-stack

Affected versions

1.5.0

Patched versions

1.5.1, 1.6.0

Description

Summary

Inbacnet-stack 1.5.0 (master branch commit 631bb8b), the BACnet FILE object implementation allows remote WriteProperty(PROP_FILE_SIZE) requests to change the size of a stream-access FILE object without checking whether that FILE is marked Read_Only. When the FILE backend is RAMFS, enlarging the file only performs realloc() and updates the stored size; the newly exposed tail region is not initialized before later reads.

As a result, a remote client can enlarge a read-only RAMFS-backed stream-access FILE and then immediately retrieve the newly exposed region with AtomicReadFile(stream-access). Depending on the allocator and build configuration, the returned data may contain sanitizer fill bytes, stale heap contents, or other unintended process memory. Shrinking the file through the same path can also remotely truncate content that was intended to be read-only.

Details

Affected code is located in:

  • src/bacnet/basic/object/bacfile.c
  • src/bacnet/basic/sys/bramfs.c
  • src/bacnet/basic/service/h_arf.c

Relevant logic:

  • bacfile_write_property() treats PROP_FILE_SIZE as writable and forwards valid unsigned values to bacfile_file_size_set().
  • bacfile_file_size_set() only checks whether File_Access_Stream is enabled:
if (pObject->File_Access_Stream) {
    status = bacfile_file_size_set_callback(pObject->Pathname, file_size);
}
  • It does not check bacfile_read_only(), unlike the normal AtomicWriteFile paths.
  • For a RAMFS-backed FILE, the callback reaches:
bacfile_write_property()
  -> bacfile_file_size_set()
    -> bacfile_ramfs_file_size_set()
  • In bacfile_ramfs_file_size_set(), enlargement is implemented as:
new_data = realloc(pFile->data, new_size);
if (new_data) {
    pFile->data = new_data;
    pFile->size = new_size;
    status = true;
}
  • No zero-fill or explicit initialization is applied to the newly added tail bytes.
  • A later AtomicReadFile(stream-access) request reaches:
handler_atomic_read_file()
  -> bacfile_read_stream_data()
    -> bacfile_ramfs_read_stream_data()
      -> memcpy(fileData, pFile->data + fileStartPosition, len)
  • This causes the expanded tail region to be returned as ordinary FILE content.

PoC

A client PoC is provided.
poc.c

The PoC targets OBJECT_FILE, instance 1, on the target server and performs the vulnerable sequence below:

The vulnerable sequence is:

  • ReadProperty(OBJECT_FILE, 1, PROP_FILE_SIZE)
  • WriteProperty(OBJECT_FILE, 1, PROP_FILE_SIZE = old_size + grow_by)
  • AtomicReadFile(OBJECT_FILE, 1, FILE_STREAM_ACCESS, fileStartPosition = old_size, requestedOctetCount = grow_by)

Reproduction steps

1. Build the server and poc client

cmake -S /home/user/bacnet-stack \
  -B /home/user/bacnet-stack/build \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_C_COMPILER=gcc

cmake --build /home/user/bacnet-stack/build --target server-ramfs poc -j"$(nproc)"

The reproduction below uses a target named server-ramfs. This is not an upstream example binary as-is; it is a variant derived from apps/server by switching the backup/restore FILE backend from the default POSIX path to the RAMFS-backed path, then initializing that FILE as stream-access plus read-only.

The relevant source diff against apps/server/main.c is:

--- apps/server/main.c
+++ apps/server-ramfs/main.c
@@ -51,7 +51,7 @@
-#include "bacfile-posix.h"
+#include "bacnet/basic/sys/bramfs.h"
@@ -158,6 +158,24 @@
+#if defined BACNET_BACKUP_RESTORE
+static void Server_RAMFS_Backup_File_Init(uint32_t instance)
+{
+    const char *pathname = "backup_1.bin";
+    static const uint8_t blob[] = "AAAAAAAAAAAA\0BBBBBBBBBBBB\0CCCCCCCCCCCC\0";
+    bacfile_pathname_set(instance, pathname);
+    bacfile_file_access_stream_set(instance, true);
+    bacfile_read_only_set(instance, true);
+    (void)bacfile_ramfs_write_stream_data(pathname, 0, blob, sizeof(blob));
+}
+#endif
@@ -182,12 +200,12 @@
-    bacfile_posix_init();
+    bacfile_ramfs_init();
...
-        bacfile_pathname_set(object_data.object_instance, "backup_1.bin");
+        Server_RAMFS_Backup_File_Init(object_data.object_instance);

2. Start the server

env \
  BACNET_DATALINK=bip \
  BACNET_IFACE=lo \
  BACNET_IP_PORT=47809 \
  /home/user/bacnet-stack/build/server-ramfs 260001

3. Run the poc

env \
  BACNET_DATALINK=bip \
  BACNET_IFACE=lo \
  BACNET_IP_PORT=47810 \
  BACNET_IP_BROADCAST_PORT=47809 \
  /home/user/bacnet-stack/build/poc 260001 1 64

4. Rebuild with MSAN and rerun the vulnerable sequence

Because this issue is primarily an uninitialized-memory exposure, MSAN is the stronger diagnostic build for confirming that the returned bytes were never initialized before being sent.

cmake -S /home/user/bacnet-stack \
  -B /home/user/bacnet-stack/build-msan \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_C_COMPILER=clang \
  -DCMAKE_C_FLAGS='-fsanitize=memory -fno-omit-frame-pointer -fno-common' \
  -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=memory'

cmake --build /home/user/bacnet-stack/build-msan --target server-ramfs poc -j"$(nproc)"
env \
  BACNET_DATALINK=bip \
  BACNET_IFACE=lo \
  BACNET_IP_PORT=47809 \
  /home/user/bacnet-stack/build-msan/server-ramfs 260001
env \
  BACNET_DATALINK=bip \
  BACNET_IFACE=lo \
  BACNET_IP_PORT=47810 \
  BACNET_IP_BROADCAST_PORT=47809 \
  /home/user/bacnet-stack/build-msan/poc 260001 1 64

Observed results

Vulnerable sequence without sanitizers

In a plain Debug build, the same vulnerable sequence first expands the read-only FILE from 40 to 104 bytes and then reads the new tail region starting at the old EOF. The request succeeds and returns non-file bytes that look like stale heap words rather than legitimate file contents:

[+] bound target device 260001 file 1
[*] sent ReadProperty(File_Size)
[+] old File_Size = 40
[*] sent WriteProperty(File_Size=104)
[+] WriteProperty(File_Size) acknowledged on target FILE
[*] sent AtomicReadFile(stream, start=40, count=64)
[+] AtomicReadFile returned 64 byte(s), eof=false
  0000 : e3 49 52 90 ff ff ff ff e4 5e 2d 90 ff ff ff ff  .IR......^-.....
  0010 : e5 29 34 90 ff ff ff ff e6 47 4a 10 ff ff ff ff  .)4......GJ.....
  0020 : e7 12 51 10 ff ff ff ff e8 27 2c 10 ff ff ff ff  ..Q......',.....
  0030 : e8 f2 33 10 ff ff ff ff ea 07 0e 10 ff ff ff ff  ..3.............

This output shows that:

  • WriteProperty(PROP_FILE_SIZE) succeeded even though the FILE was configured read-only
  • AtomicReadFile(stream-access) then returned the newly exposed tail region
  • the returned bytes are not part of the original initialized file contents

The tail region contains pointer-like stale heap data rather than the initialized A/B/C file payload, which is strong evidence of real memory disclosure rather than merely a sanitizer fill pattern.

Vulnerable sequence under MSAN

When the server is rebuilt with MSAN, the server reports that uninitialized bytes are being sent while constructing the AtomicReadFile response:

Uninitialized bytes in __interceptor_sendto at offset 15 inside [0x7fffcbafca70, 80)
==206162==WARNING: MemorySanitizer: use-of-uninitialized-value
    #0 0x701552 in bip_send_mpdu /home/user/bacnet-stack/ports/linux/bip-init.c:362:12
    #1 0x7f0f21 in bvlc_send_pdu /home/user/bacnet-stack/src/bacnet/basic/bbmd/h_bbmd.c:642:12
    #2 0x703071 in bip_send_pdu /home/user/bacnet-stack/ports/linux/bip-init.c:500:12
    #3 0x66b52a in datalink_send_pdu /home/user/bacnet-stack/src/bacnet/datalink/datalink.c:167:21
    #4 0x5e4c7e in handler_atomic_read_file /home/user/bacnet-stack/src/bacnet/basic/service/h_arf.c:259:22
    #5 0x5e15e8 in apdu_handler /home/user/bacnet-stack/src/bacnet/basic/service/h_apdu.c:609:17
    #6 0x4aaf98 in npdu_handler /home/user/bacnet-stack/src/bacnet/basic/npdu/h_npdu.c:279:21
    #7 0x496166 in main /home/user/bacnet-stack/apps/server-ramfs/main.c:447:13
    #8 0x7f789c08c082 in __libc_start_main /build/glibc-B3wQXB/glibc-2.31/csu/../csu/libc-start.c:308:16
    #9 0x41c3cd in _start (/home/user/bacnet-stack/build-msan/server-ramfs+0x41c3cd)

SUMMARY: MemorySanitizer: use-of-uninitialized-value /home/user/bacnet-stack/ports/linux/bip-init.c:362:12 in bip_send_mpdu
Exiting

Although the first observable use occurs in sendto(), the data flow is consistent with the expanded RAMFS tail being copied into the AtomicReadFile reply and only then being caught by MSAN when that reply is sent.

Impact

This is a remote confidentiality and integrity issue on the server side.

  • Confidentiality: a remote client can recover bytes from the newly expanded RAMFS tail even though those bytes were never initialized as legitimate file contents
  • Integrity: a remote client can modify the length of a FILE that was intended to be read-only, including truncating it to zero or extending it arbitrarily

The issue is reachable through ordinary BACnet WriteProperty and AtomicReadFile traffic against a stream-access FILE object backed by RAMFS. It can leak stale heap contents or silently corrupt the effective contents of a read-only file object.

Suggested fix

  • Reject WriteProperty(PROP_FILE_SIZE) when bacfile_read_only(object_instance) is true
  • Zero-initialize any newly allocated tail region in bacfile_ramfs_file_size_set() before exposing it through reads

Remediation

Remediated within PR #1411 and includes the suggested fixes.

Severity

Moderate

CVE ID

CVE-2026-62977

Weaknesses

Improper Access Control

The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor. Learn more on MITRE.

Use of Uninitialized Resource

The product uses or accesses a resource that has not been initialized. Learn more on MITRE.

Credits