Document what a reader cannot infer (#436)

Every public method now states its parameters and its return: 18 were
missing a @param and 22 a @return, including every arch's process!
override, RegisterFile's Hash accessors, Base.cached and the branch
registrations in Conditional.

Thirteen private methods gained a line, the ones whose name does not say
what they do -- take_until, which keeps ties and so may return more than it
is asked for; X86#to_lambda, which answers a vector register with one value
per lane; the Thumb BL encoding helpers, whose offset is scattered across
both halfwords; PIC's distance from the GOT to "/bin/sh"; and the argv
builders, where "with sh" means libc has already filled in argv[0]. The
rest -- inst_mov, emulator, call_str and their kind -- are left bare, since
their names already say it.

Descriptions are left alone. A method whose name tells may still carry a
one-line summary, and the ones here do; nothing in lib carries more than a
line without earning it.
This commit is contained in:
david942j
2026-08-30 23:49:28 +08:00
committed by GitHub
parent 6702447fb6
commit 3d7714fa04
21 changed files with 88 additions and 2 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ require 'one_gadget/version'
# @param [String?] arg
# Can be either +build_id+ or path to libc.
# @param [Mixed] options
# See {OneGadget#gadgets} for ore information.
# See {OneGadget#gadgets} for more information.
# @return [Array<OneGadget::Gadget::Gadget>, Array<Integer>]
# The gadgets found.
# @example
+4
View File
@@ -19,6 +19,8 @@ module OneGadget
end
# @see OneGadget::Emulators::X86#process!
# @param [String] cmd One line from result of objdump.
# @return [Boolean] If successfully processed.
def process!(cmd)
resolve_pending_branch(cmd)
cmd = cmd.gsub(/#-?(0x)?[0-9a-f]+/) { |v| v[1..] }
@@ -60,6 +62,7 @@ module OneGadget
# A landing pad marks where an indirect branch may arrive; nothing about the
# state changes at one.
# @return [void]
alias inst_bti inst_nop
# Return the argument value of calling a function.
@@ -157,6 +160,7 @@ module OneGadget
class << self
# AArch64 is 64-bit.
# @return [Integer]
def bits
64
end
+1
View File
@@ -9,6 +9,7 @@ module OneGadget
class Amd64 < X86
class << self
# Bits.
# @return [Integer]
def bits
64
end
+6
View File
@@ -28,11 +28,15 @@ module OneGadget
end
# Memoized bytes of +file+ (the target libc), shared across emulator instances.
# @param [String] file Path to the target libc.
# @return [String] Its bytes.
def self.file_data(file)
(@file_data ||= {})[file] ||= File.binread(file)
end
# @see OneGadget::Emulators::AArch64#process!
# @param [String] cmd One line from result of objdump.
# @return [Boolean] If successfully processed.
def process!(cmd)
resolve_pending_branch(cmd)
line = cmd.strip
@@ -112,6 +116,7 @@ module OneGadget
# Thumb candidate offers no evidence either and is no better served than
# before.
# @param [Array<String>] lines The candidate's objdump lines.
# @return [void]
def note_instruction_set(lines)
return if @thumb
@@ -297,6 +302,7 @@ module OneGadget
class << self
# ARM (32-bit) is 32-bit.
# @return [Integer]
def bits
32
end
+7
View File
@@ -188,6 +188,7 @@ module OneGadget
# # fall-through path emits x0 != 0 ; taken path emits x0 == 0
# @example x86 reuses it for +jrcxz+/+jecxz+/+jcxz+ (always branch-if-zero)
# branch_on_zero(0x4a200, 'rcx', negate: false)
# @return [true] Registering the branch is how the line is handled.
def branch_on_zero(target, operand, negate:)
reg = operand_str(operand)
hit = negate ? '!=' : '==' # taken (not negated) => reg == 0
@@ -205,6 +206,7 @@ module OneGadget
# @example aarch64 +tbz w0, #4, 4a200+ - branch taken when bit 4 of +w0+ is 0
# branch_on_bit(0x4a200, 'w0', 4, negate: false) #=> true
# # taken path emits (w0 & 0x10) == 0 ; fall-through emits (w0 & 0x10) != 0
# @return [true] Registering the branch is how the line is handled.
def branch_on_bit(target, operand, bit, negate:)
reg = operand_str(operand)
mask = OneGadget::Helper.hex(1 << bit)
@@ -227,6 +229,9 @@ module OneGadget
# end
# # if the previous line did +branch_on_compare(:ne, 0x4a200)+ and this
# # +cmd+ sits at 0x4a200, the branch was taken; otherwise it fell through
# @return [void]
# @raise [OneGadget::Error::InfeasiblePathError]
# When the resolved relation cannot hold with the ones already recorded.
def resolve_pending_branch(cmd)
return if @pending.nil?
@@ -244,6 +249,8 @@ module OneGadget
# 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).
# @param [String] expr The left side, as rendered.
# @return [Array<(String, String, String)>] The comparisons recorded on it.
def comparisons_on(expr)
@constraints.filter_map { |type, obj| obj if type == :cmp && obj.first == expr }
end
+9
View File
@@ -58,6 +58,9 @@ module OneGadget
# Whether +(type, obj)+ is an address constraint on a bare (deref-0) target,
# i.e. one carrying a base register and offset to normalise.
# @param [Symbol] type The constraint's type.
# @param [Object] obj Its payload.
# @return [Boolean]
def address_deref0?(type, obj)
ADDRESS_TYPES.include?(type) && obj.deref_count.zero?
end
@@ -65,6 +68,9 @@ module OneGadget
# De-duplication key: an address constraint collapses per (type, base) so
# constraints of different types on the same register stay distinct; a raw
# constraint keys on its own text.
# @param [Symbol] type The constraint's type.
# @param [Object] obj Its payload.
# @return [Object] Equal for two constraints that impose the same requirement.
def constraint_key(type, obj)
return obj unless ADDRESS_TYPES.include?(type)
@@ -72,6 +78,9 @@ module OneGadget
end
# Render a constraint to its output string.
# @param [Symbol] type The constraint's type.
# @param [Object] obj Its payload.
# @return [String] The constraint as reported.
def render_constraint(type, obj)
case type
when :writable then "writable: #{obj}"
+1
View File
@@ -9,6 +9,7 @@ module OneGadget
class I386 < X86
class << self
# Yap, bits.
# @return [Integer]
def bits
32
end
+2
View File
@@ -72,6 +72,8 @@ module OneGadget
private
# Split an operand list on the commas that separate operands, which are the
# ones outside brackets: a memory operand carries commas of its own.
def parse_args(str)
args = []
cur = +''
+3 -1
View File
@@ -186,7 +186,9 @@ module OneGadget
private
# @return [(String, Integer)]
# Split a memory operand into what it is based on and how far from it.
# @param [String] arg The operand, with its brackets already removed.
# @return [(String, Integer)] The base, and the offset from it.
def mem_obj(arg)
# We have three forms:
# 0. reg
+6
View File
@@ -174,6 +174,9 @@ module OneGadget
raise Error::InstructionArgumentError, "#{reg.inspect} is not a valid register" unless register?(reg)
end
# Whether the +idx+-th argument meets +expect+, a requirement a catalogued
# call states about it that is not about the pointer being mapped (those go
# through {#record_pointer}). See {SafeCalls}.
def check_argument(idx, expect)
case expect
when :global_var? then global_var?(argument(idx))
@@ -316,6 +319,9 @@ module OneGadget
self.class.bits / 8
end
# Whether +obj+ is reached through the program counter, which is what a
# libc global looks like once resolved -- as opposed to a value the caller
# supplies.
def global_var?(obj)
obj.to_s.include?(pc)
end
@@ -36,15 +36,22 @@ module OneGadget
@entry_halves = {}
end
# @param [String] name Any name the architecture accepts, narrow or full.
# @return [OneGadget::Emulators::Lambda, Integer] What that name currently reads as.
def [](name)
value = super(full(name))
narrow?(name) ? narrowed(name, value) : value
end
# @param [String] name Any name the architecture accepts, narrow or full.
# @param [OneGadget::Emulators::Lambda, Integer] value
# @return [void]
def []=(name, value)
super(full(name), value)
end
# @param [String] name Any name the architecture accepts, narrow or full.
# @return [Boolean] Whether its storage holds a value.
def key?(name)
super(full(name))
end
+3
View File
@@ -17,6 +17,8 @@ module OneGadget
end
# @see OneGadget::Emulators::X86#process!
# @param [String] cmd One line from result of objdump.
# @return [Boolean] If successfully processed.
def process!(cmd)
resolve_pending_branch(cmd)
@cur_addr = cmd[/\A\s*([0-9a-f]+):/, 1]&.to_i(16)
@@ -253,6 +255,7 @@ module OneGadget
class << self
# RV64 is 64-bit.
# @return [Integer]
def bits
64
end
@@ -20,6 +20,7 @@ module OneGadget
# gadget staging data at +[bp+imm]+ (e.g. an argv array off the frame
# pointer) is recovered instead of collapsing to a bare +writable:+. A nil
# +bp+ leaves the arch +sp+-only. Call from the arch initializer after +super+.
# @param [String, nil] bp The frame register's name, or nil for an arch with none.
# @return [void]
def setup_frame_pointer(bp)
@bp = bp
+9
View File
@@ -10,6 +10,10 @@ module OneGadget
# Super class for amd64 and i386 processor.
class X86 < Processor
# Constructor for a x86 processor.
# @param [Array<String>] registers All the register names this architecture accepts.
# @param [String] sp The stack pointer's name.
# @param [String] bp The frame pointer's name.
# @param [String] pc The program counter's name.
def initialize(registers, sp, bp, pc)
super(registers, sp)
@pc = pc
@@ -124,6 +128,8 @@ module OneGadget
branch_on_compare(JCC[mnem], jump_target(cmd))
end
# The counter register a +jcxz+-family branch tests, at the width its
# mnemonic names.
def cx_reg(mnem)
{ 'jcxz' => 'cx', 'jecxz' => 'ecx', 'jrcxz' => 'rcx' }[mnem]
end
@@ -337,6 +343,9 @@ module OneGadget
dispatch_safe_call(addr)
end
# A vector register holds one value per lane, so it becomes an array of
# them -- each lane named by the shift that reaches it -- where an ordinary
# register becomes a single {Lambda}.
def to_lambda(reg)
return super unless reg =~ /^xmm\d+$/
@@ -130,10 +130,14 @@ module OneGadget
generate_argv_without_sh(argv_ptr, argv, allow_null)
end
# Whether the array is usable as it stands: an empty argv, or one whose only
# entry is a libc global.
def argv_already_valid?(argv)
argv[0] == '0' || (global_var?(argv[0]) && argv[1] == '0')
end
# The constraint for an argv whose first entry libc has already filled in
# with the shell's own name, leaving the caller to arrange the rest.
def generate_argv_with_sh(argv)
# argv[0] is not controlled by the user, argv[0] probably is "/bin/sh" or "sh" (but actually, the content of
# argv[0] doesn't quite matter, just need to make sure it's readable)
+5
View File
@@ -42,6 +42,8 @@ module OneGadget
sites.uniq
end
# Whether the two halfwords are a Thumb +BL+: the first names the encoding,
# and the second's top bits say it is the long form rather than +BLX+.
def thumb_bl?(high, low)
high.between?(0xf000, 0xf7ff) && low.allbits?(0xd000)
end
@@ -52,6 +54,9 @@ module OneGadget
((low >> 8) & 0x0f) == 0x0b
end
# Where a Thumb +BL+ goes. Its offset is scattered across both halfwords,
# with the two middle bits stored inverted against the sign bit, and is
# taken from the address two instructions on.
def thumb_bl_target(addr, high, low)
s = (high >> 10) & 1
i1 = (~(((low >> 13) & 1) ^ s)) & 1
+8
View File
@@ -37,6 +37,10 @@ module OneGadget
# (its output and everything derived from it), so re-analysing the same
# file - common in the specs, harmless for the CLI which reads a file once -
# doesn't redo the disassembly or the whole-binary scans.
# @param [Symbol] kind What is being cached, so two kinds may share a command.
# @param [String] command The objdump command the value is a function of.
# @yieldreturn [Object] The value, computed only on a miss.
# @return [Object] The cached value.
def self.cached(kind, command)
@cached ||= Hash.new { |h, k| h[k] = {} }
@cached[kind][command] ||= yield
@@ -101,6 +105,8 @@ module OneGadget
# also matches the +posix_spawn+ setup helpers, which emulation runs through.
# The mnemonic must be a call, so a branch whose target symbol merely looks
# similar (+<execlp@@GLIBC_2.4+0x136>+) doesn't end the window.
# @param [String] line One disassembled line.
# @return [Boolean]
def terminal_call_line?(line)
return false unless line[/\A\s*[0-9a-f]+:\s*(\S+)/, 1]&.match?(/\A#{call_str}x?(?:\.[wn])?\z/)
@@ -123,6 +129,8 @@ module OneGadget
end
# Emulate a candidate suffix and turn it into a gadget, or +nil+ if it isn't one.
# @param [Array<String>] lines The suffix, ending at the terminal call.
# @return [OneGadget::Gadget::Gadget, nil]
def resolve_suffix(lines)
processor = emulate(lines)
(@refused ||= {})[processor.refused_line] = true if processor.refused_line
+2
View File
@@ -77,6 +77,8 @@ module OneGadget
end
end
# How far +"/bin/sh"+ sits before the GOT, which is how PIC code names it:
# a window reaches the string as an offset from the register holding the base.
def rel_sh
@rel_sh ||= got_offset - str_offset('/bin/sh')
end
+1
View File
@@ -34,6 +34,7 @@ module OneGadget
# @param [Array<String>] options The options.
# @example
# objdump.extra_options = %w[-M intel]
# @return [void]
def extra_options=(options)
@options = options
end
+2
View File
@@ -424,6 +424,8 @@ module OneGadget
0.9**(depth + base)
end
# The constraints as a reader wants them: every +writable:+ folded into one
# sentence naming all of the addresses, the rest left alone.
def merge_constraints
key = 'writable: '
w_cons, normal = constraints.partition { |c| c.start_with?(key) }
+6
View File
@@ -79,6 +79,12 @@ module OneGadget
take_until(high, 3)
end
# The +count+ best-scoring of +ary+, in the order they were given, plus
# anything scoring as well as the last of them -- so a tie is never broken
# arbitrarily, and more than +count+ may come back.
# @param [Array<OneGadget::Gadget::Gadget>] ary
# @param [Integer] count
# @return [Array<OneGadget::Gadget::Gadget>]
def take_until(ary, count)
return ary if ary.size <= count