La saison des vulnérabilités est ouverte

La saison des vulnérabilités est ouverte

These variables are written to "externally" (meaning from a callee of one of the ops) when a frame is invoked, when some kind of unwind happens, stuff like that.
One of these variables is the pointer to the current op in the bytecode. It is read at the very least when looking for an exception handler and when looking for what actual function the interpreter is currently inside of in cases of inlining.
I was hoping I could take the address of the local variables once before I tail-call into the first opcode implementation and hope that the addresses will remain stable because when tail-calling from one to another it "could" just reuse the "initial" parts of the stack frame, where I'm assuming the arguments go.
However, when I put a tiny toy interpreter into #CompilerExplorer here, it segfaults, and ASAN very quickly yells at me that I'm accessing memory after the stack frame it's in has disappeared :(
https://godbolt.org/z/d1G8GT4o1
Maybe someone has a brilliant idea for me :)

#define DEBUG 1 typedef struct CU { uint8_t **bytecodes; } CU; typedef struct Frame { struct Frame *caller; uint8_t *bytecode; char *registers; uint8_t *return_address; } Frame; typedef struct TC { uint8_t **interp_bc_start; uint8_t **interp_op; char **interp_frame_base; CU **interp_compunit; Frame *cur_frame; } TC; Frame *make_frame(TC *tc, CU *compunit, uint8_t bytecode_idx) { Frame *frame = calloc(sizeof(Frame), 1); frame->caller = tc->cur_frame; frame->bytecode = compunit->bytecodes[bytecode_idx]; frame->registers = calloc(256, 1); frame->return_address = *tc->interp_op; return frame; } #define OP_ARGS TC *tc, uint8_t *bc_start, uint8_t *op, char *frame_base, CU *compunit #define OP_PASS_ARGS tc, bc_start, op, frame_base, compunit typedef __attribute__((preserve_none)) int (*terp_op_fptr) (OP_ARGS); enum BC_op { CONST_I8, SET, ADD, SUB, PRINT, IF, EXIT, INVOKE, RETURN, MAX_BC_OP }; terp_op_fptr OP_FUNCS[MAX_BC_OP]; #define OPN(name) terp_op_ ## name #define OP(name) __attribute__((preserve_none)) int OPN(name) (OP_ARGS) #define GOTO_NEXT_OP uint8_t opcode = *op; op++; [[clang::musttail]] return OP_FUNCS[opcode](OP_PASS_ARGS) #define GET_REG(argidx) frame_base[op[argidx]] #define GET_I8(argidx) op[argidx] #if DEBUG __attribute__((noinline)) void debug_tc_vars(TC *tc) { fprintf(stderr, "tc vars:\n bc_start *(%p) = %p\n op *(%p) = %p = start + %lx\n regs *(%p) = %p\n compunit *(%p) = %p\n\n", tc->interp_bc_start, *tc->interp_bc_start, tc->interp_op, *tc->interp_op, ((intptr_t)*tc->interp_op - (intptr_t)*tc->interp_bc_start), tc->interp_frame_base, *tc->interp_frame_base, tc->interp_compunit, *tc->interp_compunit ); } #else #define debug_tc_vars(TC) #endif __attribute__((noinline)) void do_invocation(TC *tc, CU *cu, uint8_t bc_idx, uint8_t argument) { fprintf(stderr, "Creating new frame to do invocation of bytecode %d\n", bc_idx); Frame *newframe = make_frame(tc, cu, bc_idx); newframe->registers[0] = argument; tc->cur_frame = newframe; *tc->interp_bc_start = newframe->bytecode; *tc->interp_op = newframe->bytecode; *tc->interp_frame_base = newframe->registers; fprintf(stderr, "invocation success!\n"); } __attribute__((noinline)) void do_return(TC *tc) { fprintf(stderr, "returning from frame\n"); Frame *prev_frame = tc->cur_frame; Frame *newframe = prev_frame->caller; tc->cur_frame = prev_frame->caller; *tc->interp_bc_start = newframe->bytecode; *tc->interp_op = prev_frame->return_address; *tc->interp_frame_base = newframe->registers; free(prev_frame); } OP(const_i8) { // fprintf(stderr, " const_i8\n"); GET_REG(0) = GET_I8(1); op += 2; debug_tc_vars(tc); GOTO_NEXT_OP; } OP(set) { // fprintf(stderr, " set\n"); GET_REG(0) = GET_REG(1); op += 2; debug_tc_vars(tc); GOTO_NEXT_OP; } OP(add) { // fprintf(stderr, " add\n"); GET_REG(0) = GET_REG(1) + GET_REG(2); op += 3; debug_tc_vars(tc); GOTO_NEXT_OP; } OP(sub) { // fprintf(stderr, " sub\n"); GET_REG(0) = GET_REG(1) - GET_REG(2); op += 3; GOTO_NEXT_OP; } OP(print) { // fprintf(stderr, " print\n"); uint8_t val = GET_REG(0); fprintf(stderr, "printed value in reg %d: %d\n", GET_I8(0), val); op += 1; debug_tc_vars(tc); GOTO_NEXT_OP; } OP(if) { // fprintf(stderr, " if\n"); uint8_t test = GET_REG(0); if (test) { op = bc_start + GET_I8(1); debug_tc_vars(tc); } else { op += 2; debug_tc_vars(tc); } GOTO_NEXT_OP; } OP(exit) { fprintf(stderr, "interpreter exited out.\n"); debug_tc_vars(tc); return 99; } OP(invoke) { debug_tc_vars(tc); do_invocation(tc, compunit, GET_I8(0), GET_REG(1)); debug_tc_vars(tc); GOTO_NEXT_OP; } OP(return) { debug_tc_vars(tc); do_return(tc); debug_tc_vars(tc); GOTO_NEXT_OP; } terp_op_fptr OP_FUNCS[MAX_BC_OP] = { OPN(const_i8), OPN(set), OPN(add), OPN(sub), OPN(print), OPN(if), OPN(exit), OPN(invoke), OPN(return), }; __attribute__((preserve_none)) int enter_interp(TC *tc, uint8_t *bc_start, uint8_t *op, char *frame_base, CU *compunit) { fprintf(stderr, "setting vars\n"); tc->interp_bc_start = &bc_start; tc->interp_op = &op; tc->interp_frame_base = &frame_base; tc->interp_compunit = &compunit; fprintf(stderr, "initial dispatch\n"); GOTO_NEXT_OP; } int main() { TC *tc = calloc(sizeof(TC), 1); CU *mycu = calloc(sizeof(CU), 1); uint8_t main_program[] = { 0, 2, 100, // R2 = 100 0, 3, 15, // R3 = 15 0, 4, 3, // R4 = 3 0, 1, 1, // R1 = 1 // label(12): 7, 1, 2, // invoke(1, R2) 2, 2, 2, 3, // R2 = R2 + R3 3, 4, 4, 1, // R4 = R4 - R1 5, 4, 12, // if (R4) goto 12 4, 3, // print(R3) 6, // exit }; uint8_t subroutine[] = { 4, 1, // print(R1) 8, // return }; uint8_t *trash_op_address = NULL; tc->interp_op = &trash_op_address; mycu->bytecodes = calloc(sizeof(uint8_t*), 2); mycu->bytecodes[0] = main_program; mycu->bytecodes[1] = subroutine; Frame *startframe = make_frame(tc, mycu, 0); fprintf(stderr, "start interpreter\n"); fprintf(stderr, "interp returned %d", enter_interp(tc, startframe->bytecode, startframe->bytecode, startframe->registers, mycu)); return 0; }
Ever wanted to know who's that great chap running Compiler Explorer? Mike Godbolt gave a talk:
[#Compiler] Day 1 of #AoCO2025 Study Notes
While the original uses #CompilerExplorer, I wanted to replicate the analysis locally.
In this post, I have used #gcc, #clang, llvm-objdump and #LLDB to analyze.
Read more here: https://gapry.github.io/2026/01/01/Advent-of-Compiler-Optimisations-Study-Notes-01.html
RE: https://hachyderm.io/@mattgodbolt/115689717135274179
Another #AbsoluteBanger from @mattgodbolt's Advent of Compiler Optimisations, in which the compiler knows when to stop being clever because it knows modern CPUs with all their mad out-of-order pipelining jibber-jabber don't need any help - and he shows us a tool on #CompilerExplorer for looking into that. Read this one, but then watch the video too.
Slurrrrrrp....... Ahhh! @mattgodbolt
@uecker @Codeberg @mattgodbolt
Wat?
As a Codeberg member myself, I checked your example here as well. The @compiler_explorer UI seems to handle the link just fine: if you click on the offered link, the raw file content is shown as you would expect. Then, the compiler is complaining about the file where the CE-internal machinery has supposedly put down the source text that it has downloaded from Codeberg.
So, there seem to be two different code paths in play. One, close to the UI, that works. And another one, hidden in the background, that doesn't.
More thanks to Matt Godbolt for today's episode of #AdventOfCompilerOptimisations, where the only horrors are in the tortured C code that gets turned into beautiful elegant assembler. The golden moment was the footnote alerting me to the existence of #CompilerExplorer's "optimisation pipeline explorer".
I didn’t realize that #CompilerExplorer had become this…complicated 😰 — kudos to Matt Godbolt, et al for running it:
“How Compiler Explorer Works In 2025”, Matt Godbolt (https://xania.org/202506/how-compiler-explorer-works).
On HN: https://news.ycombinator.com/item?id=44183299
On Lobsters: https://lobste.rs/s/ocu642/how_compiler_explorer_works_2025