Class: Rubycc::Rmake::Expander
- Inherits:
-
Object
- Object
- Rubycc::Rmake::Expander
- Defined in:
- lib/rubycc/rmake/expander.rb
Overview
Turns raw make text with $(...) / ${...} / $x references into its
expanded string. It implements exactly the reference forms the mkmf corpus
uses (test/fixtures/mkmf): plain variable references, nested references,
the $(var:from=to) substitution reference, and the automatic variables
($@, $<, $^, $*, and their D/F directory/file variants). Undefined
variables expand to the empty string, matching make.
Recursive (=) variables are stored unexpanded and expanded on every
reference; simple (:=) variables are stored already-expanded. That split
lives in the Variable value object, so the expander only has to re-expand
whatever text a recursive variable holds — which is where an A=$(B) /
B=$(A) cycle would otherwise recurse forever, so every variable
dereference spends one unit of the depth budget below.
Constant Summary collapse
- MAX_EXPANSION_DEPTH =
DoS fail-safe: the maximum number of nested variable dereferences during a single expansion. A reference cycle between recursive variables would recurse without bound; this cap converts that into an ExpansionError long before the Ruby stack is exhausted. Legitimate mkmf variable chains (e.g. rubyarchhdrdir -> rubyhdrdir -> includedir -> prefix) are well under a dozen deep, so the limit is generous.
200- MAX_EXPANSION_REFERENCES =
DoS fail-safe, part two: the depth cap above only counts how deep the call stack goes, so it says nothing about fan-out. A chain like
A1 = $(A0)$(A0)/A2 = $(A1)$(A1)/ ... stays only n levels deep while doing 2^n work, so a 30-line Makefile can wedge the parser (:=assignments expand at parse time, before any target is built). The two budgets below bound the total work of one top-level expansion, in the same spirit as the preprocessor's EXPANSION_TOKEN_LIMIT.Both are needed, because each misses the explosion the other catches:
- MAX_EXPANSION_REFERENCES counts variable dereferences. It is the only
one that sees a fan-out whose leaves expand to nothing (
A0 =empty still costs 2^n dereferences while producing zero characters), and it is also what catches a cycle that grows the call count rather than the text. - MAX_EXPANSION_OUTPUT counts characters produced. It is the only one
that sees a blow-up driven by text size rather than reference count
(
BIG = <megabytes>referenced a handful of times per level exhausts memory long before the reference count is reached).
Both limits are orders of magnitude above what the mkmf corpus needs: measured over the rmake test suite (test/fixtures/mkmf included), the heaviest single expansion resolves 32 references and yields ~4 KB.
The reference cap is deliberately kept small rather than merely "safe": a budget is spent per top-level call, so the time it takes to reach the cap is itself the attacker's resource — a Makefile with many
:=assignments multiplies it by the number of assignments. 100_000 still leaves ~3000x headroom over the measured 32 while tripping in well under a second. - MAX_EXPANSION_REFERENCES counts variable dereferences. It is the only
one that sees a fan-out whose leaves expand to nothing (
100_000- MAX_EXPANSION_OUTPUT =
4 MiB of characters
4_194_304- AUTOMATIC_CHARS =
Base characters of the automatic variables rmake recognises. Each may be written bare ($@) or parenthesised ($(@)), and the parenthesised form may carry a D/F modifier ($(@D) = directory part, $(@F) = file part).
%w[@ < ^ ? * + %].freeze
Instance Method Summary collapse
-
#expand(text, autos = nil, depth = 0) ⇒ Object
Expand
text. -
#initialize(variables) ⇒ Expander
constructor
- variables
Hash=> Variable.
Constructor Details
#initialize(variables) ⇒ Expander
- variables
Hash=> Variable
68 69 70 71 72 |
# File 'lib/rubycc/rmake/expander.rb', line 68 def initialize(variables) @variables = variables @references = 0 @output_chars = 0 end |
Instance Method Details
#expand(text, autos = nil, depth = 0) ⇒ Object
Expand text. autos is a Hash=> String keyed by automatic
variable base character (e.g. => "foo.o", "<" => "foo.c"); pass nil
outside a recipe, where automatic variables expand to "".
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/rubycc/rmake/expander.rb', line 77 def (text, autos = nil, depth = 0) # depth 0 is the entry point from outside, so one external call gets one # budget; the recursive calls below all pass depth + 1 and therefore # share it. if depth.zero? @references = 0 @output_chars = 0 end guard_depth(depth) out = +"" i = 0 n = text.length while i < n c = text[i] if c == "$" consumed, value = (text, i, autos, depth) @output_chars += value.length guard_output out << value i += consumed else @output_chars += 1 guard_output out << c i += 1 end end out end |