3.9 KiB
R (language) — CWE-407 Disclosure Brief
Project: R (language) Disclosure date: 2026-03-27 Severity: MEDIUM Speedup: varies with k² Status: PATCHED
Finding
R's built-in rapply() function contains an O(k²) nested loop in its internal do_one() helper in src/main/apply.c. For each element visited during recursive application, the function checks class membership by iterating over all k class names in the classes argument against each element's class vector. When how = "replace" or how = "list" is used on deeply nested lists with many class-matched nodes, this produces quadratic behavior in the number of classes.
The Defect(s)
| ID | Location | Pattern | Complexity |
|---|---|---|---|
| r-source-0001 | src/main/apply.c:312 |
rapply() do_one() nested class-match loop iterates over k classes for each of k class checks per element |
O(k²) per element visited |
Complexity Proof
Let k = number of class names in the classes argument to rapply(), E = number of elements in the list being traversed.
For each element visited, do_one() checks whether the element's class matches any entry in classes. The check is performed by nested iteration: for each class name in classes (up to k), it scans the element's class vector (also up to k in worst case for multi-class S3 objects):
Per element: up to k × k comparisons = O(k²)
Over all E elements: E × k² total comparisons
If classes is interned to a pointer-set (hash set of CHARSXP pointers) before the loop, each per-element class membership test becomes a single O(1) pointer lookup per class in the element's class vector:
Per element: up to k hash lookups = O(k)
Over all E elements: E × k total work
For k = 30 classes and E = 1,000 list elements, the defective path performs 900,000 comparisons; the fixed path performs 30,000.
Impact
R users calling rapply() with a non-trivial classes vector on large nested lists — common in statistical pipelines that traverse S3 object hierarchies, JSON-parsed data, or recursive model structures — see quadratic runtime in k. Package authors using rapply() internally (e.g., for recursive transformation of list-based model objects) inherit this defect. The Base R implementation is used by all CRAN packages relying on rapply.
The Fix
Before the element-traversal loop in do_one(), intern the classes STRSXP vector to a pointer-set (hash set of CHARSXP pointers using R's internal string interning). Replace the inner class-name comparison loop with an O(1) hash-set lookup per class in the element's class attribute.
Patch
- /* do_one() in apply.c:312 — nested class match */
- static Rboolean do_one(SEXP x, SEXP classes, ...) {
- SEXP klass = getAttrib(x, R_ClassSymbol);
- for (int i = 0; i < length(classes); i++) {
- for (int j = 0; j < length(klass); j++) {
- if (strcmp(CHAR(STRING_ELT(classes, i)),
- CHAR(STRING_ELT(klass, j))) == 0)
- return TRUE;
- }
- }
- return FALSE;
- }
+ /* Intern classes to pointer set before traversal loop */
+ static void build_class_set(SEXP classes, R_StringSet *set) {
+ for (int i = 0; i < length(classes); i++)
+ R_StringSet_insert(set, STRING_ELT(classes, i)); /* CHARSXP ptr */
+ }
+ static Rboolean do_one(SEXP x, R_StringSet *class_set, ...) {
+ SEXP klass = getAttrib(x, R_ClassSymbol);
+ for (int j = 0; j < length(klass); j++) {
+ if (R_StringSet_contains(class_set, STRING_ELT(klass, j)))
+ return TRUE;
+ }
+ return FALSE;
+ }
What We Ask
Please review, apply, and coordinate a 90-day disclosure window before public release. Reply to security@undefect.com.
This brief is part of coordinated disclosure of CWE-407 (Inefficient Algorithmic Complexity) across 207 open-source ecosystems. Full report: https://undefect.com