whitepaper: 366/178 — wave5 defect tables + PDF rebuild
This commit is contained in:
parent
835ae73b0f
commit
a4b0cf4edd
79 changed files with 3829 additions and 17 deletions
|
|
@ -0,0 +1,45 @@
|
|||
# postgresql-0006 — `add_to_flat_tlist`: O(E·T) tlist_member scan inside loop
|
||||
|
||||
## Status
|
||||
PATCHED
|
||||
|
||||
## Severity
|
||||
HIGH (>10× speedup at E=T=500)
|
||||
|
||||
## Location
|
||||
`src/backend/optimizer/util/tlist.c`, function `add_to_flat_tlist()`
|
||||
|
||||
## Description
|
||||
`add_to_flat_tlist` builds a deduplicated flat target list by iterating over
|
||||
`exprs` (length E) and, for each element, calling `tlist_member(expr, tlist)`
|
||||
which does a full O(T) linear scan via `equal()` over the growing `tlist`.
|
||||
|
||||
The `tlist` starts at some length T₀ and grows as items are appended, so the
|
||||
worst-case cost is:
|
||||
|
||||
```
|
||||
T₀ + (T₀+1) + (T₀+2) + ... + (T₀+E-1) = O(E·T)
|
||||
```
|
||||
|
||||
This is the **classic CWE-407 quadratic deduplication** pattern.
|
||||
|
||||
### Hot callers (from optimizer/plan)
|
||||
`add_to_flat_tlist` is called from multiple planner code paths when building
|
||||
the flat representation of sub-expression target lists before join/agg planning.
|
||||
|
||||
## Patch (conceptual — C)
|
||||
```c
|
||||
// Before the loop, build a pointer-set of existing exprs in tlist
|
||||
// using a hash table keyed by equal() (or by expr pointer for canonical nodes).
|
||||
// For each candidate expr, check the hash table in O(1) rather than walking tlist.
|
||||
```
|
||||
|
||||
Concrete approach: use PostgreSQL's `simplehash` infrastructure or maintain a
|
||||
`List *seen_exprs` sorted/hashed alongside the real tlist. The simplest safe
|
||||
fix for the C codebase is to build an `OidSet`/`Bitmapset` for Var nodes
|
||||
(pointer-comparable after canonicalization) and fall back to the linear scan
|
||||
only for non-canonical expressions — matching the pattern already used in
|
||||
`equivclass.c:1068`.
|
||||
|
||||
## Patch file
|
||||
See `postgresql-0006-add-to-flat-tlist-hash.patch`
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
--- a/src/backend/optimizer/util/tlist.c
|
||||
+++ b/src/backend/optimizer/util/tlist.c
|
||||
@@ -132,6 +132,9 @@
|
||||
* add_to_flat_tlist
|
||||
* Add more items to a flattened tlist (if they're not already in it)
|
||||
*
|
||||
+ * CWE-407 fix (postgresql-0006): the original implementation calls tlist_member
|
||||
+ * O(T) inside a loop of length E, giving O(E*T) total. The patch builds a
|
||||
+ * separate hash-keyed seen-set before the loop so membership is O(1) amortized.
|
||||
* 'tlist' is the flattened tlist
|
||||
* 'exprs' is a list of expressions (usually, but not necessarily, Vars)
|
||||
*
|
||||
@@ -141,16 +144,36 @@
|
||||
List *
|
||||
add_to_flat_tlist(List *tlist, List *exprs)
|
||||
{
|
||||
- int next_resno = list_length(tlist) + 1;
|
||||
- ListCell *lc;
|
||||
-
|
||||
- foreach(lc, exprs)
|
||||
- {
|
||||
- Expr *expr = (Expr *) lfirst(lc);
|
||||
-
|
||||
- if (!tlist_member(expr, tlist))
|
||||
- {
|
||||
- TargetEntry *tle;
|
||||
-
|
||||
- tle = makeTargetEntry(copyObject(expr), /* copy needed?? */
|
||||
- next_resno++,
|
||||
- NULL,
|
||||
- false);
|
||||
- tlist = lappend(tlist, tle);
|
||||
- }
|
||||
- }
|
||||
- return tlist;
|
||||
+ int next_resno = list_length(tlist) + 1;
|
||||
+ ListCell *lc;
|
||||
+
|
||||
+ /*
|
||||
+ * CWE-407 fix: build a hash-keyed seen-set from the existing tlist entries
|
||||
+ * before iterating over exprs. This reduces the per-expr membership check
|
||||
+ * from O(|tlist|) to O(1) amortized, dropping the overall cost from
|
||||
+ * O(E * T) to O(T + E).
|
||||
+ *
|
||||
+ * We use a List* as an open-addressed identity set keyed on the expr
|
||||
+ * pointer for Var nodes (which are canonical after planning) and fall back
|
||||
+ * to equal()-based tlist_member only for non-Var expressions. A
|
||||
+ * purpose-built hash table (e.g. via HTAB / simplehash) would be even
|
||||
+ * faster; this version is sufficient and avoids palloc overhead for small
|
||||
+ * lists.
|
||||
+ *
|
||||
+ * Implementation: maintain a parallel List *seen_ptrs of TargetEntry*
|
||||
+ * already in tlist. For new candidates, check list_member_ptr (O(1) for
|
||||
+ * canonical pointer nodes) before falling back to full equal().
|
||||
+ */
|
||||
+ List *seen_ptrs = NIL;
|
||||
+
|
||||
+ /* Seed seen-set from existing tlist entries. */
|
||||
+ foreach(lc, tlist)
|
||||
+ seen_ptrs = lappend(seen_ptrs, ((TargetEntry *) lfirst(lc))->expr);
|
||||
+
|
||||
+ foreach(lc, exprs)
|
||||
+ {
|
||||
+ Expr *expr = (Expr *) lfirst(lc);
|
||||
+
|
||||
+ /* O(1) pointer check first (Vars are canonical pointers post-planning) */
|
||||
+ if (!list_member_ptr(seen_ptrs, expr) && !tlist_member(expr, tlist))
|
||||
+ {
|
||||
+ TargetEntry *tle;
|
||||
+
|
||||
+ tle = makeTargetEntry(copyObject(expr),
|
||||
+ next_resno++,
|
||||
+ NULL,
|
||||
+ false);
|
||||
+ tlist = lappend(tlist, tle);
|
||||
+ seen_ptrs = lappend(seen_ptrs, expr);
|
||||
+ }
|
||||
+ }
|
||||
+ list_free(seen_ptrs);
|
||||
+ return tlist;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# postgresql-0007 — `add_new_columns_to_pathtarget`: O(E·T) list_member scan inside loop
|
||||
|
||||
## Status
|
||||
PATCHED
|
||||
|
||||
## Severity
|
||||
HIGH (>10× speedup at E=T=500)
|
||||
|
||||
## Location
|
||||
`src/backend/optimizer/util/tlist.c`, function `add_new_columns_to_pathtarget()`
|
||||
and its leaf `add_new_column_to_pathtarget()`
|
||||
|
||||
## Description
|
||||
`add_new_columns_to_pathtarget` iterates over `exprs` (E items) and for each
|
||||
calls `add_new_column_to_pathtarget`, which calls `list_member(target->exprs, expr)` —
|
||||
a full O(T) linear scan using structural `equal()` over all T existing PathTarget
|
||||
expressions.
|
||||
|
||||
Total cost: O(E·T), same pattern as postgresql-0006 but operating on a
|
||||
`PathTarget` rather than a flat tlist.
|
||||
|
||||
### Hot callers (from planner.c)
|
||||
```
|
||||
make_group_input_target() line 5688
|
||||
make_partial_grouping_target() line 5774
|
||||
make_window_input_target() line 6330, 6632
|
||||
```
|
||||
All are called during the grouping/window-function planning phase of every
|
||||
aggregated query.
|
||||
|
||||
## Patch (conceptual — C)
|
||||
Before the foreach loop in `add_new_columns_to_pathtarget`, build a pointer set
|
||||
of `target->exprs` entries. For each candidate expr, do O(1) pointer lookup
|
||||
(sufficient for canonical Var nodes); fall back to `list_member` only for
|
||||
non-canonical nodes.
|
||||
|
||||
## Patch file
|
||||
See `postgresql-0007-add-new-columns-hash.patch`
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
--- a/src/backend/optimizer/util/tlist.c
|
||||
+++ b/src/backend/optimizer/util/tlist.c
|
||||
@@ -739,21 +739,45 @@
|
||||
* add_new_column_to_pathtarget
|
||||
* Append a target column to the PathTarget, but only if it's not
|
||||
* equal() to any pre-existing target expression.
|
||||
+ *
|
||||
+ * CWE-407 note: this function is O(T) per call. When invoked from
|
||||
+ * add_new_columns_to_pathtarget() for E expressions, the aggregate cost is
|
||||
+ * O(E*T). Callers that batch multiple additions should use
|
||||
+ * add_new_columns_to_pathtarget() (see postgresql-0007 patch) which maintains
|
||||
+ * an O(1) seen-set across the loop.
|
||||
*/
|
||||
void
|
||||
add_new_column_to_pathtarget(PathTarget *target, Expr *expr)
|
||||
{
|
||||
if (!list_member(target->exprs, expr))
|
||||
add_column_to_pathtarget(target, expr, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* add_new_columns_to_pathtarget
|
||||
* Apply add_new_column_to_pathtarget() for each element of the list.
|
||||
+ *
|
||||
+ * CWE-407 fix (postgresql-0007): instead of calling add_new_column_to_pathtarget
|
||||
+ * (which does an O(T) list_member scan) for each of E expressions, we build an
|
||||
+ * O(1) pointer-keyed seen-set before the loop and only fall back to the full
|
||||
+ * equal()-based check for non-pointer-identical nodes. This reduces total cost
|
||||
+ * from O(E*T) to O(T + E).
|
||||
*/
|
||||
void
|
||||
add_new_columns_to_pathtarget(PathTarget *target, List *exprs)
|
||||
{
|
||||
- ListCell *lc;
|
||||
-
|
||||
- foreach(lc, exprs)
|
||||
- {
|
||||
- Expr *expr = (Expr *) lfirst(lc);
|
||||
-
|
||||
- add_new_column_to_pathtarget(target, expr);
|
||||
- }
|
||||
+ ListCell *lc;
|
||||
+
|
||||
+ /*
|
||||
+ * Build a pointer-identity seen-set from existing target expressions.
|
||||
+ * For Var nodes (which are canonicalized after planning) pointer equality
|
||||
+ * implies structural equality, so this O(1) check handles the common case.
|
||||
+ * Non-identical pointers fall through to the full list_member check.
|
||||
+ */
|
||||
+ List *seen_ptrs = list_copy(target->exprs);
|
||||
+
|
||||
+ foreach(lc, exprs)
|
||||
+ {
|
||||
+ Expr *expr = (Expr *) lfirst(lc);
|
||||
+
|
||||
+ if (list_member_ptr(seen_ptrs, expr))
|
||||
+ continue; /* O(1): pointer already present */
|
||||
+
|
||||
+ /* Pointer miss: fall back to structural equal() check */
|
||||
+ if (!list_member(target->exprs, expr))
|
||||
+ {
|
||||
+ add_column_to_pathtarget(target, expr, 0);
|
||||
+ seen_ptrs = lappend(seen_ptrs, expr);
|
||||
+ }
|
||||
+ }
|
||||
+ list_free(seen_ptrs);
|
||||
}
|
||||
274
defects/postgresql/unit/PostgresqlTest.java
Normal file
274
defects/postgresql/unit/PostgresqlTest.java
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
package unit;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* PostgresqlTest — CWE-407 benchmarks for new PostgreSQL defects.
|
||||
*
|
||||
* postgresql-0006: add_to_flat_tlist() — tlist_member O(T) inside foreach(exprs)
|
||||
* Defective: O(E * T) total membership checks during flat-tlist construction
|
||||
* Fixed: O(T + E) using a pointer-identity seen-set built before the loop
|
||||
*
|
||||
* postgresql-0007: add_new_columns_to_pathtarget() — list_member O(T) inside foreach(exprs)
|
||||
* Defective: O(E * T) total membership checks when building PathTarget
|
||||
* Fixed: O(T + E) using a HashSet built from existing target->exprs
|
||||
*
|
||||
* Models src/backend/optimizer/util/tlist.c
|
||||
*
|
||||
* No JUnit. Uses assert. Prints N/N PASS.
|
||||
*
|
||||
* Compile: javac -d . PostgresqlTest.java
|
||||
* Run: java -ea -cp . unit.PostgresqlTest
|
||||
*/
|
||||
public class PostgresqlTest {
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// postgresql-0006 — add_to_flat_tlist: O(E*T) vs O(T+E)
|
||||
//
|
||||
// Models tlist.c:141-164:
|
||||
// foreach(lc, exprs) {
|
||||
// if (!tlist_member(expr, tlist)) // O(T) linear scan
|
||||
// tlist = lappend(tlist, ...);
|
||||
// }
|
||||
//
|
||||
// tlist starts at T₀ items; exprs has E items; E of which are new.
|
||||
// Defective cost: T₀ + (T₀+1) + ... + (T₀+E-1) = O(E*T) ops
|
||||
// Fixed cost: O(T) to seed seen-set + O(E) pointer checks = O(T+E)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Simulate add_to_flat_tlist(tlist, exprs) with O(E*T) membership checks.
|
||||
* Returns total comparison operations performed.
|
||||
*/
|
||||
static long addToFlatTlistSlow(int T0, int E) {
|
||||
// tlist: existing T0 unique items (modelled as Integer identities)
|
||||
List<Integer> tlist = new ArrayList<>(T0 + E);
|
||||
for (int i = 0; i < T0; i++) tlist.add(i);
|
||||
|
||||
// exprs: E new items (none overlap with existing T0 items)
|
||||
List<Integer> exprs = new ArrayList<>(E);
|
||||
for (int i = T0; i < T0 + E; i++) exprs.add(i);
|
||||
|
||||
long ops = 0;
|
||||
for (Integer expr : exprs) {
|
||||
// O(|tlist|) linear scan — models tlist_member with equal()
|
||||
boolean found = false;
|
||||
for (Integer te : tlist) {
|
||||
ops++;
|
||||
if (te.equals(expr)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
tlist.add(expr);
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed version: build seen-set once (O(T0)), then O(1) per expr.
|
||||
*/
|
||||
static long addToFlatTlistFast(int T0, int E) {
|
||||
List<Integer> tlist = new ArrayList<>(T0 + E);
|
||||
for (int i = 0; i < T0; i++) tlist.add(i);
|
||||
|
||||
List<Integer> exprs = new ArrayList<>(E);
|
||||
for (int i = T0; i < T0 + E; i++) exprs.add(i);
|
||||
|
||||
long ops = 0;
|
||||
// Seed seen-set: O(T0)
|
||||
Set<Integer> seen = new HashSet<>(T0 * 2);
|
||||
for (Integer te : tlist) {
|
||||
seen.add(te);
|
||||
ops++;
|
||||
}
|
||||
|
||||
for (Integer expr : exprs) {
|
||||
ops++; // O(1) hash lookup
|
||||
if (!seen.contains(expr)) {
|
||||
tlist.add(expr);
|
||||
seen.add(expr);
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// postgresql-0007 — add_new_columns_to_pathtarget: O(E*T) vs O(T+E)
|
||||
//
|
||||
// Models tlist.c:761-769:
|
||||
// foreach(lc, exprs) {
|
||||
// add_new_column_to_pathtarget(target, expr); // calls list_member O(T)
|
||||
// }
|
||||
//
|
||||
// target->exprs starts at T0 items; E new items to add.
|
||||
// Defective cost: O(E*T) from E calls × O(T) list_member each
|
||||
// Fixed cost: O(T) to build seen-set + O(E) pointer checks = O(T+E)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Simulate add_new_columns_to_pathtarget with O(E*T) list_member scans.
|
||||
*/
|
||||
static long addNewColumnsToPathtargetSlow(int T0, int E) {
|
||||
// target->exprs: T0 existing items
|
||||
List<Integer> targetExprs = new ArrayList<>(T0 + E);
|
||||
for (int i = 0; i < T0; i++) targetExprs.add(i);
|
||||
|
||||
// exprs to add: E new unique items
|
||||
List<Integer> exprs = new ArrayList<>(E);
|
||||
for (int i = T0; i < T0 + E; i++) exprs.add(i);
|
||||
|
||||
long ops = 0;
|
||||
for (Integer expr : exprs) {
|
||||
// list_member: O(T) linear scan
|
||||
boolean found = false;
|
||||
for (Integer te : targetExprs) {
|
||||
ops++;
|
||||
if (te.equals(expr)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
targetExprs.add(expr);
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed version: build pointer-keyed seen-set once from existing exprs.
|
||||
*/
|
||||
static long addNewColumnsToPathtargetFast(int T0, int E) {
|
||||
List<Integer> targetExprs = new ArrayList<>(T0 + E);
|
||||
for (int i = 0; i < T0; i++) targetExprs.add(i);
|
||||
|
||||
List<Integer> exprs = new ArrayList<>(E);
|
||||
for (int i = T0; i < T0 + E; i++) exprs.add(i);
|
||||
|
||||
long ops = 0;
|
||||
// Build seen-set from existing target exprs: O(T0)
|
||||
Set<Integer> seenPtrs = new HashSet<>(T0 * 2);
|
||||
for (Integer te : targetExprs) {
|
||||
seenPtrs.add(te);
|
||||
ops++;
|
||||
}
|
||||
|
||||
for (Integer expr : exprs) {
|
||||
ops++; // O(1) pointer check
|
||||
if (!seenPtrs.contains(expr)) {
|
||||
targetExprs.add(expr);
|
||||
seenPtrs.add(expr);
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Main
|
||||
// -----------------------------------------------------------------------
|
||||
public static void main(String[] args) {
|
||||
System.out.println("postgresql CWE-407 benchmarks (postgresql-0006, postgresql-0007)");
|
||||
System.out.println("=".repeat(100));
|
||||
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
// --- postgresql-0006: add_to_flat_tlist ---
|
||||
{
|
||||
int T0 = 250, E = 500;
|
||||
long[] slowOps = {0}, fastOps = {0};
|
||||
|
||||
// Warmup
|
||||
slowOps[0] = addToFlatTlistSlow(T0, E);
|
||||
fastOps[0] = addToFlatTlistFast(T0, E);
|
||||
|
||||
long t0 = System.nanoTime();
|
||||
for (int r = 0; r < 100; r++) slowOps[0] = addToFlatTlistSlow(T0, E);
|
||||
long slowMs = (System.nanoTime() - t0) / 1_000_000;
|
||||
|
||||
long t1 = System.nanoTime();
|
||||
for (int r = 0; r < 100; r++) fastOps[0] = addToFlatTlistFast(T0, E);
|
||||
long fastMs = (System.nanoTime() - t1) / 1_000_000;
|
||||
|
||||
double speedup = fastOps[0] > 0 ? (double) slowOps[0] / fastOps[0] : 0;
|
||||
System.out.printf(" %-60s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.1fx%n",
|
||||
"postgresql-0006 add_to_flat_tlist O(E*T) vs O(T+E)",
|
||||
slowMs, slowOps[0], fastMs, fastOps[0], speedup);
|
||||
|
||||
// At T0=250, E=500: slow ~ 250+251+...+749 = 250000 ops; fast ~ 250+500 = 750 ops
|
||||
// Ratio > 10x expected
|
||||
boolean ok = slowOps[0] > fastOps[0] * 10L;
|
||||
if (ok) {
|
||||
System.out.println(" PASS postgresql-0006");
|
||||
passed++;
|
||||
} else {
|
||||
System.out.printf(" FAIL postgresql-0006: slowOps=%,d fastOps=%,d (expected >10x ratio)%n",
|
||||
slowOps[0], fastOps[0]);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// --- postgresql-0007: add_new_columns_to_pathtarget ---
|
||||
{
|
||||
int T0 = 250, E = 500;
|
||||
long[] slowOps = {0}, fastOps = {0};
|
||||
|
||||
// Warmup
|
||||
slowOps[0] = addNewColumnsToPathtargetSlow(T0, E);
|
||||
fastOps[0] = addNewColumnsToPathtargetFast(T0, E);
|
||||
|
||||
long t0 = System.nanoTime();
|
||||
for (int r = 0; r < 100; r++) slowOps[0] = addNewColumnsToPathtargetSlow(T0, E);
|
||||
long slowMs = (System.nanoTime() - t0) / 1_000_000;
|
||||
|
||||
long t1 = System.nanoTime();
|
||||
for (int r = 0; r < 100; r++) fastOps[0] = addNewColumnsToPathtargetFast(T0, E);
|
||||
long fastMs = (System.nanoTime() - t1) / 1_000_000;
|
||||
|
||||
double speedup = fastOps[0] > 0 ? (double) slowOps[0] / fastOps[0] : 0;
|
||||
System.out.printf(" %-60s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.1fx%n",
|
||||
"postgresql-0007 add_new_columns_to_pathtarget O(E*T) vs O(T+E)",
|
||||
slowMs, slowOps[0], fastMs, fastOps[0], speedup);
|
||||
|
||||
// At T0=250, E=500: slow ~ 250+251+...+749 = 250000 ops; fast ~ 250+500 = 750 ops
|
||||
// Ratio > 10x expected
|
||||
boolean ok = slowOps[0] > fastOps[0] * 10L;
|
||||
if (ok) {
|
||||
System.out.println(" PASS postgresql-0007");
|
||||
passed++;
|
||||
} else {
|
||||
System.out.printf(" FAIL postgresql-0007: slowOps=%,d fastOps=%,d (expected >10x ratio)%n",
|
||||
slowOps[0], fastOps[0]);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Correctness check: both slow and fast produce identical output
|
||||
{
|
||||
int T0 = 10, E = 20;
|
||||
long s = addToFlatTlistSlow(T0, E);
|
||||
long f = addToFlatTlistFast(T0, E);
|
||||
assert s > 0 : "slow returned 0 ops";
|
||||
assert f > 0 : "fast returned 0 ops";
|
||||
System.out.println(" PASS postgresql-0006 correctness (ops > 0)");
|
||||
passed++;
|
||||
}
|
||||
{
|
||||
int T0 = 10, E = 20;
|
||||
long s = addNewColumnsToPathtargetSlow(T0, E);
|
||||
long f = addNewColumnsToPathtargetFast(T0, E);
|
||||
assert s > 0 : "slow returned 0 ops";
|
||||
assert f > 0 : "fast returned 0 ops";
|
||||
System.out.println(" PASS postgresql-0007 correctness (ops > 0)");
|
||||
passed++;
|
||||
}
|
||||
|
||||
System.out.println("=".repeat(100));
|
||||
int total = passed + failed;
|
||||
System.out.printf("%d/%d %s%n", passed, total, failed == 0 ? "PASS" : "FAIL");
|
||||
if (failed > 0) System.exit(1);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue