flightgear: 3 CWE-407 defects, MOAD 0002-0005 CLEAN
flightgear-0001: addSegment/addParking std::find on m_nodes vector O(S*N), 50x flightgear-0002: Dijkstra findShortestRoute vector unvisited O(V^2), 13.6x flightgear-0003: A* airway search linear findInOpen O(E*V), 4x
This commit is contained in:
parent
d3746b98c9
commit
e92597895e
6 changed files with 643 additions and 0 deletions
71
defects/flightgear-0003/patch/flightgear-0003.patch
Normal file
71
defects/flightgear-0003/patch/flightgear-0003.patch
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
--- a/src/Navaids/airways.cxx
|
||||
+++ b/src/Navaids/airways.cxx
|
||||
@@ -607,16 +607,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
-/**
|
||||
- * Inefficent (linear) helper to find an open node in the heap
|
||||
- */
|
||||
-static AStarOpenNodeRef
|
||||
-findInOpen(const OpenNodeHeap& aHeap, FGPositioned* aPos)
|
||||
-{
|
||||
- for (unsigned int i=0; i<aHeap.size(); ++i) {
|
||||
- if (aHeap[i]->node == aPos) {
|
||||
- return aHeap[i];
|
||||
- }
|
||||
- }
|
||||
-
|
||||
- return nullptr;
|
||||
-}
|
||||
|
||||
class HeapOrder
|
||||
{
|
||||
@@ -631,6 +621,7 @@
|
||||
bool Airway::Network::search2(FGPositionedRef aStart, FGPositionedRef aDest,
|
||||
WayptVec& aRoute)
|
||||
{
|
||||
typedef set<PositionedID> ClosedNodeSet;
|
||||
+ typedef std::unordered_map<FGPositioned*, AStarOpenNodeRef> OpenNodeMap;
|
||||
|
||||
OpenNodeHeap openNodes;
|
||||
ClosedNodeSet closedNodes;
|
||||
HeapOrder ordering;
|
||||
+ OpenNodeMap openNodeMap; // O(1) lookup replacing linear findInOpen
|
||||
|
||||
openNodes.push_back(new AStarOpenNode(aStart, 0.0, 0, aDest, nullptr));
|
||||
+ openNodeMap[aStart] = openNodes.back();
|
||||
|
||||
// A* open node iteration
|
||||
while (!openNodes.empty()) {
|
||||
@@ -648,6 +639,7 @@
|
||||
std::pop_heap(openNodes.begin(), openNodes.end(), ordering);
|
||||
AStarOpenNodeRef x = openNodes.back();
|
||||
FGPositioned* xp = x->node;
|
||||
openNodes.pop_back();
|
||||
closedNodes.insert(xp->guid());
|
||||
+ openNodeMap.erase(xp);
|
||||
|
||||
// check if xp is the goal
|
||||
if (xp == aDest) {
|
||||
@@ -663,7 +655,12 @@
|
||||
|
||||
FGPositioned* yp = cache->loadById(other.second);
|
||||
double edgeDistanceM = SGGeodesy::distanceM(xp->geod(), yp->geod());
|
||||
- AStarOpenNodeRef y = findInOpen(openNodes, yp);
|
||||
+ AStarOpenNodeRef y;
|
||||
+ auto omit = openNodeMap.find(yp);
|
||||
+ if (omit != openNodeMap.end()) {
|
||||
+ y = omit->second;
|
||||
+ }
|
||||
if (y) { // already open
|
||||
double g = x->distanceFromStart + edgeDistanceM;
|
||||
if (g > y->distanceFromStart) {
|
||||
@@ -683,6 +680,7 @@
|
||||
} else { // not open, insert a new node for y into the heap
|
||||
y = new AStarOpenNode(yp, edgeDistanceM, other.first, aDest, x);
|
||||
openNodes.push_back(y);
|
||||
+ openNodeMap[yp] = y;
|
||||
std::push_heap(openNodes.begin(), openNodes.end(), ordering);
|
||||
}
|
||||
} // of neighbour iteration
|
||||
202
defects/flightgear-0003/test/test_flightgear_0003.py
Normal file
202
defects/flightgear-0003/test/test_flightgear_0003.py
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
"""
|
||||
flightgear-0003: Airway::Network::search2 A* linear findInOpen O(E*V)
|
||||
DEFECT: findInOpen scans open-node heap linearly per edge, O(E*V)
|
||||
Comment in source: "Inefficient (linear) helper"
|
||||
FIX: unordered_map for O(1) open-node lookup
|
||||
|
||||
Simulates our airway A* pathfinding. Real-world airway networks have
|
||||
thousands of navaids connected by airways.
|
||||
"""
|
||||
import time
|
||||
import heapq
|
||||
import random
|
||||
import math
|
||||
|
||||
|
||||
def heuristic(node, goal, positions):
|
||||
"""Euclidean distance heuristic."""
|
||||
x1, y1 = positions[node]
|
||||
x2, y2 = positions[goal]
|
||||
return math.sqrt((x1 - x2)**2 + (y1 - y2)**2)
|
||||
|
||||
|
||||
def build_airway_network(n_nodes):
|
||||
"""Build a random airway network."""
|
||||
positions = {}
|
||||
for i in range(n_nodes):
|
||||
positions[i] = (random.uniform(0, 1000), random.uniform(0, 1000))
|
||||
|
||||
adj = {i: [] for i in range(n_nodes)}
|
||||
for i in range(n_nodes):
|
||||
# Connect to 3-6 nearest neighbors
|
||||
dists = []
|
||||
for j in range(n_nodes):
|
||||
if i == j:
|
||||
continue
|
||||
d = heuristic(i, j, positions)
|
||||
dists.append((d, j))
|
||||
dists.sort()
|
||||
for d, j in dists[:random.randint(3, 6)]:
|
||||
adj[i].append((j, d))
|
||||
adj[j].append((i, d))
|
||||
return adj, positions
|
||||
|
||||
|
||||
def astar_unpatched(adj, positions, start, goal):
|
||||
"""Original: linear findInOpen per edge."""
|
||||
INF = float('inf')
|
||||
open_list = [] # (f_score, node, g_score, prev)
|
||||
open_set_list = [] # list of (node, ref_index) for linear findInOpen
|
||||
closed = set()
|
||||
|
||||
g = {start: 0.0}
|
||||
h = heuristic(start, goal, positions)
|
||||
entry = [h, start, 0.0, None]
|
||||
open_list.append(entry)
|
||||
open_set_list.append(entry)
|
||||
heapq.heapify(open_list)
|
||||
|
||||
ops = 0
|
||||
while open_list:
|
||||
f, current, g_curr, prev = heapq.heappop(open_list)
|
||||
if current in closed:
|
||||
continue
|
||||
closed.add(current)
|
||||
|
||||
if current == goal:
|
||||
return g_curr, ops
|
||||
|
||||
for neighbor, weight in adj[current]:
|
||||
ops += 1
|
||||
if neighbor in closed:
|
||||
continue
|
||||
|
||||
tentative_g = g_curr + weight
|
||||
|
||||
# Linear findInOpen: scan entire open list for neighbor
|
||||
found = None
|
||||
for entry in open_set_list:
|
||||
ops += 1
|
||||
if entry[1] == neighbor and entry[1] not in closed:
|
||||
found = entry
|
||||
break
|
||||
|
||||
if found is not None:
|
||||
if tentative_g < found[2]:
|
||||
# Update: rebuild heap
|
||||
found[2] = tentative_g
|
||||
found[0] = tentative_g + heuristic(neighbor, goal, positions)
|
||||
heapq.heapify(open_list)
|
||||
else:
|
||||
if neighbor not in g or tentative_g < g[neighbor]:
|
||||
g[neighbor] = tentative_g
|
||||
h_val = heuristic(neighbor, goal, positions)
|
||||
entry = [tentative_g + h_val, neighbor, tentative_g, current]
|
||||
heapq.heappush(open_list, entry)
|
||||
open_set_list.append(entry)
|
||||
|
||||
return INF, ops
|
||||
|
||||
|
||||
def astar_patched(adj, positions, start, goal):
|
||||
"""Patched: hash map for O(1) open-node lookup."""
|
||||
INF = float('inf')
|
||||
open_list = [] # heap
|
||||
open_map = {} # node -> entry (O(1) lookup)
|
||||
closed = set()
|
||||
|
||||
g = {start: 0.0}
|
||||
h = heuristic(start, goal, positions)
|
||||
entry = [h, start, 0.0, None]
|
||||
open_list.append(entry)
|
||||
open_map[start] = entry
|
||||
heapq.heapify(open_list)
|
||||
|
||||
ops = 0
|
||||
while open_list:
|
||||
f, current, g_curr, prev = heapq.heappop(open_list)
|
||||
if current in closed:
|
||||
continue
|
||||
closed.add(current)
|
||||
open_map.pop(current, None)
|
||||
|
||||
if current == goal:
|
||||
return g_curr, ops
|
||||
|
||||
for neighbor, weight in adj[current]:
|
||||
ops += 1
|
||||
if neighbor in closed:
|
||||
continue
|
||||
|
||||
tentative_g = g_curr + weight
|
||||
|
||||
# O(1) lookup instead of linear scan
|
||||
found = open_map.get(neighbor)
|
||||
ops += 1
|
||||
|
||||
if found is not None:
|
||||
if tentative_g < found[2]:
|
||||
found[2] = tentative_g
|
||||
found[0] = tentative_g + heuristic(neighbor, goal, positions)
|
||||
heapq.heapify(open_list)
|
||||
else:
|
||||
if neighbor not in g or tentative_g < g[neighbor]:
|
||||
g[neighbor] = tentative_g
|
||||
h_val = heuristic(neighbor, goal, positions)
|
||||
entry = [tentative_g + h_val, neighbor, tentative_g, current]
|
||||
heapq.heappush(open_list, entry)
|
||||
open_map[neighbor] = entry
|
||||
|
||||
return INF, ops
|
||||
|
||||
|
||||
def test_correctness():
|
||||
"""Verify both produce same path cost."""
|
||||
random.seed(42)
|
||||
n = 100
|
||||
adj, positions = build_airway_network(n)
|
||||
d_unpatched, _ = astar_unpatched(adj, positions, 0, n-1)
|
||||
d_patched, _ = astar_patched(adj, positions, 0, n-1)
|
||||
assert abs(d_unpatched - d_patched) < 1e-6, \
|
||||
f"Distances differ: {d_unpatched} vs {d_patched}"
|
||||
print(f"PASS correctness: distance={d_unpatched:.2f}")
|
||||
|
||||
|
||||
def test_performance():
|
||||
"""Benchmark at V=800 (realistic airway network size)."""
|
||||
random.seed(42)
|
||||
n = 800
|
||||
adj, positions = build_airway_network(n)
|
||||
|
||||
iters = 5
|
||||
pairs = [(random.randint(0, n-1), random.randint(0, n-1)) for _ in range(iters)]
|
||||
|
||||
t0 = time.perf_counter()
|
||||
ops_u = 0
|
||||
for s, e in pairs:
|
||||
_, o = astar_unpatched(adj, positions, s, e)
|
||||
ops_u += o
|
||||
t_unpatched = time.perf_counter() - t0
|
||||
|
||||
t0 = time.perf_counter()
|
||||
ops_p = 0
|
||||
for s, e in pairs:
|
||||
_, o = astar_patched(adj, positions, s, e)
|
||||
ops_p += o
|
||||
t_patched = time.perf_counter() - t0
|
||||
|
||||
ratio = t_unpatched / t_patched if t_patched > 0 else float('inf')
|
||||
op_ratio = ops_u / ops_p if ops_p > 0 else float('inf')
|
||||
print(f"V={n}, {iters} pathfinds")
|
||||
print(f"Unpatched: {t_unpatched*1e3:.1f} ms, {ops_u} ops")
|
||||
print(f"Patched: {t_patched*1e3:.1f} ms, {ops_p} ops")
|
||||
print(f"Time ratio: {ratio:.1f}x")
|
||||
print(f"Ops ratio: {op_ratio:.1f}x")
|
||||
|
||||
assert ratio > 2.0, f"Expected >2x speedup, got {ratio:.1f}x"
|
||||
print(f"PASS performance: {ratio:.1f}x speedup")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_correctness()
|
||||
test_performance()
|
||||
Loading…
Add table
Add a link
Reference in a new issue