dsdv: LookForQueuedPackets() ignores a failed LookupRoute() and sends on a route with a null output device (assert in default, silent SIGSEGV in optimized)
Summary
dsdv::RoutingProtocol::LookForQueuedPackets() discards the return value of m_routingTable.LookupRoute() and forwards queued packets on the resulting default-constructed entry's route, whose output device is null. In a default (assert-enabled) build this dies at NS_ASSERT (interface >= 0) in Ipv4L3Protocol::SendRealOut (ipv4-l3-protocol.cc:960 on current master); in an optimized build the assert is compiled out and the same fault is a bare SIGSEGV with completely empty stderr (verified: exit 139, stderr 0 bytes).
The defect is deterministic on any node with more than one non-loopback interface (reproducer below — 3-node point-to-point chain, stock modules only), and is present unchanged from ns-3.36 through current master (b677fede, 2026-08-14); the code dates to the original 2010 contribution.
This is very likely the same crash as #503 (closed) (2021, wifi ad hoc, ~0.27 % of 7800 runs, closed 2025-06-09 "assuming it was fixed" — no fix was ever made; the code is byte-identical today, and that issue still carries module::internet although the fault is in dsdv).
Reproducer (stock modules only)
Drop the file below into scratch/ and run:
./ns3 run dsdv-multi-interface-crashTopology: n0 --10.1.1.0/30-- n1 --10.1.2.0/30-- n2, one UDP flow n2→n0. Only n1 is multi-interface. Verified on ns-3.36, ns-3.48 and master b677fede, all crashing at the same simulated time:
node 0 has 1 non-loopback interface(s): 10.1.1.1
node 1 has 2 non-loopback interface(s): 10.1.1.2 10.1.2.1
node 2 has 1 non-loopback interface(s): 10.1.2.2
sending from node 2 to 10.1.1.1
--- starting simulator ---
NS_ASSERT failed, cond="interface >= 0", +1.512000000s 2 file=.../src/internet/model/ipv4-l3-protocol.cc, line=960Controls on the identical topology: --nNodes=2 (single-interface) completes and delivers 9728 B; --protocol=aodv|olsr|rip complete and deliver (9728 / 8512 / 9728 B); --csma=1 crashes identically, so the device type is irrelevant. Under --build-profile=optimized the same run is a SIGSEGV with empty stderr — which is how it appears in CI logs, with nothing to search for.
scratch/dsdv-multi-interface-crash.cc
/*
* Minimal standalone reproducer for an ns-3 DSDV defect.
*
* Stock ns-3 modules only: core, network, internet, point-to-point,
* applications, dsdv. No external contrib code.
*
* Topology: a three-node point-to-point chain.
*
* n0 ---- 10.1.1.0/30 ---- n1 ---- 10.1.2.0/30 ---- n2
* 10.1.1.1 10.1.1.2 10.1.2.1 10.1.2.2
*
* n1 is the only node with more than one non-loopback interface.
*/
#include "ns3/applications-module.h"
#include "ns3/core-module.h"
#include "ns3/csma-module.h"
#include "ns3/aodv-module.h"
#include "ns3/dsdv-module.h"
#include "ns3/olsr-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
using namespace ns3;
NS_LOG_COMPONENT_DEFINE("DsdvMultiInterfaceCrash");
int
main(int argc, char* argv[])
{
uint32_t nNodes = 3;
double stopTime = 40.0;
bool useCsma = false;
std::string protocol = "dsdv";
CommandLine cmd(__FILE__);
cmd.AddValue("nNodes", "Number of nodes in the chain (>=3 gives a multi-interface node)", nNodes);
cmd.AddValue("stopTime", "Simulation stop time in seconds", stopTime);
cmd.AddValue("csma", "Use CSMA links instead of point-to-point", useCsma);
cmd.AddValue("protocol", "Routing protocol: dsdv | aodv | olsr | rip", protocol);
cmd.Parse(argc, argv);
NS_ABORT_MSG_IF(nNodes < 2, "need at least 2 nodes");
NodeContainer nodes;
nodes.Create(nNodes);
PointToPointHelper p2p;
p2p.SetDeviceAttribute("DataRate", StringValue("5Mbps"));
p2p.SetChannelAttribute("Delay", StringValue("2ms"));
CsmaHelper csma;
csma.SetChannelAttribute("DataRate", StringValue("5Mbps"));
csma.SetChannelAttribute("Delay", StringValue("2ms"));
// One device per link. The interior nodes of the chain therefore each hold
// two non-loopback interfaces -- exactly the shape of a satellite with
// several inter-satellite links. The device type is irrelevant to the bug;
// --csma selects CSMA to demonstrate that.
std::vector<NetDeviceContainer> links;
for (uint32_t i = 0; i + 1 < nNodes; ++i)
{
NodeContainer pair(nodes.Get(i), nodes.Get(i + 1));
links.push_back(useCsma ? csma.Install(pair) : p2p.Install(pair));
}
// The routing protocol is selectable purely so the same topology can be
// used to show that the sibling protocols survive it. Default is dsdv.
InternetStackHelper internet;
DsdvHelper dsdv;
AodvHelper aodv;
OlsrHelper olsr;
RipHelper rip;
if (protocol == "dsdv")
{
internet.SetRoutingHelper(dsdv);
}
else if (protocol == "aodv")
{
internet.SetRoutingHelper(aodv);
}
else if (protocol == "olsr")
{
internet.SetRoutingHelper(olsr);
}
else if (protocol == "rip")
{
internet.SetRoutingHelper(rip);
}
else
{
NS_ABORT_MSG("unknown protocol " << protocol);
}
internet.Install(nodes);
// A distinct /30 per link, as an ISL mesh would use.
Ipv4AddressHelper ipv4;
std::vector<Ipv4InterfaceContainer> ifaces;
for (uint32_t i = 0; i + 1 < nNodes; ++i)
{
std::ostringstream base;
base << "10.1." << (i + 1) << ".0";
ipv4.SetBase(base.str().c_str(), "255.255.255.252");
ifaces.push_back(ipv4.Assign(links[i]));
}
// Report the interface count per node, so the run output shows the trigger
// condition directly.
for (uint32_t i = 0; i < nNodes; ++i)
{
Ptr<Ipv4> ip = nodes.Get(i)->GetObject<Ipv4>();
std::cout << "node " << i << " has " << (ip->GetNInterfaces() - 1)
<< " non-loopback interface(s):";
for (uint32_t k = 1; k < ip->GetNInterfaces(); ++k)
{
std::cout << " " << ip->GetAddress(k, 0).GetLocal();
}
std::cout << std::endl;
}
// Traffic from the last node to the first: a path of (nNodes-1) hops, so
// the route entry at the sender has hop count > 1.
Ipv4Address dstAddr = ifaces.front().GetAddress(0); // 10.1.1.1, on n0
uint16_t port = 9;
PacketSinkHelper sink("ns3::UdpSocketFactory",
InetSocketAddress(Ipv4Address::GetAny(), port));
ApplicationContainer sinkApp = sink.Install(nodes.Get(0));
sinkApp.Start(Seconds(0.0));
sinkApp.Stop(Seconds(stopTime));
OnOffHelper onoff("ns3::UdpSocketFactory", InetSocketAddress(dstAddr, port));
onoff.SetAttribute("OnTime", StringValue("ns3::ConstantRandomVariable[Constant=1]"));
onoff.SetAttribute("OffTime", StringValue("ns3::ConstantRandomVariable[Constant=0]"));
onoff.SetAttribute("DataRate", StringValue("2kbps"));
onoff.SetAttribute("PacketSize", UintegerValue(64));
ApplicationContainer srcApp = onoff.Install(nodes.Get(nNodes - 1));
srcApp.Start(Seconds(1.0));
srcApp.Stop(Seconds(stopTime));
std::cout << "sending from node " << (nNodes - 1) << " to " << dstAddr << std::endl;
std::cout << "--- starting simulator ---" << std::endl;
Simulator::Stop(Seconds(stopTime));
Simulator::Run();
Simulator::Destroy();
uint64_t rx = DynamicCast<PacketSink>(sinkApp.Get(0))->GetTotalRx();
std::cout << "--- simulator finished without crashing; sink received " << rx << " bytes ---"
<< std::endl;
return 0;
}Mechanism
- DSDV advertises its own identity as
m_ipv4->GetAddress (1, 0)— a hardcoded interface index (dsdv-routing-protocol.cc:862,:913on master). n1 therefore announces only10.1.1.2on both links and never announces10.1.2.1. - Receivers set the next hop from the update's source address (
:619). n2 hears n1 as10.1.2.1— an address no advertisement ever names — so n2 has no routing-table entry keyed on its own next hop. LookForQueuedPackets()callsm_routingTable.LookupRoute (rt.GetNextHop (), newrt)and ignores the return value (:1161). The lookup fails,newrtstays default-constructed.RoutingTableEntry's constructor unconditionally doesm_ipv4Route = Create<Ipv4Route> ()(dsdv-rtable.cc), sonewrt.GetRoute ()is a non-nullPtr<Ipv4Route>whose output device is null.Ipv4L3Protocol::SendRealOut'sif (!route)guard passes,GetInterfaceForDevice (nullptr)returns −1, and the assert fires (or, optimized, −1 indexes the interface vector).
Because the route pointer is non-null, the NS_ASSERT (route) directly below the failed lookup can never fire — it guards nothing. Notably, MR !1652 (merged) ("dsdv: Fix order of assert to avoid segmentation fault", merged 2023-09) edited exactly these lines, moving that assert above its first use, while leaving the unchecked LookupRoute on the line above it untouched: the segfault that motivated that MR is, I believe, this one, and the hardened assert cannot catch it.
While the deterministic trigger is multi-interface, defect and trigger are separable: #503 (closed) hit the same three frames (SendRealOut ← SendPacketFromQueue ← LookForQueuedPackets) on a single-interface wifi network, intermittently. A failed lookup must never produce a send, regardless of what made the lookup fail.
Proposed fix
The guarded form of the identical idiom already exists eleven lines earlier in the same file, in RouteOutput (:330):
else
{
RoutingTableEntry newrt;
if (!m_routingTable.LookupRoute(rt.GetNextHop(), newrt))
{
NS_LOG_LOGIC("No route to next hop "
<< rt.GetNextHop() << "; leaving packets queued for "
<< rt.GetDestination());
continue;
}
route = newrt.GetRoute();
...I have this as a patch applying cleanly to master and am happy to open an MR.
One important caveat for whoever lands this: the one-line fix is a memory-safety fix, not a functional one. With it applied, the reproducer no longer crashes but delivers 0 bytes — the advertisement defect in step 1 means multi-interface DSDV still cannot route; packets stay queued forever. Fixing step 1 as well (per-interface advertisement, or a canonical main address as OLSR does with MID + m_mainAddress) restores delivery to parity with AODV/RIP on the same topology (9728 B, verified). I'd suggest treating that as a follow-up issue rather than blocking the crash fix on it, but the release notes for the crash fix should say plainly that multi-interface DSDV remains non-functional until the advertisement is fixed. (For scope: src/dsdv/doc/dsdv.rst documents no single-interface restriction, and DSDV's own LoopbackRoute() comment at :523-527 explicitly contemplates multi-interface nodes — so this is an internal inconsistency, not documented behaviour.)
A higher-leverage optional hardening: SendRealOut could treat interface < 0 as DROP_NO_ROUTE (mirroring its !route guard ten lines above) so this whole class of routing-protocol bug fails safely instead of asserting/indexing with −1 — DSDV is not the only in-tree protocol that constructs routes from unchecked lookups.
Environment
- ns-3.36, ns-3.48, and ns-3-dev master
b677fede0a664ecc4e80615c331dd7f07e58c1c0(2026-08-14) — identical behaviour, identical line numbers on master vs 3.48 - GCC 13.3.0, Linux x86-64;
defaultandoptimizedprofiles as noted
Found while building an inter-satellite-link mesh (every satellite is multi-interface by construction). The reproducer, controls, both build profiles, and the fix-restores-delivery claim were each run and verified as described above.