Skip to content

Instantly share code, notes, and snippets.

@shugo
Last active July 17, 2026 04:01
Show Gist options
  • Select an option

  • Save shugo/2d659157532d7488979fd923e56f7ed3 to your computer and use it in GitHub Desktop.

Select an option

Save shugo/2d659157532d7488979fd923e56f7ed3 to your computer and use it in GitHub Desktop.
Heuristic checker for non-volatile locals read on the longjmp path.
#!/usr/bin/env ruby
# Heuristic checker for non-volatile locals read on the longjmp path.
#
# Scans C sources for functions using EC_EXEC_TAG()/EXEC_TAG()/RUBY_SETJMP
# and reports locals that are declared before the setjmp, are not volatile,
# and are read in the longjmp-reachable region (outside the TAG_NONE block).
#
# Two severities:
# STRICT - also written between setjmp and longjmp: indeterminate per
# C11 7.13.2.1p3, a real portability bug.
# HARDEN - unmodified, so guaranteed by the standard, but LLVM 17/19
# have been observed to clobber such variables (rb_iterate0,
# rb_fiber_start); candidates for defensive volatile.
#
# Known false-positive sources (flagged, needs human review):
# - address-taken variables (marked [&taken]): already memory-resident,
# volatile usually unnecessary
# - variables reassigned after EC_POP_TAG before being read
# - the *(volatile struct x *)&p access trick (exec_recursive)
# - reads under `if (state == TAG_NONE)`-style guards after the pop
SETJMP_RE = /\b(EC_EXEC_TAG|EXEC_TAG|RUBY_SETJMP)\s*\(/
POP_RE = /\b(EC_POP_TAG|POP_TAG)\s*\(/
MARKERS = %w[EC_EXEC_TAG EXEC_TAG RUBY_SETJMP]
KEYWORDS = %w[
if else while for do switch case default
return goto break continue sizeof typedef
struct union enum static extern register auto
const volatile inline restrict void char short
int long float double signed unsigned _Bool
NULL true false ID VALUE
]
def each_match(re, str)
pos = 0
while pos <= str.length && (m = re.match(str, pos))
yield m
pos = m.end(0)
pos += 1 if m.end(0) == m.begin(0)
end
end
# Replace comments/string/char literals with spaces, keep offsets.
def strip_comments_strings(src)
out = src.dup
i = 0
n = src.bytesize
while i < n
c = src.getbyte(i)
if c == 0x2f && src.getbyte(i + 1) == 0x2a # /*
j = src.index('*/', i + 2)
j = j ? j + 2 : n
(i...j).each {|k| out.setbyte(k, 0x20) unless out.getbyte(k) == 0x0a }
i = j
elsif c == 0x2f && src.getbyte(i + 1) == 0x2f # //
j = src.index("\n", i) || n
(i...j).each {|k| out.setbyte(k, 0x20) }
i = j
elsif c == 0x22 || c == 0x27 # " '
q = c
j = i + 1
while j < n
if src.getbyte(j) == 0x5c # backslash
j += 2
elsif src.getbyte(j) == q
j += 1
break
else
j += 1
end
end
((i + 1)...[j - 1, n].min).each do |k|
out.setbyte(k, 0x20) unless out.getbyte(k) == 0x0a
end
i = j
else
i += 1
end
end
out
end
# Given pos of '{', return pos just after its matching '}'.
def match_brace(text, open_pos)
depth = 0
(open_pos...text.bytesize).each do |i|
case text.getbyte(i)
when 0x7b then depth += 1
when 0x7d
depth -= 1
return i + 1 if depth == 0
end
end
text.bytesize
end
# Return [body_start, body_end, name] of the function containing pos.
def function_bounds(text, pos)
depth = 0
start = nil
pos.downto(0) do |i|
case text.getbyte(i)
when 0x7d # }
depth += 1
when 0x7b # {
if depth == 0
# could be a nested block; keep going until the brace at
# file depth 1 (line-start heuristic: preceded by ')')
j = i - 1
j -= 1 while j >= 0 && " \t\n\r".include?(text[j])
if j >= 0 && text.getbyte(j) == 0x29 && start.nil? # )
start = i
# check this is top level: count braces before it
d = text[0, i].count('{') - text[0, i].count('}')
break if d == 0
start = nil
end
else
depth -= 1
end
end
end
return nil unless start
fin = match_brace(text, start)
# function name: last identifier before the '(' of the parameter list
sig = text[[0, start - 500].max, [start, 500].min]
name = sig.scan(/(\w+)\s*\(/).last
name = name ? name[0] : '?'
[start, fin, name]
end
# Parse parameter declarations from the signature before bo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment