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