java-topology/whitepaper/outreach/r.md

3.9 KiB
Raw Blame History

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