Home · ASPLOS 2024 · Compilers & Programming Languages

SIRO: Empowering Version Compatibility in Intermediate Representations via Program Synthesis

Bowen Zhang, Wei Chen, Peisen Yao, Chengpeng Wang, Wensheng Tang and Charles Zhang

Conference
ASPLOS 2024
Published
2024
Pages
882-899
Citations
3

Static analyzers, symbolic executors, and fuzzers all work on a compiler's intermediate representation, but compilers change that representation with every release, so a tool built for one version of LLVM's IR simply cannot read a program compiled by a newer one, and rewriting the tool by hand is punishing; this work from HKUST builds SIRO, which mostly auto-generates a translator between two IR versions — a reusable skeleton handles the whole program structure and program synthesis writes the per-instruction translators, using the results of running small test programs as the correctness oracle — producing ten LLVM IR translators and, deployed on the Linux kernel, uncovering 80 new bugs.

The argument in one glance

  1. Problem

    Analysis tools break when the compiler changes its IR

    Static analyzers, symbolic executors, and fuzzers work on a compiler's intermediate representation (IR), like LLVM IR. But every compiler release changes the IR's text format, library APIs, and instruction meanings, so a tool built for one version cannot read a program compiled by a newer one, and cannot be combined with a tool targeting another version.

  2. What people did

    Refactor the tool, or downgrade-compile the programs

    Either rewrite the whole analysis tool up to the new IR version, or recompile every program back down to the old IR version the tool understands.

  3. Why it fell short

    Both are punishing manual work; hand-written translators failed

    Refactoring a large analyzer or downgrading every program is heavy, error-prone manual labor. The clean alternative — an IR translator between versions — had been attempted in the LLVM community and failed, because writing it by hand across all the textual, API, and instruction changes was too laborious to sustain.

  4. This paper's idea

    Mostly auto-generate the translator (SIRO)

    SIRO builds an IR translator with little manual effort. A version-agnostic skeleton, reusing the compiler's own IR library, translates the whole hierarchical program structure, leaving only per-instruction translators to fill in. Those are written by program synthesis, using the results of running small test programs as the correctness oracle. SIRO produced ten LLVM IR translators and, deployed on the Linux kernel, found 80 new bugs.

Why this problem is worth solving

A large fraction of program-analysis and security tooling is built not on source code but on a compiler’s internal representation, which ties those tools to the compiler’s release treadmill.

When a compiler processes a program, it does not go straight from source to machine code; it first produces an intermediate representation (IR), a structured mid-level form of the program that is easier to analyze and transform than either source or assembly. LLVM IR is the most widely used example. An enormous amount of software operates directly on this IR rather than on source: static analyzers traverse the IR to reason about program facts, symbolic executors interpret it to compute path conditions, and fuzzers instrument it to get runtime feedback. Because these tools are so tightly bound to the IR, the paper calls them IR-based software. Working at the IR level is powerful — it is language-agnostic, already normalized by the compiler, and rich with the structure analyses need — which is exactly why so much of the program-analysis and security ecosystem is built there. But it also means these tools inherit a dependency on the precise version of the IR they were written against.

That dependency turns routine compiler upgrades into a compatibility wall that fragments the whole ecosystem.

Compilers update frequently, and each new version changes the IR in ways that break tools built for an older one. The changes come in three flavors. First, text incompatibility: the on-disk textual and bitcode format of the IR shifts, so a newer program file cannot even be parsed by an older tool. Second, API incompatibility: the IR library inside the compiler — the getters that read parts of an instruction and the builders that construct them — changes its interfaces from release to release, so code written against the old library no longer compiles or behaves differently. Third, semantic incompatibility: the meaning of instructions evolves, and new instruction kinds appear that older versions never had. Because of all this, a tool built on one IR version simply cannot accept a program emitted by a newer compiler. Worse, it blocks composition: you cannot plug an existing static analyzer built on an old IR into a newer fuzzer, because they speak different IR versions. The result is a fragmented ecosystem where valuable, mature analysis tools become stranded on old compiler versions, unable to analyze modern software.

What people did before, and where it broke

  • Refactor the analysis tool up to the new IR version. Rewrite the analyzer, symbolic executor, or fuzzer against the newer IR library and semantics. Where it broke. For a large, mature tool this is a massive, error-prone engineering effort that must be repeated on every compiler release, so tools fall behind.
  • Downgrade-compile programs to the old IR version. Recompile the target program with an older compiler so the existing tool can accept it. Where it broke. This requires a working old toolchain for every program, is manual and fragile, and is sometimes impossible for code that depends on newer compiler features.
  • Write an IR translator between versions by hand. Build a converter that rewrites IR programs from one version to another so tools can consume them unchanged. Where it broke. Attempts in the LLVM community failed, because manually handling all the textual, API, and instruction changes across versions was too laborious to build and maintain.
  • Just live with fragmentation. Keep each tool pinned to its IR version and accept that they cannot be combined or updated cheaply. Where it broke. Mature analyzers get stranded on old compilers and cannot analyze modern programs, and cross-tool collaboration is blocked.
  • The gap. No approach produced correct IR translators cheaply enough to keep up with compiler releases. Providing that, by auto-generating most of each translator, is what SIRO does.

How SIRO works

From hand-writing an IR translator to mostly synthesizing one

  1. new

  2. new

  3. new

  4. new

  5. new

The idea

Reuse the compiler’s own machinery behind a version-agnostic skeleton. SIRO’s first move is to avoid reimplementing what the compiler already provides. Instead of parsing IR text, it works on the compiler’s in-memory IR objects using the compiler’s own library to read and rebuild them, which sidesteps the textual format changes and much of the API churn. On top of that it builds a divide-and-conquer skeleton that recursively walks the IR’s hierarchy — module, function, basic block, instruction — extracting and reconstructing each level in a way that does not depend on the version, so the only version-specific work left is translating individual instructions. Structuring a hard problem around a stable, reusable framework and leaning on the compiler’s existing representation rather than rebuilding it is the same move the compiler-IR framework work covered elsewhere on this site makes, treating the intermediate representation and its libraries as reusable infrastructure to build on top of rather than machinery to reinvent for each new task.

Auto-generate the laborious part with program synthesis. The bulk of a translator is the per-instruction logic, and most instructions are common across versions, so SIRO reframes writing each common-instruction translator as a program synthesis problem: it abstracts a translator as predicates that pick out a sub-kind of the instruction plus atomic translators assembled from the library’s getters, builders, and operand translators, and asks a synthesizer to find the right combination automatically. This replaces the developer’s manual API-wrangling with an automated search over the compiler’s own building blocks. Casting a tedious, error-prone implementation task as a search for a correct program over a space of primitive operations, so a machine writes the code instead of a person, is the same approach the sketch-guided synthesis work covered elsewhere on this site takes, turning what would be painstaking hand construction into an automated synthesis problem whose answer is a correct program.

Let running the program be the correctness oracle. The synthesis is hard because the search space of API combinations is vast and correct translators are rare, and a candidate cannot be checked in isolation since a real IR program mixes many instructions. SIRO’s answer is to validate by execution: it takes small IR programs as test cases whose return value is known, assembles per-test translators for exactly the instructions a given test uses, translates and runs the program, and keeps the candidate translators only when the output matches the expected oracle — refining the mapping from evidence rather than proving each piece abstractly. Using the observable behavior of running a translated or transformed program as the ground-truth signal that guides and validates an automated construction is the same discipline the automated-testing transpilation work covered elsewhere on this site is built on, generating and running test cases so that behavioral equivalence, checked by execution, drives the search toward a correct translation.

Following a translator being built by SIRO

Synthesizing an LLVM IR translator between two versions

A trace of SIRO producing a version-pair translator, following the paper's skeleton and synthesis design.

Step 1 of 5

Point SIRO at two IR versions

A developer wants a translator from an older LLVM IR version (understood by their analyzer) to a newer one, or vice versa. They give SIRO the two compilers' IR libraries and a set of small test IR programs.

The test programs are simple; each has a known return value.

Why it works

  • In-memory translation dodges the format churn. Because SIRO operates on the compiler’s in-memory IR objects rather than text → the changing textual and bitcode formats never have to be parsed by hand → so text incompatibility and much API incompatibility are overcome by reusing the library.
  • A hierarchical skeleton is version-agnostic. Because IR has a fixed nesting of module, function, block, and instruction → a divide-and-conquer skeleton that extracts and reconstructs each level works unchanged across versions → so only the instruction translators remain version-specific.
  • Common instructions dominate and are synthesizable. Because most instructions persist across versions and form the bulk of the work → casting their translators as synthesis over the library’s getters and builders automates that bulk → so manual effort shrinks to a small number of genuinely new instructions.
  • Execution gives a cheap, sound oracle. Because a small IR program has a known return value → running a translated program and comparing the output validates whole per-test translators at once → so the otherwise-uncheckable candidates can be filtered by real behavior.
  • Per-test translators tame the search. Because a candidate cannot be validated in isolation among many instructions → profiling each test and assembling translators only for the instructions it uses makes validation tractable → so the vast search space is reduced iteratively toward correct translators.

What it costs (trade-offs and scope)

  • New instructions still need manual handling. Program synthesis covers common instructions, but genuinely new instruction kinds in a higher version are not synthesized. They must be judged unnecessary and skipped, or hand-written as an analysis-preserving translation, so some manual expertise remains, albeit small.
  • Correctness is validated, not proven. SIRO refines candidate translators using test-case execution as an oracle, so confidence comes from testing rather than formal proof. The reported accuracies (91 percent for bug detection, 95 percent for fuzzing reproduction) are strong but not guarantees, and quality depends on the test cases provided.
  • It relies on a usable IR library and executable tests. The in-memory approach depends on the compiler exposing getters and builders SIRO can compose, and the oracle depends on being able to run small IR programs and observe their return values. IRs or settings without these would not fit as cleanly.
  • Analysis-preserving is the bar, not bit-exact equivalence. For hard cases SIRO aims to preserve program-analysis results (control-flow and data-flow effects) rather than perfect semantic equivalence, which is the right target for analysis tools but means a translated program is faithful for analysis, not necessarily identical in every respect.
  • Effort, though small, is not zero. Producing a translator still requires collecting representative test cases and handling new instructions; ten translators took about ten days of one person, each under three hours, which is dramatically cheaper than hand-writing but not free.
  • Demonstrated on LLVM IR. SIRO is instantiated and evaluated on LLVM IR version pairs. The framework is general in principle, but its concrete effectiveness on very different IRs would depend on their structure, libraries, and how test-executable they are.

The evaluation

Ways to keep an IR-based tool working across compiler versions

PropertyRefactor the tool / downgrade programsHand-written IR translatorSIRO
Manual effort
Handles text, API, and instruction changesManually, error-prone
How correctness is gainedManual careManual care
Cost per version pairRecurring large effort
Demonstrated outcomeTools lag or stay pinnedNot achieved
Ways to keep an IR-based analysis tool working as compilers change their intermediate representation, as compared in the paper. Refactoring the whole tool to the new IR version, or downgrade-compiling every program to the old version, is heavy, error-prone manual work that recurs on every release. Writing an IR translator between versions by hand is the clean idea, but LLVM-community attempts failed because manually handling all the textual, API, and instruction changes was unsustainable. SIRO makes translator construction cheap by reusing the compiler's own IR library to translate in-memory IR objects (overcoming text incompatibility and much API incompatibility), a version-agnostic divide-and-conquer skeleton that reconstructs the module-function-block-instruction hierarchy so only per-instruction translators remain, and program synthesis that finds those instruction translators automatically by generating candidate atomic translators from the library's getters and builders and refining them with small test IR programs whose return values serve as a correctness oracle. New instructions are either skipped when the tool never sees them or hand-written to preserve analysis results. SIRO produced ten LLVM IR translators (about ten days of one person's effort, each under three hours), reached 91 percent accuracy assisting bug detection and 95 percent for fuzzing target reproduction, and is deployed in industry, where it let a kernel bug detector obtain the Linux kernel's IR and uncover 80 new bugs; the paper received an ACM SIGSOFT Distinguished Paper Award.Underlined values explain themselves when clicked.

Check yourself

Check your understanding

  1. 1. Why does a compiler updating its intermediate representation break IR-based tools like static analyzers?

  2. 2. How does SIRO avoid hand-writing an entire IR translator for each version pair?

  3. 3. How does SIRO know whether a synthesized instruction translator is correct?

  4. 4. What did SIRO achieve, and what are its limits?

Sources

The paper is “Siro: Empowering Version Compatibility in Intermediate Representations via Program Synthesis” by Bowen Zhang, Wei Chen, Peisen Yao, Chengpeng Wang, Wensheng Tang, and Charles Zhang, published at ASPLOS 2024, DOI 10.1145/3620666.3651366, where it received an ACM SIGSOFT Distinguished Paper Award. The authors are at the Hong Kong University of Science and Technology and Zhejiang University.

This explainer is based on the full text of the paper, obtained from an author’s copy hosted on Chengpeng Wang’s academic homepage. The problem framing (program-analysis tools such as static analyzers, symbolic executors, and fuzzers operating on a compiler’s intermediate representation, most commonly LLVM IR; the IR version compatibility issue arising from frequent compiler updates; the three kinds of incompatibility — text, API, and semantic; why refactoring tools or downgrade-compiling programs is punishing; and why hand-written IR translators had failed in the LLVM community), the SIRO design (working on in-memory IR via the compiler’s own library to overcome text and much API incompatibility; the version-agnostic divide-and-conquer skeleton following the extract-and-reconstruct principle over the IR hierarchy; abstracting a common-instruction translator as predicates selecting sub-kinds plus atomic translators built from getters, builders, and operand translators; the synthesis challenges of a vast search space and unconventional validation; the synthesis system of type-guided candidate generation, per-test translators guided by profiling, test-based validation using program return values as an oracle, and refinement; and handling new instructions by checking necessity or using analysis-preserving one-to-many translation), and the results (ten LLVM IR translators produced in about ten days of one person’s effort with each under three hours, 91 percent accuracy for bug detection and 95 percent for fuzzing target reproduction, and industrial deployment assisting a kernel bug detector to obtain the Linux kernel’s IR and uncover 80 new bugs) are all drawn directly from the paper. Background explanations of intermediate representations, LLVM IR, static analysis, fuzzing, and program synthesis are standard and provided to make the paper’s contribution understandable.

Related papers on Compilers & Programming Languages

See all 162 →