Module: OneGadget::Emulators::Conditional
- Included in:
- Processor
- Defined in:
- lib/one_gadget/emulators/conditional.rb
Overview
Shared modelling of compare instructions and conditional branches.
A gadget candidate may cross a conditional branch: the fetcher stitches the actual taken/not-taken path (see Fetchers::Base#candidates), and the emulator turns the branch decision into a gadget constraint.
Branches are resolved with one line of look-ahead: at the branch we record a pending decision, and on the next line we compare that line's address to the branch target to learn whether the stitched path took the branch.
The including class (an Processor subclass) must
provide registers and register? (operand lookup), operands(cmd) (the
arch's operand splitter), self.class.bits (32/64, for the signedness cast),
and the +@flags+/+@pending+/+@constraints+ state Processor#initialize sets up.
Constant Summary collapse
- RELATION =
Taken-semantics of each supported branch condition, keyed by a predicate named after the comparison it encodes (the LLVM
icmpnames): a leadingu= unsigned,s= signed. Value is[relation, signedness], where signedness (+nil+/+:u+/+:s+) selects the operand cast. :eq :ne equality :ult :ule :ugt :uge unsigned < <= > >= :slt :sle :sgt :sge signed < <= > >= { eq: ['==', nil], ne: ['!=', nil], ult: ['<', :u], ule: ['<=', :u], ugt: ['>', :u], uge: ['>=', :u], slt: ['<', :s], sle: ['<=', :s], sgt: ['>', :s], sge: ['>=', :s] }.freeze
- NEGATE =
Relation under the not-taken branch.
{ '==' => '!=', '!=' => '==', '>=' => '<', '<' => '>=', '>' => '<=', '<=' => '>' }.freeze
- COMPARE_OPS =
The flag-setting compares we model, keyed by the ALU operation the compare performs -- its flags reflect that result. Each entry says whether magnitude conditions (anything beyond +eq+/+ne+) are sound afterwards (+ordered+) and names the method that renders its constraint text. An arch maps its own mnemonics onto these ops (its
COMPARES), so adding an arch needs no change here; adding a genuinely new ALU op means one entry plus itsrender_*method. { sub: { ordered: true, render: :render_sub }, # subtraction: flags from lhs - rhs add: { ordered: true, render: :render_add }, # addition: flags from lhs + rhs and: { ordered: false, render: :render_and } # bitwise AND: flags from lhs & rhs (zero flag) }.freeze
Instance Method Summary collapse
-
#branch_on_bit(target, operand, bit, negate:) ⇒ Object
Register a self-contained branch that tests a single bit of a register: also carries its own test, so no preceding compare is needed.
-
#branch_on_compare(cond, target) ⇒ true, :fail
Register a branch on the last recorded compare's flags, resolved on the next line.
-
#branch_on_zero(target, operand, negate:) ⇒ Object
Register a self-contained branch that tests a register against zero.
-
#comparisons_on(expr) ⇒ Object
Every comparison recorded so far on
expr, the left side as rendered. -
#handle_compare(op, cmd) ⇒ true
Model a compare line: record its two operands' current values under the compare's ALU op, so a following conditional branch can be rendered.
-
#mnemonic(cmd) ⇒ String
The mnemonic of an objdump line.
-
#operand_str(operand) ⇒ String
Render an operand for a constraint: a register becomes its current value, an immediate becomes hex, anything else (a memory operand) stays as-is.
-
#record_compare(op, lhs, rhs) ⇒ true
Record a compare so a following conditional branch can be rendered.
-
#resolve_pending_branch(cmd) ⇒ Object
Resolve the pending branch using +cmd+'s address: if it equals the branch target the stitched path took the branch, else it fell through.
-
#satisfiable?(triples) ⇒ Boolean
Whether some value satisfies every comparison in
triplesat once, by intersecting the range each one allows. -
#value_str(val) ⇒ String
A value as a constraint reads it: a concrete one in hex, whichever side of a compare it came from, and anything else as it renders itself.
Instance Method Details
#branch_on_bit(target, operand, bit, negate:) ⇒ Object
Register a self-contained branch that tests a single bit of a register: also carries its own test, so no preceding compare is needed. Renders a bitmask test.
208 209 210 211 212 213 214 215 |
# File 'lib/one_gadget/emulators/conditional.rb', line 208 def branch_on_bit(target, operand, bit, negate:) reg = operand_str(operand) mask = OneGadget::Helper.hex(1 << bit) hit = negate ? '!=' : '==' miss = negate ? '==' : '!=' @pending = { target:, compare: ->(taken) { ["(#{reg} & #{mask})", taken ? hit : miss, ZERO] } } true end |
#branch_on_compare(cond, target) ⇒ true, :fail
Register a branch on the last recorded compare's flags, resolved on the next
line. Call it from handle_branch; cond is the comparison predicate and
target is the branch's direct destination address.
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/one_gadget/emulators/conditional.rb', line 163 def branch_on_compare(cond, target) return :fail if @flags.nil? rel = RELATION[cond] return :fail if rel.nil? # A magnitude condition needs a compare whose flags reflect a full ordering # (+:sub+/+:add+); an equality-only compare (+:and+) supports just eq/ne. return :fail unless COMPARE_OPS.fetch(@flags[:op])[:ordered] || %i[eq ne].include?(cond) op = @flags[:op] lhs = @flags[:lhs] rhs = @flags[:rhs] @pending = { target:, compare: ->(taken) { compare_triple(op, lhs, rhs, rel, taken) } } true end |
#branch_on_zero(target, operand, negate:) ⇒ Object
Register a self-contained branch that tests a register against zero. It carries
its own compare, so no preceding compare is needed. negate: selects the sense:
false branches when the register is zero, true when it isn't.
191 192 193 194 195 196 197 |
# File 'lib/one_gadget/emulators/conditional.rb', line 191 def branch_on_zero(target, operand, negate:) reg = operand_str(operand) hit = negate ? '!=' : '==' # taken (not negated) => reg == 0 miss = negate ? '==' : '!=' @pending = { target:, compare: ->(taken) { [reg, taken ? hit : miss, ZERO] } } true end |
#comparisons_on(expr) ⇒ Object
Every comparison recorded so far on expr, the left side as rendered.
Matching on that text is what makes this sound: the renderer substitutes
each register's current value, so two constraints printing the same left
side really are about the same tracked value (and a differing signedness
cast is part of that text, keeping incomparable ones apart).
247 248 249 |
# File 'lib/one_gadget/emulators/conditional.rb', line 247 def comparisons_on(expr) @constraints.filter_map { |type, obj| obj if type == :cmp && obj.first == expr } end |
#handle_compare(op, cmd) ⇒ true
Model a compare line: record its two operands' current values under the compare's ALU op, so a following conditional branch can be rendered.
Call this from process! when the mnemonic is one of the arch's compares
(its COMPARES maps the mnemonic to the op), before dispatching to the
inst_* handlers.
98 99 100 101 |
# File 'lib/one_gadget/emulators/conditional.rb', line 98 def handle_compare(op, cmd) lhs, rhs = operands(cmd) record_compare(op, operand_str(lhs), operand_str(rhs)) end |
#mnemonic(cmd) ⇒ String
The mnemonic of an objdump line. Use it at the top of process! to decide
whether a line is a compare or a branch.
112 113 114 |
# File 'lib/one_gadget/emulators/conditional.rb', line 112 def mnemonic(cmd) cmd[/\A[0-9a-f]+:\s*(\S+)/, 1] || '' end |
#operand_str(operand) ⇒ String
Render an operand for a constraint: a register becomes its current value,
an immediate becomes hex, anything else (a memory operand) stays as-is.
#handle_compare uses it on each compare operand; call it yourself only
when writing a bespoke branch_on_* helper.
127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/one_gadget/emulators/conditional.rb', line 127 def operand_str(operand) if register?(operand) raise Error::ClobberedRegisterError, operand if clobbered?(registers[operand]) return value_str(registers[operand]) end OneGadget::Helper.hex(Integer(operand)) rescue ArgumentError operand end |
#record_compare(op, lhs, rhs) ⇒ true
Record a compare so a following conditional branch can be rendered. Normally reached through #handle_compare; call it directly only when an arch models a flag-setting instruction that #handle_compare doesn't cover.
77 78 79 80 |
# File 'lib/one_gadget/emulators/conditional.rb', line 77 def record_compare(op, lhs, rhs) @flags = { op:, lhs:, rhs: } true end |
#resolve_pending_branch(cmd) ⇒ Object
Resolve the pending branch using +cmd+'s address: if it equals the branch
target the stitched path took the branch, else it fell through. On
resolution the rendered relation is appended to the gadget's constraints.
Must be called at the top of process! for every line (a no-op when nothing
is pending), so the branch registered on the previous line sees this line's address.
230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/one_gadget/emulators/conditional.rb', line 230 def resolve_pending_branch(cmd) return if @pending.nil? taken = branch_addr(cmd) == @pending[:target] triple = @pending[:compare].call(taken) @pending = nil @constraints << [:cmp, triple] return if satisfiable?(comparisons_on(triple.first)) raise Error::InfeasiblePathError, "cannot hold together: #{triple.first}" end |
#satisfiable?(triples) ⇒ Boolean
Whether some value satisfies every comparison in triples at once, by
intersecting the range each one allows. Comparisons against anything but
an integer are ignored rather than guessed at, so an undecidable one never
makes a path look impossible.
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/one_gadget/emulators/conditional.rb', line 258 def satisfiable?(triples) low = nil high = nil excluded = [] triples.each do |_expr, op, rhs| next unless OneGadget::Helper.integer?(rhs) value = Integer(rhs) case op when '==' low = [low, value].compact.max high = [high, value].compact.min when '!=' then excluded << value when '<' then high = [high, value - 1].compact.min when '<=' then high = [high, value].compact.min when '>' then low = [low, value + 1].compact.max when '>=' then low = [low, value].compact.max end end return false if low && high && (low > high || (low == high && excluded.include?(low))) true end |
#value_str(val) ⇒ String
A value as a constraint reads it: a concrete one in hex, whichever side of a compare it came from, and anything else as it renders itself.
144 145 146 |
# File 'lib/one_gadget/emulators/conditional.rb', line 144 def value_str(val) val.is_a?(Integer) ? OneGadget::Helper.hex(val) : val.to_s end |