Last active
July 17, 2026 04:01
-
-
Save shugo/2d659157532d7488979fd923e56f7ed3 to your computer and use it in GitHub Desktop.
Heuristic checker for non-volatile locals read on the longjmp path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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