Bali, Indonesia
Insights

BACnet Broadcast Floods and the eMMC Writeback Stall: Diagnosing BAS Gateway Lockups Under Attack

Invalid Date
0:00
When field reports from mechanical contractors and smart building analysts—such as the recent coverage in HVAC Cybersecurity: Protecting Smart Building Systems—warn about denial-of-service vectors targeting building automation systems (BAS), software engineers often picture classic volumetric network saturation. We envision upstream pipes filling up or packet processors dropping buffers under raw ingress pressure. But on modern industrial IoT gateways running embedded Linux, the real failure mode is far more insidious: it is an I/O priority inversion driven by the Linux virtual memory subsystem. During a recent operational review of dual-homed building control edge units (quad-core ARM Cortex-A53 nodes with 2 GB LPDDR4 and 8 GB soldered eMMC storage), we chased down recurrent, non-deterministic gateway reboots across a multi-tenant commercial campus. Every few days, the field controllers managing air handling units (AHUs) and variable air volume (VAV) boxes would drop offline simultaneously. The hardware watchdog (/dev/watchdog) had tripped, triggering an unceremonious hard reset. Upstream network monitors showed no link saturation, and CPU utilization hovered at a modest 35%. What had actually collapsed was the boundary between network packet ingress, page cache dirty throttling, and sluggish flash storage.
BACnet/IP (ANSI/ASHRAE Standard 135) relies heavily on UDP broadcast messages sent over port 47808 for dynamic device discovery and object resolution. A single unmanaged switch loop, a misconfigured BACnet Broadcast Management Device (BBMD), or a script kiddie armed with a rudimentary UDP flooder on the facilities VLAN can generate tens of thousands of Who-Is and I-Am frames per second. Because BACnet historically lacked cryptographic integrity, modern edge controllers implement aggressive application-level audit logging. Every unmapped frame, mismatched network number, or unexpected device identifier is parsed, validated, and appended to an on-disk audit store—frequently a SQLite database or a structured JSON log directory located on an ext4 root partition. At 15,000 packets per second, an edge daemon trying to record every rogue discovery attempt produces approximately 12 MB/s of unstructured logging data. While 12 MB/s sounds trivial for enterprise NVMe drives, it is catastrophic for embedded eMMC storage operating under write amplification.
Linux handles file I/O through the page cache. By default, processes write to memory pages marked 'dirty', and the kernel asynchronously flushes them to block storage via background bdi-writeback threads (kworker). Two core sysctl parameters govern this threshold: vm.dirty_background_ratio (often default at 10%) and vm.dirty_ratio (often default at 20%). On an edge node with 2 GB of RAM, dirty memory can accumulate up to ~400 MB before throttling kicks in.
INI
# Default kernel behavior exposes flash-backed edge controllers to latency spikes
vm.dirty_background_ratio = 10
vm.dirty_ratio = 20
vm.dirty_expire_centisecs = 3000
When a BACnet broadcast storm hits, the audit logger writes rapidly to the page cache. The slow eMMC storage controller—often capable of sustaining no more than 4 to 8 MB/s of sustained random writes due to background garbage collection and low-cost flash controllers—cannot keep pace with the influx. In under forty seconds, the dirty pages breach vm.dirty_background_ratio and rapidly climb toward vm.dirty_ratio. The moment dirty memory hits dirty_ratio, the Linux kernel stops treating writeback as an asynchronous background task. To prevent out-of-memory panics, the kernel forces the calling process (the gateway daemon itself) to execute balance_dirty_pages. The process enters uninterruptible sleep (TASK_UNINTERRUPTIBLE, visible as the dreaded D state in top), synchronously flushing blocks to flash before it is allowed to process another instruction.
In many proprietary BAS gateway implementations, architectural convenience wins over fault isolation. The developer writes a single-threaded asynchronous event loop (using libuv or standard Linux epoll) to handle BACnet socket reads, Modbus polling over RS-485, audit serialization, and hardware watchdog keep-alives. When the audit logger triggers synchronous disk writeback on the blocked eMMC, the entire event loop grinds to a halt. While the CPU core is technically idle waiting on flash I/O completion (manifesting as 95%+ iowait), no epoll callbacks can fire. Modbus polling timers lapse, BACnet UDP receive buffers drop packets silently, and critically, the application never circles back to write a single byte to /dev/watchdog. After 30 seconds of block-layer serialization latency, the external hardware watchdog circuit does what it was designed to do: pulls the processor reset line low. The gateway reboots, mounts the ext4 partition with an orphaned journal, replays the dirty blocks, and crashes again minutes later as the network storm resumes.
TEXT
+-------------------------------------------------------------+
|                   BACnet/IP Broadcast Storm                 |
|               (UDP 47808 Flood: Who-Is / I-Am)              |
+-------------------------------------------------------------+
                               | (Ingress > 15k pkts/sec)
                               v
+-------------------------------------------------------------+
|            Edge Gateway Process (Single Event Loop)         |
|  1. Parse Packet -> 2. Sync Audit Append -> 3. Pet Watchdog |
+-------------------------------------------------------------+
                               |
            Page Cache Fills > vm.dirty_ratio
                               v
+-------------------------------------------------------------+
|          Kernel Enforces Synchronous Writeback (D-State)    |
|   * Event loop blocked * Audit writes stall * epoll starved |
+-------------------------------------------------------------+
                               |
                 eMMC Flash Write Latency > 30s
                               v
+-------------------------------------------------------------+
|               Hardware Watchdog Timer Expires               |
|             ===> GATEWAY HARD KERNEL RESET <===             |
+-------------------------------------------------------------+
Remediating this class of failure requires both kernel-level memory tuning and structural application refactoring. You cannot fix a hardware I/O bottleneck with application logging alone, nor can you fix a flawed concurrency model solely through sysctl flags. First, change the Linux dirty page semantics from loose percentages to rigid absolute byte limits. Percentages are disastrous on low-memory embedded units; absolute bounds force writeback to begin almost immediately in microscopic batches, preventing huge dirty block spikes that lock the bus controller:
BASH
# Cap dirty page allocation to small, continuous bursts
sysctl -w vm.dirty_background_bytes=4194304   # 4 MB
sysctl -w vm.dirty_bytes=16777216              # 16 MB
sysctl -w vm.dirty_writeback_centisecs=100    # Scan every 1s
Second, decouple logging and hardware supervisory tasks into dedicated runtime boundaries. The watchdog keep-alive must execute inside a dedicated, high-priority real-time thread (SCHED_FIFO or SCHED_RR) that performs zero heap allocation and zero disk I/O. If the audit daemon stalls, the watchdog should register the service failure gracefully rather than bringing down the entire node's networking stack. Third, network-facing BACnet ingress must adopt non-blocking lossy logging. If logging throughput exceeds the write endurance or capability of the physical storage medium, audit entries must be dropped at the ring-buffer level via bounded memory-backed queues. Dropping audit telemetry during an active broadcast storm is acceptable; hanging the physical climate loop of a biological research lab or a commercial data center is not. Finally, the front line of defense should be offloaded to early packet filters using eBPF or targeted rate-limiting rules at the network layer. Rather than allowing thousands of broadcast frames to reach userspace socket buffers, an in-kernel XDP filter can drop unauthorized BACnet broadcasts before socket buffer allocation occurs:
BASH
# Rate limit BACnet broadcasts at the ingress filter
nft add table netdev filter
nft add chain netdev filter ingress '{ type filter hook ingress device eth0 priority 0; }'
nft add rule netdev filter ingress udp dport 47808 meta pkttype broadcast limit rate over 100/second drop
Building automation systems are increasingly converged with enterprise IP networks. As IT and OT merge, the software controlling physical facilities must be engineered with the same defensive durability we demand of mission-critical databases: deterministic memory models, zero-alloc crash isolation, and strict isolation between telemetry storage and physical control loops.