From 7c9fbc9e47455ee9b14c04fed0e9b7461b97f76c Mon Sep 17 00:00:00 2001 From: Tomas Jansky Date: Tue, 16 Jun 2026 15:11:56 +0200 Subject: [PATCH 01/11] ft-replay: Remove redundant enum keyword from scoped enum types PacketInfo members and DecidePort() parameter used the elaborated-type- specifier 'enum Foo' for types declared as 'enum class Foo'. While many compilers accept this, mixing the two forms is non-conforming per the C++ standard and can fail with stricter compiler settings. Replace with the plain type name directly. --- tools/ft-replay/src/packet.hpp | 6 +++--- tools/ft-replay/src/packetBuilder.cpp | 2 +- tools/ft-replay/src/packetBuilder.hpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/ft-replay/src/packet.hpp b/tools/ft-replay/src/packet.hpp index d11bcd26..fb9ef1e8 100644 --- a/tools/ft-replay/src/packet.hpp +++ b/tools/ft-replay/src/packet.hpp @@ -47,15 +47,15 @@ enum class OutInterface { * */ struct PacketInfo { - enum L3Type l3Type; + L3Type l3Type; uint16_t l3Offset; - enum L4Type l4Type; + L4Type l4Type; uint16_t l4Offset; //< Zero if l4type == L4Type::NotFound uint16_t ipAddressesChecksum; //< checksum of IP addresses in host byte order - enum OutInterface outInterface; //< used for multi-port replaying + OutInterface outInterface; //< used for multi-port replaying }; /** diff --git a/tools/ft-replay/src/packetBuilder.cpp b/tools/ft-replay/src/packetBuilder.cpp index df6c4c17..acf79d3c 100644 --- a/tools/ft-replay/src/packetBuilder.cpp +++ b/tools/ft-replay/src/packetBuilder.cpp @@ -129,7 +129,7 @@ void PacketBuilder::PresetHwChecksum(Packet& packet) } } -int PacketBuilder::DecidePort(const std::byte* rawData, enum L3Type l3Type, uint16_t l3Offset) const +int PacketBuilder::DecidePort(const std::byte* rawData, L3Type l3Type, uint16_t l3Offset) const { if (l3Type == L3Type::IPv4) { const iphdr* ipHeader = reinterpret_cast(rawData + l3Offset); diff --git a/tools/ft-replay/src/packetBuilder.hpp b/tools/ft-replay/src/packetBuilder.hpp index 686462b5..7b7e10cc 100644 --- a/tools/ft-replay/src/packetBuilder.hpp +++ b/tools/ft-replay/src/packetBuilder.hpp @@ -97,7 +97,7 @@ class PacketBuilder { std::unique_ptr GetDataCopyWithVlan(const std::byte* rawData, uint16_t dataLen); void ValidatePacketLength(uint16_t packetLength) const; void PresetHwChecksum(Packet& packet); - int DecidePort(const std::byte* rawData, enum L3Type l3Type, uint16_t l3Offset) const; + int DecidePort(const std::byte* rawData, L3Type l3Type, uint16_t l3Offset) const; uint16_t _vlanID = 0; double _timeMultiplier = 1.0; From 58710272536231cffaa595072cc9e049b656a746 Mon Sep 17 00:00:00 2001 From: Tomas Jansky Date: Tue, 16 Jun 2026 15:12:07 +0200 Subject: [PATCH 02/11] ft-replay: Fix asymmetric interface selection when src == dst IP DecidePort() returns 0 when source and destination IP addresses are equal (or when parsing fails). The previous check 'result < 0' mapped this case to Interface1, making the assignment non-deterministic and asymmetric. Changing to '<= 0' ensures equal addresses always go to Interface0, providing a stable deterministic default. --- tools/ft-replay/src/packetBuilder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ft-replay/src/packetBuilder.cpp b/tools/ft-replay/src/packetBuilder.cpp index acf79d3c..521bb1da 100644 --- a/tools/ft-replay/src/packetBuilder.cpp +++ b/tools/ft-replay/src/packetBuilder.cpp @@ -254,7 +254,7 @@ PacketInfo PacketBuilder::GetPacketInfo(const RawPacket* rawPacket) const info.l4Type = LayerToL4Protocol(l4Layer._type); } - if (DecidePort(rawPacket->data, info.l3Type, info.l3Offset) < 0) { + if (DecidePort(rawPacket->data, info.l3Type, info.l3Offset) <= 0) { info.outInterface = OutInterface::Interface0; } else { info.outInterface = OutInterface::Interface1; From e6fec5cd7a35113527649d705e5a378dc029905a Mon Sep 17 00:00:00 2001 From: Tomas Jansky Date: Tue, 16 Jun 2026 15:12:16 +0200 Subject: [PATCH 03/11] ft-profile-sampler: Fix missing throw in GetHistogramBin() std::runtime_error was constructed but immediately discarded without being thrown. When the histogram has not been initialized the function silently continued, leading to undefined behaviour on the empty vector access below. Add the missing 'throw' keyword. --- tools/ft-profile-sampler/src/biflow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ft-profile-sampler/src/biflow.cpp b/tools/ft-profile-sampler/src/biflow.cpp index 64673d30..6d27b520 100644 --- a/tools/ft-profile-sampler/src/biflow.cpp +++ b/tools/ft-profile-sampler/src/biflow.cpp @@ -125,7 +125,7 @@ void Biflow::CreateHistogram(ft::Timestamp start, ft::Timestamp interval, unsign double Biflow::GetHistogramBin(unsigned idx) const { if (pkt_hist.size() == 0) { - std::runtime_error("packet histogram has not been created"); + throw std::runtime_error("packet histogram has not been created"); } if (idx >= start_window_idx && idx <= end_window_idx) { From a83ee88e24a7aa82bfd8657e890a628fb0fdb4fc Mon Sep 17 00:00:00 2001 From: Tomas Jansky Date: Tue, 16 Jun 2026 15:12:27 +0200 Subject: [PATCH 04/11] ft-profile-sampler: Fix out-of-bounds loop in GatherWindowStats() The loop condition 'windowIndex <= end_window_idx / WINDOW_SIZE + 1' allowed windowIndex to reach nOfWindows when end_window_idx was the last valid bin, which is one past the end of windowsAcc (size nOfWindows). The spurious '+ 1' served no purpose since the inner body already guards the write via 'if (pkts > 0)' and GetHistogramBin() returns 0 for out-of-range indices. Remove the '+ 1' to keep the loop strictly within bounds. --- tools/ft-profile-sampler/src/metrics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ft-profile-sampler/src/metrics.cpp b/tools/ft-profile-sampler/src/metrics.cpp index f466e2ec..2f92e39f 100644 --- a/tools/ft-profile-sampler/src/metrics.cpp +++ b/tools/ft-profile-sampler/src/metrics.cpp @@ -132,7 +132,7 @@ void Metrics::GatherWindowStats(const std::vector& data, unsigned histSi } const auto& biflow = data[i]; for (unsigned windowIndex = biflow.start_window_idx / WINDOW_SIZE; - windowIndex <= biflow.end_window_idx / WINDOW_SIZE + 1; + windowIndex <= biflow.end_window_idx / WINDOW_SIZE; windowIndex++) { double pkts = 0; for (unsigned j = 0; j < WINDOW_SIZE; j++) { From 03defd567e18bb6c283a5c80a7b57b9c1cbc80d5 Mon Sep 17 00:00:00 2001 From: Tomas Jansky Date: Tue, 16 Jun 2026 15:12:55 +0200 Subject: [PATCH 05/11] ft-profile-sampler: Propagate worker exceptions via future::get() CreateInitialPopulation() called fut.wait() on the worker futures, which blocks until completion but silently discards any stored exception. If a worker throws, the population is left in an incomplete state and UpdateFitnessStats() runs on bad data. Switching to fut.get() re-throws the stored exception to the caller. --- tools/ft-profile-sampler/src/evolution.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ft-profile-sampler/src/evolution.cpp b/tools/ft-profile-sampler/src/evolution.cpp index bae47949..4056cfde 100644 --- a/tools/ft-profile-sampler/src/evolution.cpp +++ b/tools/ft-profile-sampler/src/evolution.cpp @@ -35,7 +35,7 @@ void Evolution::CreateInitialPopulation() } for (auto& fut : futures) { - fut.wait(); + fut.get(); } UpdateFitnessStats(); From 1b02e7c2f754cc1f387ca15603f80d282c56e257 Mon Sep 17 00:00:00 2001 From: Tomas Jansky Date: Tue, 16 Jun 2026 15:13:05 +0200 Subject: [PATCH 06/11] ft-profile-sampler: Fix out-of-bounds gene index in InitialPopulationWorker() std::uniform_int_distribution(0, profileSize) is inclusive on both ends, so it can generate an index equal to profileSize. Since genotype has size profileSize (valid indices 0..profileSize-1), this is an out-of-bounds access causing undefined behaviour. Change the upper bound to profileSize - 1. --- tools/ft-profile-sampler/src/evolution.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ft-profile-sampler/src/evolution.cpp b/tools/ft-profile-sampler/src/evolution.cpp index 4056cfde..4723f575 100644 --- a/tools/ft-profile-sampler/src/evolution.cpp +++ b/tools/ft-profile-sampler/src/evolution.cpp @@ -46,7 +46,7 @@ void Evolution::InitialPopulationWorker(uint64_t seed) uint64_t profileSize = _profile->GetSize(); std::mt19937_64 rnd(seed); std::uniform_int_distribution geneCountDistrib(_minGenesCnt, _maxGenesCnt); - std::uniform_int_distribution geneDistrib(0, profileSize); + std::uniform_int_distribution geneDistrib(0, profileSize - 1); while (true) { { std::lock_guard lock(_mtx); From 677af0f0cbd4cad92261f10008b0efa88dc41da5 Mon Sep 17 00:00:00 2001 From: Tomas Jansky Date: Tue, 16 Jun 2026 15:13:17 +0200 Subject: [PATCH 07/11] ft-fast-analyzer: Default-initialize SMSubnetSegment::bidir to false The 'bidir' member had no default initializer, so default-constructing SMSubnetSegment (e.g. from pybind11 py::init<>()) left it with an indeterminate value. Contains() reads bidir unconditionally, resulting in undefined behaviour. Initialize to 'false' (unidirectional) as the safe default. --- tools/ft-fast-analyzer/src/statisticalmodel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ft-fast-analyzer/src/statisticalmodel.h b/tools/ft-fast-analyzer/src/statisticalmodel.h index 386d17bf..e8899858 100644 --- a/tools/ft-fast-analyzer/src/statisticalmodel.h +++ b/tools/ft-fast-analyzer/src/statisticalmodel.h @@ -80,7 +80,7 @@ struct SMSubnetSegment final : public SMSegment { std::optional source; ///< Source subnet. std::optional dest; ///< Destination subnet. - bool bidir; ///< Bidirectional flag. + bool bidir = false; ///< Bidirectional flag. }; /** From 44119faac4add327753e4115bd78195cd76c5616 Mon Sep 17 00:00:00 2001 From: Tomas Jansky Date: Tue, 16 Jun 2026 15:13:29 +0200 Subject: [PATCH 08/11] ft-analyzer: Fix incorrect sysconfig path for ft_fast_analyzer import sysconfig.get_path("stdlib") returns the standard library directory (e.g. /usr/lib/python3.9), not the site-packages directory. The path was then extended with a hardcoded "/site-packages/flowtest" suffix which accidentally worked on some layouts but is not guaranteed. Use sysconfig.get_path("platlib") which directly returns the compiled extension modules site-packages directory, and os.path.join() for safe path construction. --- tools/ft-analyzer/ftanalyzer/common/fast_analyzer_wrapper.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/ft-analyzer/ftanalyzer/common/fast_analyzer_wrapper.py b/tools/ft-analyzer/ftanalyzer/common/fast_analyzer_wrapper.py index 39320a55..ea17eafb 100644 --- a/tools/ft-analyzer/ftanalyzer/common/fast_analyzer_wrapper.py +++ b/tools/ft-analyzer/ftanalyzer/common/fast_analyzer_wrapper.py @@ -36,8 +36,7 @@ ) from ftanalyzer.reports.statistical_report import StatisticalReport -SITE_PACKAGES_PREFIX = sysconfig.get_path("stdlib") -GLOBAL_IMPORT_DIR = f"{SITE_PACKAGES_PREFIX}/site-packages/flowtest" +GLOBAL_IMPORT_DIR = os.path.join(sysconfig.get_path("platlib"), "flowtest") def fast_analyzer_available() -> bool: From 1c0b10a6a72518942fda59af49ad0d1dc8de1442 Mon Sep 17 00:00:00 2001 From: Tomas Jansky Date: Tue, 16 Jun 2026 15:13:43 +0200 Subject: [PATCH 09/11] ft-generator: Fix ParseSpeedUnit erroneous x8 multiplication ParseSpeedUnit() applied 'multiplier * 8' before returning, turning e.g. '100 gbps' into 800e9 instead of the correct 100e9 bits/sec. Since gbps/mbps/kbps/bps units are already in bits per second, the extra factor of 8 was wrong and inconsistent with the default _linkSpeedBps value of 100e9 (100 Gbps). Remove the '* 8' so the function returns the correct bits-per-second value. --- tools/ft-generator/src/config/common.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ft-generator/src/config/common.cpp b/tools/ft-generator/src/config/common.cpp index 697f263d..7497d1e6 100644 --- a/tools/ft-generator/src/config/common.cpp +++ b/tools/ft-generator/src/config/common.cpp @@ -191,7 +191,7 @@ uint64_t ParseSpeedUnit(const YAML::Node& node) throw ConfigError(node, "invalid speed unit value"); } - return OverflowCheckedMultiply(*value, multiplier * 8); + return OverflowCheckedMultiply(*value, multiplier); } void ConfigError::PrintPrettyError( From 8dc0da631a8a10f5a1022da01ed766e6c8f2a582 Mon Sep 17 00:00:00 2001 From: Tomas Jansky Date: Tue, 16 Jun 2026 15:13:56 +0200 Subject: [PATCH 10/11] ft-generator: Fix test_timestamps calc_min_gap_nanos using min() instead of sum The C++ implementation (CalcMinGlobalPacketGapPicos) computes the minimum inter-packet gap as the wire transfer time PLUS the configured min_packet_gap: 'minGap = transferTime + minPacketGapNanos'. The Python test helper incorrectly returned min(transfer_nanos, min_packet_gap_ns), which can be smaller than the actual minimum gap enforced by the code. This made the test weaker than intended and could mask regressions. Change to use the sum to match the implementation. --- .../tests/ftgeneratortests/tests/pcap/test_timestamps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ft-generator/tests/ftgeneratortests/tests/pcap/test_timestamps.py b/tools/ft-generator/tests/ftgeneratortests/tests/pcap/test_timestamps.py index 0a881b69..ce5ffe36 100644 --- a/tools/ft-generator/tests/ftgeneratortests/tests/pcap/test_timestamps.py +++ b/tools/ft-generator/tests/ftgeneratortests/tests/pcap/test_timestamps.py @@ -32,7 +32,7 @@ def calc_min_gap_nanos(packet_size, link_speed_gbps, min_packet_gap_ns): transfer_nanos = frame_total * 1_000_000_000 / (link_speed_gbps * 1_000_000_000) - return min(transfer_nanos, min_packet_gap_ns) + return transfer_nanos + min_packet_gap_ns def test_timestamp(ft_generator: Generator): From eac4c7f28c7d2b71e27ac4eca4a0f92b4c7db0a0 Mon Sep 17 00:00:00 2001 From: Tomas Jansky Date: Tue, 16 Jun 2026 15:14:08 +0200 Subject: [PATCH 11/11] ft-replay: Fix DPDK MTU using full frame size instead of L3 MTU rte_eth_dev_set_mtu() and the devInfo min/max_mtu validation both operate on the L3 payload MTU (e.g. 1500 for standard Ethernet), but _MTUSize (default 1518) represents the full Ethernet frame size including the 14-byte header and 4-byte FCS. Passing the frame size directly caused an 18-byte discrepancy. Define ETH_OVERHEAD = 18 and subtract it when calling rte_eth_dev_set_mtu() and when comparing against devInfo min/max_mtu. --- tools/ft-replay/src/dpdkPlugin.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/ft-replay/src/dpdkPlugin.cpp b/tools/ft-replay/src/dpdkPlugin.cpp index 67f76e83..8e0475fb 100644 --- a/tools/ft-replay/src/dpdkPlugin.cpp +++ b/tools/ft-replay/src/dpdkPlugin.cpp @@ -30,6 +30,9 @@ namespace replay { +// Ethernet frame overhead: 14-byte header + 4-byte FCS +static constexpr size_t ETH_OVERHEAD = 18; + // register the dpdk plugin to the factory OutputPluginFactoryRegistrator dpdkPluginRegistration("dpdk"); @@ -146,7 +149,7 @@ int DpdkPlugin::GetDpdkPort(const std::string& PCIAddress, struct rte_eth_dev_in throwErr(); } - if (_MTUSize < devInfo->min_mtu || _MTUSize > devInfo->max_mtu) { + if ((_MTUSize - ETH_OVERHEAD) < devInfo->min_mtu || (_MTUSize - ETH_OVERHEAD) > devInfo->max_mtu) { _logger->error( "MTU size of out NIC supported range {}-{}", devInfo->min_mtu, @@ -185,7 +188,7 @@ void DpdkPlugin::ConfigureDpdkPort(uint16_t portId) } } - ret = rte_eth_dev_set_mtu(portId, _MTUSize); + ret = rte_eth_dev_set_mtu(portId, _MTUSize - ETH_OVERHEAD); if (ret < 0) { _logger->error("rte_eth_dev_set_mtu() has failed with code {}", ret); throwErr();