esp-idf: 2 CWE-312 defects (WiFi PSK + HTTP auth password logged); drone: CWE-407 pubsub O(S*T) + MOAD-0005 thundering herd; MOADs 0002/0003 CLEAN
This commit is contained in:
parent
ad90dddbcc
commit
bc17a5e700
10 changed files with 883 additions and 0 deletions
74
defects/drone-0001/patch/drone-0001.patch
Normal file
74
defects/drone-0001/patch/drone-0001.patch
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
--- a/pubsub/inmem.go
|
||||||
|
+++ b/pubsub/inmem.go
|
||||||
|
@@ -14,7 +14,6 @@ import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
- "golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
@@ -91,7 +90,8 @@ func (r *InMemory) Publish(ctx context.Context, topic string, payload []byte, op
|
||||||
|
topic = formatTopic(pubConfig.app, pubConfig.namespace, topic)
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for _, sub := range r.registry {
|
||||||
|
- if slices.Contains(sub.topics, topic) && !sub.isClosed() {
|
||||||
|
+ if sub.hasTopic(topic) && !sub.isClosed() {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(subscriber *inMemorySubscriber) {
|
||||||
|
defer wg.Done()
|
||||||
|
@@ -128,9 +128,9 @@ func (s *inMemorySubscriber) Subscribe(_ context.Context, topics ...string) erro
|
||||||
|
defer s.mutex.RUnlock()
|
||||||
|
topics = s.formatTopics(topics...)
|
||||||
|
for _, ch := range topics {
|
||||||
|
- if slices.Contains(s.topics, ch) {
|
||||||
|
- continue
|
||||||
|
- }
|
||||||
|
- s.topics = append(s.topics, ch)
|
||||||
|
+ if _, ok := s.topicSet[ch]; !ok {
|
||||||
|
+ s.topicSet[ch] = struct{}{}
|
||||||
|
+ s.topics = append(s.topics, ch)
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -140,9 +140,9 @@ func (s *inMemorySubscriber) Unsubscribe(_ context.Context, topics ...string) er
|
||||||
|
defer s.mutex.RUnlock()
|
||||||
|
topics = s.formatTopics(topics...)
|
||||||
|
for _, ch := range topics {
|
||||||
|
- if slices.Contains(s.topics, ch) {
|
||||||
|
- s.topics[i] = s.topics[len(s.topics)-1]
|
||||||
|
- s.topics = s.topics[:len(s.topics)-1]
|
||||||
|
- }
|
||||||
|
+ if _, ok := s.topicSet[ch]; ok {
|
||||||
|
+ delete(s.topicSet, ch)
|
||||||
|
+ // rebuild slice from set
|
||||||
|
+ s.topics = s.topics[:0]
|
||||||
|
+ for t := range s.topicSet {
|
||||||
|
+ s.topics = append(s.topics, t)
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
+func (s *inMemorySubscriber) hasTopic(topic string) bool {
|
||||||
|
+ s.mutex.RLock()
|
||||||
|
+ defer s.mutex.RUnlock()
|
||||||
|
+ _, ok := s.topicSet[topic]
|
||||||
|
+ return ok
|
||||||
|
+}
|
||||||
|
|
||||||
|
// struct change: add topicSet map alongside topics slice
|
||||||
|
type inMemorySubscriber struct {
|
||||||
|
config *SubscribeConfig
|
||||||
|
handler func([]byte) error
|
||||||
|
channel chan []byte
|
||||||
|
once sync.Once
|
||||||
|
mutex sync.RWMutex
|
||||||
|
topics []string
|
||||||
|
+ topicSet map[string]struct{}
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
179
defects/drone-0001/test/Drone0001Test.java
Normal file
179
defects/drone-0001/test/Drone0001Test.java
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for drone-0001: CWE-407 O(S*T) linear topic scan in InMemory pubsub Publish.
|
||||||
|
*
|
||||||
|
* Location: pubsub/inmem.go InMemory.Publish() and inMemorySubscriber.Subscribe()
|
||||||
|
*
|
||||||
|
* Defect: for each subscriber (S), slices.Contains(sub.topics, topic) performs
|
||||||
|
* an O(T) linear scan over topics. With S subscribers each holding T topics, one
|
||||||
|
* Publish call costs O(S*T). Subscribe() and Unsubscribe() also use
|
||||||
|
* slices.Contains inside their own loops.
|
||||||
|
*
|
||||||
|
* In a busy Drone CI instance running many concurrent SSE streams and
|
||||||
|
* pipeline-event notifications this creates quadratic work per event published.
|
||||||
|
*
|
||||||
|
* Fix: maintain a map[string]struct{} topicSet alongside the topics slice.
|
||||||
|
* hasTopic() becomes O(1) map lookup. Publish drops from O(S*T) to O(S).
|
||||||
|
*
|
||||||
|
* Speedup at S=500 subscribers, T=20 topics: 500*20=10000 ops vs 500 ops = 20x.
|
||||||
|
*
|
||||||
|
* Compile and run (no build tool required):
|
||||||
|
* javac defects/drone-0001/test/Drone0001Test.java -d /tmp/drone-0001
|
||||||
|
* java -cp /tmp/drone-0001 Drone0001Test
|
||||||
|
*/
|
||||||
|
public class Drone0001Test {
|
||||||
|
|
||||||
|
private static int passed = 0;
|
||||||
|
private static int failed = 0;
|
||||||
|
|
||||||
|
// --- Defective subscriber: slice-based topic membership ---
|
||||||
|
|
||||||
|
static class SubscriberDefective {
|
||||||
|
final List<String> topics = new ArrayList<>();
|
||||||
|
|
||||||
|
void addTopic(String t) {
|
||||||
|
if (!topics.contains(t)) topics.add(t); // O(T) scan
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasTopic(String t) {
|
||||||
|
return topics.contains(t); // O(T) scan — defect site
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fixed subscriber: map-based topic membership ---
|
||||||
|
|
||||||
|
static class SubscriberFixed {
|
||||||
|
final Set<String> topicSet = new HashSet<>();
|
||||||
|
|
||||||
|
void addTopic(String t) {
|
||||||
|
topicSet.add(t); // O(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasTopic(String t) {
|
||||||
|
return topicSet.contains(t); // O(1) — fix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts total element comparisons when Publish scans S subscribers,
|
||||||
|
* each with T topics, and the publish topic is NOT present (worst case).
|
||||||
|
*/
|
||||||
|
static long publishCostDefective(int S, int T, String publishTopic) {
|
||||||
|
List<SubscriberDefective> subs = new ArrayList<>();
|
||||||
|
for (int s = 0; s < S; s++) {
|
||||||
|
SubscriberDefective sub = new SubscriberDefective();
|
||||||
|
for (int t = 0; t < T; t++) sub.addTopic("t" + s + "." + t);
|
||||||
|
subs.add(sub);
|
||||||
|
}
|
||||||
|
long ops = 0;
|
||||||
|
for (SubscriberDefective sub : subs) {
|
||||||
|
for (int i = 0; i < sub.topics.size(); i++) {
|
||||||
|
ops++;
|
||||||
|
if (sub.topics.get(i).equals(publishTopic)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
static long publishCostFixed(int S, int T, String publishTopic) {
|
||||||
|
List<SubscriberFixed> subs = new ArrayList<>();
|
||||||
|
for (int s = 0; s < S; s++) {
|
||||||
|
SubscriberFixed sub = new SubscriberFixed();
|
||||||
|
for (int t = 0; t < T; t++) sub.addTopic("t" + s + "." + t);
|
||||||
|
subs.add(sub);
|
||||||
|
}
|
||||||
|
// one O(1) hash lookup per subscriber
|
||||||
|
long ops = 0;
|
||||||
|
for (SubscriberFixed sub : subs) {
|
||||||
|
sub.hasTopic(publishTopic);
|
||||||
|
ops++;
|
||||||
|
}
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
static void testDefectiveQuadraticCost() {
|
||||||
|
long cost100 = publishCostDefective(100, 20, "absent");
|
||||||
|
long cost200 = publishCostDefective(200, 20, "absent");
|
||||||
|
// doubling S doubles ops (O(S*T) with fixed T)
|
||||||
|
check("defective cost at S=100,T=20 is exactly 2000",
|
||||||
|
cost100 == 100L * 20);
|
||||||
|
check("defective cost doubles when S doubles (O(S*T))",
|
||||||
|
cost200 == cost100 * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFixedLinearCost() {
|
||||||
|
long cost100 = publishCostFixed(100, 20, "absent");
|
||||||
|
long cost200 = publishCostFixed(200, 20, "absent");
|
||||||
|
check("fixed cost at S=100 is exactly 100 (one lookup per subscriber)",
|
||||||
|
cost100 == 100);
|
||||||
|
check("fixed cost at S=200 is exactly 200",
|
||||||
|
cost200 == 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testSpeedupRatioAtScale() {
|
||||||
|
int S = 500, T = 20;
|
||||||
|
long defCost = publishCostDefective(S, T, "none");
|
||||||
|
long fixCost = publishCostFixed(S, T, "none");
|
||||||
|
double ratio = (double) defCost / fixCost;
|
||||||
|
check("speedup at S=500,T=20 is at least 15x (expect ~20x)",
|
||||||
|
ratio >= 15.0);
|
||||||
|
System.out.printf(" drone-0001 publish cost: defective=%d fixed=%d ratio=%.1fx%n",
|
||||||
|
defCost, fixCost, ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFixedSubscriberDeduplicates() {
|
||||||
|
SubscriberFixed sub = new SubscriberFixed();
|
||||||
|
sub.addTopic("events:pipeline");
|
||||||
|
sub.addTopic("events:pipeline"); // duplicate
|
||||||
|
sub.addTopic("events:repo");
|
||||||
|
check("set-based subscriber deduplicates topics",
|
||||||
|
sub.topicSet.size() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFixedHasTopicCorrect() {
|
||||||
|
SubscriberFixed sub = new SubscriberFixed();
|
||||||
|
sub.addTopic("events:pipeline:exec");
|
||||||
|
check("hasTopic returns true for subscribed topic",
|
||||||
|
sub.hasTopic("events:pipeline:exec"));
|
||||||
|
check("hasTopic returns false for unsubscribed topic",
|
||||||
|
!sub.hasTopic("events:repo:push"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDefectiveHasTopicCorrect() {
|
||||||
|
SubscriberDefective sub = new SubscriberDefective();
|
||||||
|
sub.addTopic("events:pipeline:exec");
|
||||||
|
check("defective hasTopic returns true for subscribed topic",
|
||||||
|
sub.hasTopic("events:pipeline:exec"));
|
||||||
|
check("defective hasTopic returns false for unsubscribed topic",
|
||||||
|
!sub.hasTopic("events:repo:push"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Harness ---
|
||||||
|
|
||||||
|
static void check(String desc, boolean cond) {
|
||||||
|
if (cond) {
|
||||||
|
System.out.println(" PASS: " + desc);
|
||||||
|
passed++;
|
||||||
|
} else {
|
||||||
|
System.out.println(" FAIL: " + desc);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("=== Drone0001Test (CWE-407 pubsub Publish O(S*T) topic scan) ===\n");
|
||||||
|
|
||||||
|
testDefectiveQuadraticCost();
|
||||||
|
testFixedLinearCost();
|
||||||
|
testSpeedupRatioAtScale();
|
||||||
|
testFixedSubscriberDeduplicates();
|
||||||
|
testFixedHasTopicCorrect();
|
||||||
|
testDefectiveHasTopicCorrect();
|
||||||
|
|
||||||
|
System.out.println("\n--- " + passed + " passed, " + failed + " failed ---");
|
||||||
|
if (failed > 0) System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
defects/drone-0002/patch/drone-0002.patch
Normal file
81
defects/drone-0002/patch/drone-0002.patch
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
--- a/cache/ttl_cache.go
|
||||||
|
+++ b/cache/ttl_cache.go
|
||||||
|
@@ -14,6 +14,7 @@ import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
+ "sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
@@ -25,6 +26,7 @@ type TTLCache[K comparable, V any] struct {
|
||||||
|
mx sync.RWMutex
|
||||||
|
cache map[K]cacheEntry[V]
|
||||||
|
purgeStop chan struct{}
|
||||||
|
+ group singleflightGroup[K, V]
|
||||||
|
getter Getter[K, V]
|
||||||
|
maxAge time.Duration
|
||||||
|
countHit atomic.Int64
|
||||||
|
@@ -200,13 +202,27 @@ func (c *TTLCache[K, V]) Get(ctx context.Context, key K) (V, error) {
|
||||||
|
item, ok := c.fetch(key, now)
|
||||||
|
if ok {
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
- item, err := c.getter.Find(ctx, key)
|
||||||
|
+ // Use singleflight to prevent thundering herd: concurrent goroutines that
|
||||||
|
+ // miss the cache for the same key share a single backend fetch instead of
|
||||||
|
+ // each issuing their own database call.
|
||||||
|
+ item, err, _ := c.group.Do(key, func() (V, error) {
|
||||||
|
+ return c.getter.Find(ctx, key)
|
||||||
|
+ })
|
||||||
|
if err != nil {
|
||||||
|
return nothing, fmt.Errorf("cache: failed to find one: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mx.Lock()
|
||||||
|
c.cache[key] = cacheEntry[V]{
|
||||||
|
added: now,
|
||||||
|
data: item,
|
||||||
|
}
|
||||||
|
c.mx.Unlock()
|
||||||
|
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
+// singleflightGroup is a minimal generic singleflight implementation.
|
||||||
|
+type singleflightGroup[K comparable, V any] struct {
|
||||||
|
+ mu sync.Mutex
|
||||||
|
+ m map[K]*call[V]
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+type call[V any] struct {
|
||||||
|
+ wg sync.WaitGroup
|
||||||
|
+ val V
|
||||||
|
+ err error
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+func (g *singleflightGroup[K, V]) Do(key K, fn func() (V, error)) (V, error, bool) {
|
||||||
|
+ g.mu.Lock()
|
||||||
|
+ if g.m == nil {
|
||||||
|
+ g.m = make(map[K]*call[V])
|
||||||
|
+ }
|
||||||
|
+ if c, ok := g.m[key]; ok {
|
||||||
|
+ g.mu.Unlock()
|
||||||
|
+ c.wg.Wait()
|
||||||
|
+ return c.val, c.err, true
|
||||||
|
+ }
|
||||||
|
+ c := &call[V]{}
|
||||||
|
+ c.wg.Add(1)
|
||||||
|
+ g.m[key] = c
|
||||||
|
+ g.mu.Unlock()
|
||||||
|
+
|
||||||
|
+ c.val, c.err = fn()
|
||||||
|
+ c.wg.Done()
|
||||||
|
+
|
||||||
|
+ g.mu.Lock()
|
||||||
|
+ delete(g.m, key)
|
||||||
|
+ g.mu.Unlock()
|
||||||
|
+
|
||||||
|
+ return c.val, c.err, false
|
||||||
|
+}
|
||||||
200
defects/drone-0002/test/Drone0002Test.java
Normal file
200
defects/drone-0002/test/Drone0002Test.java
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.concurrent.atomic.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for drone-0002: MOAD-0005 Thundering Herd in cache/ttl_cache.go TTLCache.Get().
|
||||||
|
*
|
||||||
|
* Location: cache/ttl_cache.go TTLCache.Get()
|
||||||
|
*
|
||||||
|
* Defect pattern (CWE-362 cache stampede):
|
||||||
|
* 1. fetch(key) — acquires RLock, returns cache miss
|
||||||
|
* 2. getter.Find(ctx, key) — called with NO lock held
|
||||||
|
* 3. mx.Lock(); cache[key] = item; mx.Unlock()
|
||||||
|
*
|
||||||
|
* Under concurrent load: C goroutines all see a cache miss simultaneously for
|
||||||
|
* the same key, all call getter.Find() independently, all issue C database
|
||||||
|
* round-trips for the same record. A thundering herd of DB queries results.
|
||||||
|
*
|
||||||
|
* Fix: wrap getter.Find() in a singleflight group keyed by the cache key.
|
||||||
|
* Only the first goroutine issues the DB call; all others share its result.
|
||||||
|
* Backend calls drop from C to 1 for any C concurrent misses on the same key.
|
||||||
|
*
|
||||||
|
* Compile and run (no build tool required):
|
||||||
|
* javac defects/drone-0002/test/Drone0002Test.java -d /tmp/drone-0002
|
||||||
|
* java -cp /tmp/drone-0002 Drone0002Test
|
||||||
|
*/
|
||||||
|
public class Drone0002Test {
|
||||||
|
|
||||||
|
private static int passed = 0;
|
||||||
|
private static int failed = 0;
|
||||||
|
|
||||||
|
// --- Fake backend store with call counter ---
|
||||||
|
|
||||||
|
static class FakeStore {
|
||||||
|
final AtomicInteger callCount = new AtomicInteger(0);
|
||||||
|
final long latencyMs;
|
||||||
|
|
||||||
|
FakeStore(long latencyMs) { this.latencyMs = latencyMs; }
|
||||||
|
|
||||||
|
String find(String key) throws InterruptedException {
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
Thread.sleep(latencyMs);
|
||||||
|
return "value-for-" + key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Defective cache: no singleflight ---
|
||||||
|
|
||||||
|
static class CacheDefective {
|
||||||
|
final Map<String, String> cache = new ConcurrentHashMap<>();
|
||||||
|
final FakeStore store;
|
||||||
|
|
||||||
|
CacheDefective(FakeStore s) { this.store = s; }
|
||||||
|
|
||||||
|
String get(String key) throws Exception {
|
||||||
|
String v = cache.get(key);
|
||||||
|
if (v != null) return v;
|
||||||
|
// thundering herd: multiple threads all reach here simultaneously
|
||||||
|
v = store.find(key);
|
||||||
|
cache.put(key, v);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fixed cache: singleflight coalesces concurrent misses ---
|
||||||
|
|
||||||
|
static class CacheFixed {
|
||||||
|
final Map<String, String> cache = new ConcurrentHashMap<>();
|
||||||
|
final FakeStore store;
|
||||||
|
final ConcurrentHashMap<String, CompletableFuture<String>> inflight = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
CacheFixed(FakeStore s) { this.store = s; }
|
||||||
|
|
||||||
|
String get(String key) throws Exception {
|
||||||
|
String v = cache.get(key);
|
||||||
|
if (v != null) return v;
|
||||||
|
|
||||||
|
CompletableFuture<String> mine = new CompletableFuture<>();
|
||||||
|
CompletableFuture<String> existing = inflight.putIfAbsent(key, mine);
|
||||||
|
if (existing != null) return existing.get(); // wait for in-flight call
|
||||||
|
|
||||||
|
try {
|
||||||
|
v = store.find(key);
|
||||||
|
cache.put(key, v);
|
||||||
|
mine.complete(v);
|
||||||
|
return v;
|
||||||
|
} catch (Exception e) {
|
||||||
|
mine.completeExceptionally(e);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
inflight.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
static int countBackendCalls(boolean useFixed, int concurrent, long latencyMs)
|
||||||
|
throws Exception {
|
||||||
|
FakeStore store = new FakeStore(latencyMs);
|
||||||
|
Object cache = useFixed ? new CacheFixed(store) : new CacheDefective(store);
|
||||||
|
|
||||||
|
CountDownLatch ready = new CountDownLatch(concurrent);
|
||||||
|
CountDownLatch start = new CountDownLatch(1);
|
||||||
|
CountDownLatch done = new CountDownLatch(concurrent);
|
||||||
|
ExecutorService pool = Executors.newFixedThreadPool(concurrent);
|
||||||
|
|
||||||
|
for (int i = 0; i < concurrent; i++) {
|
||||||
|
pool.submit(() -> {
|
||||||
|
ready.countDown();
|
||||||
|
try {
|
||||||
|
start.await();
|
||||||
|
if (useFixed) ((CacheFixed) cache).get("mykey");
|
||||||
|
else ((CacheDefective) cache).get("mykey");
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
} finally { done.countDown(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ready.await(5, TimeUnit.SECONDS); // all threads waiting at start gate
|
||||||
|
start.countDown(); // release simultaneously
|
||||||
|
done.await(10, TimeUnit.SECONDS);
|
||||||
|
pool.shutdown();
|
||||||
|
return store.callCount.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
static void testDefectiveTriggersManyBackendCalls() throws Exception {
|
||||||
|
int concurrent = 20;
|
||||||
|
int calls = countBackendCalls(false, concurrent, 30);
|
||||||
|
check("defective cache triggers >1 backend calls (thundering herd confirmed)",
|
||||||
|
calls > 1);
|
||||||
|
System.out.printf(" drone-0002 defective backend calls at C=%d: %d (expect ~%d)%n",
|
||||||
|
concurrent, calls, concurrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFixedTriggersExactlyOneBackendCall() throws Exception {
|
||||||
|
int concurrent = 20;
|
||||||
|
int calls = countBackendCalls(true, concurrent, 30);
|
||||||
|
check("fixed cache triggers exactly 1 backend call (singleflight working)",
|
||||||
|
calls == 1);
|
||||||
|
System.out.printf(" drone-0002 fixed backend calls at C=%d: %d (expect 1)%n",
|
||||||
|
concurrent, calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFixedReturnCorrectValue() throws Exception {
|
||||||
|
FakeStore store = new FakeStore(5);
|
||||||
|
CacheFixed cache = new CacheFixed(store);
|
||||||
|
String result = cache.get("mykey");
|
||||||
|
check("fixed cache returns correct value", "value-for-mykey".equals(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFixedSecondCallHitsCache() throws Exception {
|
||||||
|
FakeStore store = new FakeStore(0);
|
||||||
|
CacheFixed cache = new CacheFixed(store);
|
||||||
|
cache.get("k1");
|
||||||
|
int afterFirst = store.callCount.get();
|
||||||
|
cache.get("k1"); // second call — must hit cache
|
||||||
|
int afterSecond = store.callCount.get();
|
||||||
|
check("second Get on same key hits cache (no backend call)",
|
||||||
|
afterFirst == afterSecond);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testSpeedupRatioAtConcurrency20() throws Exception {
|
||||||
|
int concurrent = 20;
|
||||||
|
int defCalls = countBackendCalls(false, concurrent, 15);
|
||||||
|
int fixCalls = countBackendCalls(true, concurrent, 15);
|
||||||
|
double ratio = (double) defCalls / Math.max(1, fixCalls);
|
||||||
|
check("fixed version reduces backend calls by at least 10x at C=20",
|
||||||
|
ratio >= 10.0);
|
||||||
|
System.out.printf(" drone-0002 thundering herd ratio at C=%d: %.1fx fewer backend calls%n",
|
||||||
|
concurrent, ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Harness ---
|
||||||
|
|
||||||
|
static void check(String desc, boolean cond) {
|
||||||
|
if (cond) {
|
||||||
|
System.out.println(" PASS: " + desc);
|
||||||
|
passed++;
|
||||||
|
} else {
|
||||||
|
System.out.println(" FAIL: " + desc);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
System.out.println("=== Drone0002Test (MOAD-0005 Thundering Herd in TTLCache.Get) ===\n");
|
||||||
|
|
||||||
|
testDefectiveTriggersManyBackendCalls();
|
||||||
|
testFixedTriggersExactlyOneBackendCall();
|
||||||
|
testFixedReturnCorrectValue();
|
||||||
|
testFixedSecondCallHitsCache();
|
||||||
|
testSpeedupRatioAtConcurrency20();
|
||||||
|
|
||||||
|
System.out.println("\n--- " + passed + " passed, " + failed + " failed ---");
|
||||||
|
if (failed > 0) System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
defects/drone/SCAN.md
Normal file
27
defects/drone/SCAN.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
## Drone CI (Harness Gitness) 5-MOAD Scan — 2026-03-31
|
||||||
|
|
||||||
|
Target: https://github.com/harness/drone (depth=1)
|
||||||
|
|
||||||
|
### MOAD-0001 (CWE-407) — 1 DEFECT FOUND
|
||||||
|
See drone-0001. pubsub/inmem.go Publish() calls slices.Contains(sub.topics, topic)
|
||||||
|
for every subscriber — O(S*T) per Publish call.
|
||||||
|
|
||||||
|
### MOAD-0002 (Intertangle) — CLEAN
|
||||||
|
Manager struct aggregates stores via dependency injection; each store interface is
|
||||||
|
independently swappable. No shared mutable global state coupling pipeline execution
|
||||||
|
to unrelated subsystems. Wire-based DI separates concerns cleanly.
|
||||||
|
|
||||||
|
### MOAD-0003 (Leaked Context) — CLEAN
|
||||||
|
app/api/request/context.go uses context.Context (standard Go pattern for
|
||||||
|
per-request identity). No goroutine-local or thread-local identity leakage found.
|
||||||
|
Pattern matches Kubernetes apiserver context design.
|
||||||
|
|
||||||
|
### MOAD-0004 (CWE-312) — CLEAN
|
||||||
|
Searched all log calls for token/secret/password/webhook literals. No credential
|
||||||
|
values passed to zerolog log lines. Secret service logs error conditions only,
|
||||||
|
never values. Webhook tokens are stored encrypted in DB, not logged.
|
||||||
|
|
||||||
|
### MOAD-0005 (Thundering Herd) — 1 DEFECT FOUND
|
||||||
|
See drone-0002. cache/ttl_cache.go TTLCache.Get() has classic cache stampede:
|
||||||
|
fetch (RLock miss) then getter.Find() with no lock, then write. Multiple goroutines
|
||||||
|
can all miss and all call DB simultaneously.
|
||||||
9
defects/esp-idf-0001/patch/esp-idf-0001.patch
Normal file
9
defects/esp-idf-0001/patch/esp-idf-0001.patch
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
--- a/components/esp_wifi/src/smartconfig.c
|
||||||
|
+++ b/components/esp_wifi/src/smartconfig.c
|
||||||
|
@@ -32,7 +32,6 @@ static void handler_got_ssid_passwd(void *arg, esp_event_base_t base, int32_t e
|
||||||
|
memcpy(password, evt->password, sizeof(evt->password));
|
||||||
|
memcpy(cellphone_ip, evt->cellphone_ip, sizeof(evt->cellphone_ip));
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "SSID:%s", ssid);
|
||||||
|
- ESP_LOGD(TAG, "PASSWORD:%s", password);
|
||||||
|
ESP_LOGD(TAG, "Phone ip: %d.%d.%d.%d", cellphone_ip[0], cellphone_ip[1], cellphone_ip[2], cellphone_ip[3]);
|
||||||
131
defects/esp-idf-0001/test/EspIdf0001Test.java
Normal file
131
defects/esp-idf-0001/test/EspIdf0001Test.java
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for esp-idf-0001: CWE-312 WiFi password logged verbatim via ESP_LOGD
|
||||||
|
* in components/esp_wifi/src/smartconfig.c handler_got_ssid_passwd().
|
||||||
|
*
|
||||||
|
* Pattern:
|
||||||
|
* ESP_LOGD(TAG, "PASSWORD:%s", password)
|
||||||
|
*
|
||||||
|
* When debug logging is enabled the WiFi PSK (pre-shared key) is emitted
|
||||||
|
* verbatim to serial/JTAG output. Any listener on those interfaces obtains
|
||||||
|
* the plaintext credential without any further effort.
|
||||||
|
*
|
||||||
|
* Fix: remove the ESP_LOGD("PASSWORD:%s", ...) line. SSID logging (non-secret)
|
||||||
|
* is acceptable; PSK logging is not.
|
||||||
|
*
|
||||||
|
* Compile and run (no build tool required):
|
||||||
|
* javac defects/esp-idf-0001/test/EspIdf0001Test.java -d /tmp/esp-idf-0001
|
||||||
|
* java -cp /tmp/esp-idf-0001 EspIdf0001Test
|
||||||
|
*/
|
||||||
|
public class EspIdf0001Test {
|
||||||
|
|
||||||
|
private static int passed = 0;
|
||||||
|
private static int failed = 0;
|
||||||
|
|
||||||
|
// --- Simulated log collector ---
|
||||||
|
|
||||||
|
static class LogCollector {
|
||||||
|
private final List<String> lines = new ArrayList<>();
|
||||||
|
|
||||||
|
void logd(String tag, String fmt, Object... args) {
|
||||||
|
lines.add(String.format(fmt, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean containsText(String text) {
|
||||||
|
for (String line : lines) {
|
||||||
|
if (line.contains(text)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size() { return lines.size(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Defective: logs password in DEBUG output ---
|
||||||
|
|
||||||
|
static void handleGotSsidPasswdDefective(String ssid, String password, LogCollector log) {
|
||||||
|
log.logd("smartconfig", "SSID:%s", ssid);
|
||||||
|
log.logd("smartconfig", "PASSWORD:%s", password); // CWE-312 site
|
||||||
|
log.logd("smartconfig", "Phone ip: 192.168.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fixed: password line removed ---
|
||||||
|
|
||||||
|
static void handleGotSsidPasswdFixed(String ssid, String password, LogCollector log) {
|
||||||
|
log.logd("smartconfig", "SSID:%s", ssid);
|
||||||
|
// PASSWORD log line removed — fix for esp-idf-0001
|
||||||
|
log.logd("smartconfig", "Phone ip: 192.168.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
static void testDefectiveLogsPassword() {
|
||||||
|
LogCollector log = new LogCollector();
|
||||||
|
String password = "MySecretWiFiPSK123";
|
||||||
|
handleGotSsidPasswdDefective("MyNet", password, log);
|
||||||
|
check("defective must log the password (confirms CWE-312 site is present)",
|
||||||
|
log.containsText(password));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFixedDoesNotLogPassword() {
|
||||||
|
LogCollector log = new LogCollector();
|
||||||
|
String password = "MySecretWiFiPSK123";
|
||||||
|
handleGotSsidPasswdFixed("MyNet", password, log);
|
||||||
|
check("fixed must NOT log the WiFi PSK in any log line",
|
||||||
|
!log.containsText(password));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFixedStillLogsSsid() {
|
||||||
|
LogCollector log = new LogCollector();
|
||||||
|
handleGotSsidPasswdFixed("HomeNetwork_5GHz", "secret", log);
|
||||||
|
check("SSID (non-sensitive) must still appear in logs after fix",
|
||||||
|
log.containsText("HomeNetwork_5GHz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFixedHasFewerLines() {
|
||||||
|
LogCollector defLog = new LogCollector();
|
||||||
|
LogCollector fixLog = new LogCollector();
|
||||||
|
handleGotSsidPasswdDefective("net", "pw", defLog);
|
||||||
|
handleGotSsidPasswdFixed("net", "pw", fixLog);
|
||||||
|
check("fixed version produces fewer log lines than defective",
|
||||||
|
fixLog.size() < defLog.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testPasswordNotLeakedAcrossMultipleCalls() {
|
||||||
|
// Simulate multiple smartconfig events — none should leak password
|
||||||
|
String[] passwords = {"PSK-alpha", "PSK-beta", "PSK-gamma"};
|
||||||
|
for (String pw : passwords) {
|
||||||
|
LogCollector log = new LogCollector();
|
||||||
|
handleGotSsidPasswdFixed("net", pw, log);
|
||||||
|
check("password '" + pw + "' must not appear in any log line",
|
||||||
|
!log.containsText(pw));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Harness ---
|
||||||
|
|
||||||
|
static void check(String desc, boolean cond) {
|
||||||
|
if (cond) {
|
||||||
|
System.out.println(" PASS: " + desc);
|
||||||
|
passed++;
|
||||||
|
} else {
|
||||||
|
System.out.println(" FAIL: " + desc);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("=== EspIdf0001Test (CWE-312 WiFi PSK logged) ===\n");
|
||||||
|
|
||||||
|
testDefectiveLogsPassword();
|
||||||
|
testFixedDoesNotLogPassword();
|
||||||
|
testFixedStillLogsSsid();
|
||||||
|
testFixedHasFewerLines();
|
||||||
|
testPasswordNotLeakedAcrossMultipleCalls();
|
||||||
|
|
||||||
|
System.out.println("\n--- " + passed + " passed, " + failed + " failed ---");
|
||||||
|
if (failed > 0) System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
defects/esp-idf-0002/patch/esp-idf-0002.patch
Normal file
8
defects/esp-idf-0002/patch/esp-idf-0002.patch
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
--- a/components/esp_http_client/lib/http_auth.c
|
||||||
|
+++ b/components/esp_http_client/lib/http_auth.c
|
||||||
|
@@ -155,7 +155,6 @@ char *http_auth_digest(const char *username, const char *password, esp_http_auth
|
||||||
|
if (digest_func(ha1, "%s:%s:%s", username, auth_data->realm, password) <= 0) {
|
||||||
|
goto _digest_exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
- ESP_LOGD(TAG, "%s %s %s %s", "Digest", username, auth_data->realm, password);
|
||||||
146
defects/esp-idf-0002/test/EspIdf0002Test.java
Normal file
146
defects/esp-idf-0002/test/EspIdf0002Test.java
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for esp-idf-0002: CWE-312 HTTP Digest auth password logged verbatim
|
||||||
|
* in components/esp_http_client/lib/http_auth.c http_auth_digest().
|
||||||
|
*
|
||||||
|
* Pattern:
|
||||||
|
* ESP_LOGD(TAG, "%s %s %s %s", "Digest", username, auth_data->realm, password)
|
||||||
|
*
|
||||||
|
* During HTTP Digest authentication setup the plaintext password is emitted
|
||||||
|
* to serial/JTAG when debug logging is active. Unlike Basic Auth the password
|
||||||
|
* should never leave the chip except as part of the one-way digest hash.
|
||||||
|
*
|
||||||
|
* Fix: remove the offending ESP_LOGD line. Logging the digest hash result
|
||||||
|
* (ha1/response) would be safe; logging the raw password is not.
|
||||||
|
*
|
||||||
|
* Compile and run (no build tool required):
|
||||||
|
* javac defects/esp-idf-0002/test/EspIdf0002Test.java -d /tmp/esp-idf-0002
|
||||||
|
* java -cp /tmp/esp-idf-0002 EspIdf0002Test
|
||||||
|
*/
|
||||||
|
public class EspIdf0002Test {
|
||||||
|
|
||||||
|
private static int passed = 0;
|
||||||
|
private static int failed = 0;
|
||||||
|
|
||||||
|
// --- Simulated log collector ---
|
||||||
|
|
||||||
|
static class LogCollector {
|
||||||
|
private final List<String> lines = new ArrayList<>();
|
||||||
|
|
||||||
|
void logd(String tag, String fmt, Object... args) {
|
||||||
|
lines.add(String.format(fmt, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean containsText(String text) {
|
||||||
|
for (String line : lines) {
|
||||||
|
if (line.contains(text)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int size() { return lines.size(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Trivial stand-in for MD5 hex — actual hash value irrelevant for this test. */
|
||||||
|
static String mockMd5(String input) {
|
||||||
|
return Integer.toHexString(input.hashCode() & 0x7fffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Defective: logs username + realm + password ---
|
||||||
|
|
||||||
|
static String computeDigestDefective(
|
||||||
|
String username, String realm, String password, LogCollector log) {
|
||||||
|
String ha1 = mockMd5(username + ":" + realm + ":" + password);
|
||||||
|
log.logd("HTTP_AUTH", "%s %s %s %s", "Digest", username, realm, password); // CWE-312
|
||||||
|
String ha2 = mockMd5("GET:/api/data");
|
||||||
|
return mockMd5(ha1 + ":nonce123:" + ha2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fixed: no password in any log statement ---
|
||||||
|
|
||||||
|
static String computeDigestFixed(
|
||||||
|
String username, String realm, String password, LogCollector log) {
|
||||||
|
String ha1 = mockMd5(username + ":" + realm + ":" + password);
|
||||||
|
// ESP_LOGD with password removed — fix for esp-idf-0002
|
||||||
|
String ha2 = mockMd5("GET:/api/data");
|
||||||
|
return mockMd5(ha1 + ":nonce123:" + ha2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
static void testDefectiveLogsPassword() {
|
||||||
|
LogCollector log = new LogCollector();
|
||||||
|
computeDigestDefective("alice", "testrealm@host.com", "hunter2", log);
|
||||||
|
check("defective must log the password (confirms CWE-312 site is present)",
|
||||||
|
log.containsText("hunter2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFixedDoesNotLogPassword() {
|
||||||
|
LogCollector log = new LogCollector();
|
||||||
|
computeDigestFixed("alice", "testrealm@host.com", "hunter2", log);
|
||||||
|
check("fixed must NOT log the HTTP auth password",
|
||||||
|
!log.containsText("hunter2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDefectiveAlsoExposesUsername() {
|
||||||
|
LogCollector log = new LogCollector();
|
||||||
|
computeDigestDefective("alice", "testrealm@host.com", "hunter2", log);
|
||||||
|
// The combined log line is the real risk (username + realm + password together)
|
||||||
|
check("defective log line includes username (combined exposure risk)",
|
||||||
|
log.containsText("alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFixedProducesNoLogLines() {
|
||||||
|
LogCollector log = new LogCollector();
|
||||||
|
computeDigestFixed("alice", "testrealm@host.com", "hunter2", log);
|
||||||
|
check("fixed digest computation emits no log lines",
|
||||||
|
log.size() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testBothReturnSameDigest() {
|
||||||
|
LogCollector log1 = new LogCollector();
|
||||||
|
LogCollector log2 = new LogCollector();
|
||||||
|
String d1 = computeDigestDefective("bob", "realm", "s3cr3t", log1);
|
||||||
|
String d2 = computeDigestFixed("bob", "realm", "s3cr3t", log2);
|
||||||
|
check("fixed and defective produce identical digest response (fix is behaviour-neutral)",
|
||||||
|
d1.equals(d2));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testPasswordNotLeakedForMultipleRealms() {
|
||||||
|
String[] realms = {"api.example.com", "admin.local", "iot-gateway"};
|
||||||
|
for (String realm : realms) {
|
||||||
|
LogCollector log = new LogCollector();
|
||||||
|
computeDigestFixed("user", realm, "TopSecretPW", log);
|
||||||
|
check("password not logged for realm '" + realm + "'",
|
||||||
|
!log.containsText("TopSecretPW"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Harness ---
|
||||||
|
|
||||||
|
static void check(String desc, boolean cond) {
|
||||||
|
if (cond) {
|
||||||
|
System.out.println(" PASS: " + desc);
|
||||||
|
passed++;
|
||||||
|
} else {
|
||||||
|
System.out.println(" FAIL: " + desc);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("=== EspIdf0002Test (CWE-312 HTTP auth password logged) ===\n");
|
||||||
|
|
||||||
|
testDefectiveLogsPassword();
|
||||||
|
testFixedDoesNotLogPassword();
|
||||||
|
testDefectiveAlsoExposesUsername();
|
||||||
|
testFixedProducesNoLogLines();
|
||||||
|
testBothReturnSameDigest();
|
||||||
|
testPasswordNotLeakedForMultipleRealms();
|
||||||
|
|
||||||
|
System.out.println("\n--- " + passed + " passed, " + failed + " failed ---");
|
||||||
|
if (failed > 0) System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
defects/esp-idf/SCAN.md
Normal file
28
defects/esp-idf/SCAN.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
## ESP-IDF 5-MOAD Scan — 2026-03-31
|
||||||
|
|
||||||
|
Target: https://github.com/espressif/esp-idf (depth=1)
|
||||||
|
|
||||||
|
### MOAD-0001 (CWE-407) — CLEAN
|
||||||
|
WiFi core (esp_wifi) is a precompiled binary blob (no C source for station list).
|
||||||
|
Bluetooth GATT utils (gatt_utils.c) iterates fixed-size arrays bounded by
|
||||||
|
GATT_MAX_APPS (max ~10 simultaneous GATT apps) and GATT_MAX_BG_CONN_DEV — not
|
||||||
|
O(N^2) at IoT scale; array sizes are compile-time constants, not user-controlled.
|
||||||
|
NVS page scan iterates ENTRY_COUNT (126 entries per page) — bounded, not unbounded.
|
||||||
|
No O(N^2) hot-path defect found.
|
||||||
|
|
||||||
|
### MOAD-0002 (Intertangle) — CLEAN
|
||||||
|
Components are cleanly decoupled: esp_wifi, bt, nvs_flash, esp-tls, esp_http_client
|
||||||
|
each have their own init/deinit. No shared mutable god object found coupling
|
||||||
|
independent subsystems.
|
||||||
|
|
||||||
|
### MOAD-0003 (Leaked Context) — CLEAN
|
||||||
|
ESP-IDF is C-based, uses FreeRTOS tasks. No thread-local storage API misuse found.
|
||||||
|
pvTaskGetThreadLocalStoragePointer usage in components is scoped to task setup,
|
||||||
|
not request-identity leakage.
|
||||||
|
|
||||||
|
### MOAD-0004 (CWE-312) — 2 DEFECTS FOUND
|
||||||
|
See esp-idf-0001 and esp-idf-0002.
|
||||||
|
|
||||||
|
### MOAD-0005 (Thundering Herd) — CLEAN
|
||||||
|
NVS uses NVSHandleLocked with Lock RAII guard on every read/write operation.
|
||||||
|
WiFi event loop is single-threaded event dispatch. No concurrent cache stampede found.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue