hive-0003
This commit is contained in:
parent
7e7cd2945a
commit
0fb709cb72
11 changed files with 1163 additions and 6 deletions
|
|
@ -0,0 +1,117 @@
|
|||
# ruby-0003: RubyGems Gem::Specification#dependent_gems — O(N²×D) nested scan
|
||||
|
||||
## Severity: MEDIUM
|
||||
|
||||
## Location
|
||||
- `lib/rubygems/specification.rb:1746` — `def dependent_gems(check_dev = true)`
|
||||
- `lib/rubygems/specification.rb:1748` — `Gem::Specification.each do |spec|`
|
||||
- `lib/rubygems/specification.rb:1752` — `find_all_satisfiers(dep) do |sat|`
|
||||
- `lib/rubygems/specification.rb:1872` — `def find_all_satisfiers(dep)`
|
||||
- `lib/rubygems/specification.rb:1873` — `Gem::Specification.each do |spec|`
|
||||
|
||||
## Description
|
||||
|
||||
`Gem::Specification#dependent_gems` computes which installed specs depend on
|
||||
`self`. It does this with two nested `Gem::Specification.each` loops:
|
||||
|
||||
```ruby
|
||||
# specification.rb:1746-1759
|
||||
def dependent_gems(check_dev = true)
|
||||
out = []
|
||||
Gem::Specification.each do |spec| # O(N) outer scan
|
||||
deps = check_dev ? spec.dependencies : spec.runtime_dependencies
|
||||
deps.each do |dep|
|
||||
next unless satisfies_requirement?(dep)
|
||||
sats = []
|
||||
find_all_satisfiers(dep) do |sat| # O(N) inner scan per dep
|
||||
sats << sat
|
||||
end
|
||||
out << [spec, dep, sats]
|
||||
end
|
||||
end
|
||||
out
|
||||
end
|
||||
|
||||
def find_all_satisfiers(dep)
|
||||
Gem::Specification.each do |spec| # O(N) full scan
|
||||
yield spec if spec.satisfies_requirement? dep
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
For N installed gems, each with D dependencies:
|
||||
- Outer loop: O(N)
|
||||
- For each spec's D deps: `find_all_satisfiers` runs a full O(N) scan
|
||||
- **Total: O(N × D × N) = O(N² × D)**
|
||||
|
||||
`dependent_gems` is called from `Gem::Uninstaller#ask_if_ok` during
|
||||
`gem uninstall` to warn about broken dependencies. For large gem environments
|
||||
(Ruby on Rails applications, CI servers, rbenv setups with 300–800 installed
|
||||
gems), this triggers hundreds of thousands of comparisons.
|
||||
|
||||
## Root Cause
|
||||
|
||||
`find_all_satisfiers` scans ALL specs linearly to find which ones satisfy a
|
||||
given dependency requirement. There is no reverse index from gem name → specs,
|
||||
so each `satisfies_requirement?` check performs a full scan.
|
||||
|
||||
## Fix
|
||||
|
||||
Build a reverse index once: map `gem_name → [spec, ...]`. Since
|
||||
`satisfies_requirement?` checks the gem name first (version check is secondary),
|
||||
the index reduces `find_all_satisfiers` from O(N) to O(matching_name_count).
|
||||
`dependent_gems` itself avoids the inner `find_all_satisfiers` loop:
|
||||
|
||||
```diff
|
||||
--- a/lib/rubygems/specification.rb
|
||||
+++ b/lib/rubygems/specification.rb
|
||||
@@ -1746,18 +1746,20 @@ class Gem::Specification
|
||||
def dependent_gems(check_dev = true)
|
||||
out = []
|
||||
- Gem::Specification.each do |spec|
|
||||
- deps = check_dev ? spec.dependencies : spec.runtime_dependencies
|
||||
- deps.each do |dep|
|
||||
- next unless satisfies_requirement?(dep)
|
||||
- sats = []
|
||||
- find_all_satisfiers(dep) do |sat|
|
||||
- sats << sat
|
||||
- end
|
||||
- out << [spec, dep, sats]
|
||||
+ # Build a reverse index: gem_name -> [specs that provide it]
|
||||
+ name_index = Hash.new { |h, k| h[k] = [] }
|
||||
+ Gem::Specification.each { |s| name_index[s.name] << s }
|
||||
+
|
||||
+ Gem::Specification.each do |spec|
|
||||
+ deps = check_dev ? spec.dependencies : spec.runtime_dependencies
|
||||
+ deps.each do |dep|
|
||||
+ next unless satisfies_requirement?(dep)
|
||||
+ # Only check specs with the right name — O(matches) not O(N)
|
||||
+ sats = name_index[dep.name].select { |s| s.satisfies_requirement?(dep) }
|
||||
+ out << [spec, dep, sats]
|
||||
end
|
||||
end
|
||||
out
|
||||
end
|
||||
```
|
||||
|
||||
## Complexity
|
||||
|
||||
N = installed gem count, D = avg dependencies per gem
|
||||
|
||||
| N | Before (ops) | After (ops) | Ratio |
|
||||
|------|---------------|----------------|--------|
|
||||
| 100 | ~5,000 | ~200 | 25× |
|
||||
| 300 | ~45,000 | ~600 | 75× |
|
||||
| 800 | ~320,000 | ~1,600 | 200× |
|
||||
|
||||
Assumptions: D=5 deps/gem, avg 1 satisfier per dep (same gem name, single version).
|
||||
|
||||
## Impact
|
||||
|
||||
`gem uninstall <gem>` → `ask_if_ok` → `dependent_gems`:
|
||||
- Development machines with rbenv/rvm typically have 200–500 gems installed
|
||||
- CI servers running bundler-audit or bundle exec gem commands: 300–800 gems
|
||||
- With 500 gems and D=5 deps, the fix reduces ~1.25M comparisons to ~2,500
|
||||
|
||||
The `find_all_satisfiers` private method should also be updated to use the index
|
||||
for consistency, though it is primarily used through `dependent_gems`.
|
||||
153
defects/ruby/unit/RubyGemsDependentGemsAlgorithm.java
Normal file
153
defects/ruby/unit/RubyGemsDependentGemsAlgorithm.java
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package unit;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* RubyGemsDependentGemsAlgorithm — CWE-407 test for ruby-0003
|
||||
*
|
||||
* Models Gem::Specification#dependent_gems + #find_all_satisfiers:
|
||||
* slow: for each spec, for each dep: Gem::Specification.each — O(N² × D)
|
||||
* fast: build name_index once, then name_index[dep.name].select — O(N × D)
|
||||
*
|
||||
* Test: for N gems with D deps each, slow does N*D*N ops; fast does N*D + N_matches.
|
||||
* Ratio must be >= 10x at N=200.
|
||||
*/
|
||||
public class RubyGemsDependentGemsAlgorithm {
|
||||
|
||||
static final class GemSpec {
|
||||
final String name;
|
||||
final String version;
|
||||
final List<String> depNames; // simplified: just dep gem names
|
||||
|
||||
GemSpec(String name, String version, List<String> depNames) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
this.depNames = depNames;
|
||||
}
|
||||
|
||||
boolean satisfiesRequirement(String depName) {
|
||||
return this.name.equals(depName);
|
||||
}
|
||||
}
|
||||
|
||||
// --- SLOW: O(N² × D) ---
|
||||
// Gem::Specification.each inside find_all_satisfiers inside dependent_gems
|
||||
static class SlowDependentGems {
|
||||
long comparisons = 0;
|
||||
|
||||
// find_all_satisfiers(dep): scans all N specs
|
||||
List<GemSpec> findAllSatisfiers(List<GemSpec> allSpecs, String depName) {
|
||||
List<GemSpec> result = new ArrayList<>();
|
||||
for (GemSpec spec : allSpecs) {
|
||||
comparisons++;
|
||||
if (spec.satisfiesRequirement(depName)) {
|
||||
result.add(spec);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// dependent_gems(self, allSpecs): find specs that depend on self
|
||||
List<Object[]> dependentGems(GemSpec self, List<GemSpec> allSpecs) {
|
||||
List<Object[]> out = new ArrayList<>();
|
||||
for (GemSpec spec : allSpecs) { // O(N) outer
|
||||
for (String depName : spec.depNames) { // O(D) deps
|
||||
comparisons++;
|
||||
if (self.satisfiesRequirement(depName)) {
|
||||
// find_all_satisfiers: O(N) inner
|
||||
List<GemSpec> sats = findAllSatisfiers(allSpecs, depName);
|
||||
out.add(new Object[]{spec, depName, sats});
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
// --- FAST: O(N × D) ---
|
||||
// Build name_index once, then O(matches) lookup
|
||||
static class FastDependentGems {
|
||||
long ops = 0;
|
||||
|
||||
List<Object[]> dependentGems(GemSpec self, List<GemSpec> allSpecs) {
|
||||
// Build reverse index: name -> [specs]
|
||||
Map<String, List<GemSpec>> nameIndex = new HashMap<>();
|
||||
for (GemSpec s : allSpecs) {
|
||||
nameIndex.computeIfAbsent(s.name, k -> new ArrayList<>()).add(s);
|
||||
ops++;
|
||||
}
|
||||
|
||||
List<Object[]> out = new ArrayList<>();
|
||||
for (GemSpec spec : allSpecs) { // O(N) outer
|
||||
for (String depName : spec.depNames) { // O(D) deps
|
||||
ops++;
|
||||
if (self.satisfiesRequirement(depName)) {
|
||||
// O(matches) lookup, not O(N)
|
||||
List<GemSpec> sats = nameIndex.getOrDefault(depName, Collections.emptyList());
|
||||
out.add(new Object[]{spec, depName, sats});
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
static List<GemSpec> makeSpecs(int n, int depsPerSpec) {
|
||||
List<String> names = new ArrayList<>();
|
||||
for (int i = 0; i < n; i++) names.add("gem_" + i);
|
||||
|
||||
List<GemSpec> specs = new ArrayList<>();
|
||||
Random rng = new Random(42);
|
||||
for (int i = 0; i < n; i++) {
|
||||
List<String> deps = new ArrayList<>();
|
||||
for (int d = 0; d < depsPerSpec; d++) {
|
||||
deps.add(names.get(rng.nextInt(n)));
|
||||
}
|
||||
specs.add(new GemSpec("gem_" + i, "1.0." + i, deps));
|
||||
}
|
||||
return specs;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
int[] sizes = {50, 100, 200, 300};
|
||||
int depsPerSpec = 5;
|
||||
System.out.println("RubyGemsDependentGemsAlgorithm — ruby-0003");
|
||||
System.out.println(" Pattern: find_all_satisfiers O(N) scan inside O(N×D) loop vs reverse index");
|
||||
System.out.println(" Simulation: D=" + depsPerSpec + " deps per gem");
|
||||
System.out.println();
|
||||
|
||||
int passed = 0;
|
||||
int total = 0;
|
||||
|
||||
for (int n : sizes) {
|
||||
List<GemSpec> allSpecs = makeSpecs(n, depsPerSpec);
|
||||
GemSpec target = allSpecs.get(0); // gem_0 is the gem being uninstalled
|
||||
|
||||
SlowDependentGems slow = new SlowDependentGems();
|
||||
FastDependentGems fast = new FastDependentGems();
|
||||
|
||||
List<Object[]> slowResult = slow.dependentGems(target, allSpecs);
|
||||
List<Object[]> fastResult = fast.dependentGems(target, allSpecs);
|
||||
|
||||
// Both should find the same number of dependent specs
|
||||
boolean sameCount = slowResult.size() == fastResult.size();
|
||||
|
||||
double ratio = (double) slow.comparisons / fast.ops;
|
||||
boolean correctRatio = n >= 100 ? ratio >= 10.0 : ratio >= 2.0;
|
||||
boolean pass = sameCount && correctRatio;
|
||||
|
||||
total++;
|
||||
if (pass) passed++;
|
||||
|
||||
System.out.printf(" N=%-4d D=%d slow=%8d fast=%6d ratio=%5.1fx same=%b %s%n",
|
||||
n, depsPerSpec,
|
||||
slow.comparisons, fast.ops,
|
||||
ratio, sameCount,
|
||||
pass ? "PASS" : "FAIL");
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.printf("Result: %d/%d PASS%n", passed, total);
|
||||
if (passed < total) System.exit(1);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue