IDEAS for TRACTOR:

Improved Decoding and Equivalence Testing at Scale for Translating All C to Rust

Posted August 24, 2026

author-image

By

Authors:

Marius Arvinte, CTO Office
Cory Cornelius, CTO Office

Memory safety errors still account for the majority (up to 70%) of high-severity real-world security vulnerabilities. On the software side, memory-safe languages prevent classes of bugs from existing and require developers to follow safe programming idioms. Rust is a low-level language suitable for system software development due to its high performance and modern tooling. Arguably, two of the most important features that increased its industry-wide adoption in recent years are that:

  1. If Rust code and all its dependencies do not use “unsafe” (note: unsafe Rust is a hidden language inside Rust that is not the focus of our tool) and compiles, it has spatial and temporal memory safety, and thread safety. This is a major security gain but comes with a somewhat-steeper learning curve than C and specific patterns that must be learned for ergonomic and future-proof programming.
  2. It has a thriving open-source ecosystem with varied tooling (often implemented in safe Rust, so reaping all safety benefits themselves!) for source-based coverage, automated test data collection, and process isolation across parallel tests.

Intel works on C-to-Rust translation as part of the DARPA TRACTOR (Translating All C-to-Rust) program. This is an ambitious, hands-on research program that aims to automate C translation to safe Rust.

This post covers our IDEAS (Improved Decoding and Equivalence Testing at Scale) tool, and what we learned from using it to translate various open-source C projects. IDEAS is under active development but is open-sourced and available for experimental use.

The Ideas Behind IDEAS

Given that no universal, deterministic tool for translating C-to-safe-Rust exists yet, IDEAS uses LLMs for the core C-to-Rust translation part, but that doesn’t mean we give LLM agents full control or agency over the translation strategy and idioms.

Instead of betting on complex, multi-stage agent-like architectures, IDEAS tries to be as simple as possible and follow a high-level principle:

“Focus on a memory-safe and correct translation first, because safe code is easier to understand, refactor, and extend.”

We designed IDEAS around three practical challenges (readers are invited to reflect on and answer them for themselves!):

  1. How can a 100% LLM-based C-to-Rust translator be controlled as much as possible? There should be a clearly defined translation strategy that prevents wasteful context, tool calls, or frequent refactoring of the code during translation. If the translation is stable as it progresses, it is easier for a Rust developer to review and coherently modify it.
  2. What kind of measurements can we obtain for partial C-to-Rust translations? If a tool can only translate ~50% of a codebase, could it measure if the translation up to that point is test-suite equivalent to the original C code?
  3. How can we improve the idiomaticity (subjective code quality) of the translated Rust code? Is it feasible to directly translate C-to-idiomatic-Rust, or is it more cost-effective to run a post-translation refactoring stage?

We’ll next explain how IDEAS addresses these three questions.

Controlled, Granular Translation

IDEAS is a harness around basic LLM API calls that builds its own context: it does not give a model agency in determining which code snippet to translate, where to write it, or any execution privileges over partial translations. Instead, we handle these auxiliary components using traditional software.

Figure 1: High-level block diagram of IDEAS.


Figure 1 shows a high-level block diagram of IDEAS. Except for the “LLM call” block, all other components are deterministic, including how IDEAS builds the context for each LLM call. The LLM’s goal is to translate a C snippet (either a sole symbol or a symbol cycle) of code to Rust, given already-translated Rust code and future C uses in the context.

After static analysis of the build system and project structure, IDEAS builds the context required to translate a C symbol (group)—a function, data structure, or global variable—into safe Rust using pre-determined rules:
 

  • Rust function body compression for deciding which already-translated Rust code should be in the LLM’s context, and which not.
    • We found that bringing complete function implementations one hop away in the call graph and compacting everything else to signatures has no detrimental impact on translation quality, and this respects Rust’s “Golden Rule”: everything needed for memory safety is already contained in a function’s signature.
  • C example uses of the symbol that serve as future implementation contracts. Including these in the context is critical for correctly inferring semantics behind data structures and implementations, but there may be too many C example uses to fit in the context or budget (e.g., hundreds of C functions that all use the same data structure may not even fit in the input context limits of frontier LLMs).
    • The heuristics are static-analysis based and are inspired by how LLM agents discover pointer semantics (from our own experience) when pointers are obscured as void types. Figure 2 shows a snippet of the multi-dimensional scoring.

 

@dataclass(frozen=True)
class TypeShape:
    # Constructs in decreasing order of difficulty
    void_pointers: int = 0
    function_pointers: int = 0
    unions: int = 0  # untagged unions need manual discrimination
    pointers: int = 0
    arrays: int = 0  # arrays and flexible array members
    fields: int = 0

    @property
    def rank(self) -> tuple[int, ...]:
        # Compared lexicographically, so the presence of
        # a harder construct outweighs any
        # amount of an easier one and no weights have
        # to be invented to say which is worse
        return astuple(self)

Figure 2: The multi-dimensional key (in priority order from top-to-bottom) IDEAS heuristic parsing uses to score C calling functions for being included in context. The key receives one point in each dimension based on static analysis of memory-sensitive aspects. C context functions are included in a limited-budget context according to their ranking. The complete logic is available in the IDEAS repository.

 

Once the context for a symbol is built, it is dispatched to an LLM API for translation. Our translation prompt sits at approximately ~3,500 tokens and describes several C-to-Rust translation idioms.

Generate A Safe Rust Translation

IDEAS is designed to structurally guarantee that the core translation is separated from “unsafe” usage, and that no “unsafe” code leaks into the safe translator’s context by placing the following attribute at the top of each translated Rust module: #![forbid(unsafe_code)].

The one-liner leverages the Rust compiler to detect any “unsafe” keyword usage at compile-time and succinctly report a build error if any “unsafe” blocks are present. Transparent to the user, the safe translation is re-attempted for a configurable number of attempts until it builds successfully, or all attempts are exhausted and the partial translation up to that symbol is output.

C Foreign Function Interface (FFI) Wrappers

Translating C to safe Rust is valuable but most of the time results in incompatible APIs. C FFI backwards compatibility wrappers can, and in many cases, must use “unsafe” blocks (but they may still be sound), e.g., when extracting raw C pointers from smart Rust pointers to pass as C-compatible return values.

This type of “unsafe” use at the C boundary is only acceptable for intermediate translation states, given that, ideally, one would eventually translate the entire codebase to safe Rust and discard the backwards compatibility layer.

IDEAS places structural constraints on what a wrapper function’s signature should be by combining in-context templates with post-generation verification for LLMs: the C-compatible signature of each wrapper is generated using bindgen, and its body is left unimplemented. The LLM is prompted to only populate the wrapper’s body—any deviations in the signature are statically detected after each response and the output is rejected if the signature is not byte-identical to the bindgen output, as any deviation could break backwards compatibility.

Validating Partial Translations

Once a “safe core” is translated (either for a symbol or the entire project), equivalence is checked by restoring API compatibility with the legacy C code and producing a hybrid C/Rust artifact.

If tests are available (more on this later) and a failure is identified, we re-attempt translation from scratch, otherwise the symbol and its wrapper are stored and translation proceeds in dependency graph order. Figure 3 shows how IDEAS translates and validates code incrementally.
 

Figure 3: IDEAS starts from C code and incrementally translates it symbol-by-symbol (or by group if symbols are in a use cycle): a core safe Rust translation is produced, and it keeps accumulating. C-compatible wrappers that call into the safe Rust translation are generated and replace the legacy C code in the codebase: if the hybrid artifact passes all tests, then translation progresses. If everything is successful, the final step removes all “unsafe” usage but also loses backwards compatibility.

 

Result: Correct And Idiomatic Data Structure Translation

One of the most important aspects of the TRACTOR program is to constantly question ourselves “What would a Rust developer write?”. Rust is a strongly typed programming language, so how data structures are translated plays a critical role in how future translations can (e.g., functions that use them) access them.

Because data structures sit at the bottom of the call graph, a major problem that we faced is deciding early in the translation if a translated Rust data structure captures all relevant C semantics and can be “locked in” for the remainder of the translation.

If translated incorrectly early on, a data structure could prevent correct or idiomatic translations of its consumer functions much later in the future, which “spoils” the entire incremental translation and requires major refactoring.

The earlier we can validate a translated data structure, the faster and more cost efficient the overall translation becomes. IDEAS implements helpers for testing translated Rust data structures ahead of their use sites:
 

  • C-to-Rust and Rust-to-C conversion functions and roundtrip tests. This allows us to test and validate in isolation the information carried by Rust data structures before any of their call sites are translated.
  • C context selection improvements focused on searches for void pointer specializations in the function call tree.


Figure 4, Figure 5, and Figure 6 show an example of a translated Rust data structure and its original C version, C/Rust conversion helpers for them, and roundtrip tests for the converters. Apart from the original C data structure, all other snippets are jointly generated by IDEAS using a single LLM call. Given the right context and prompt, LLMs can infer how to generate converters in one shot and tests for them, but the risk of reward hacking (e.g., vacuous converters and/or always-passing tests) remains present, nevertheless.

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct stbds_string_block {
    pub next: *mut stbds_string_block,
    pub storage: [::std::os::raw::c_char; 8usize],
}

#[derive(Debug, Clone, PartialEq)]
pub struct StbdsStringBlock {
    pub next: Option<Box<StbdsStringBlock>>,
    pub storage: [u8; 8],
}

Figure 4: A C data structure (top; represented in safe Rust using a C-compatible layout) that contains a circular pointer reference and its Rust translation (bottom; safe Rust) that uses smart pointers to manage dynamically sized arrays. This translation was generated using GPT-5.6 Sol.

 

pub unsafe fn c_to_r(cs: *const stbds_string_block) -> StbdsStringBlock {
    let cs = unsafe { &*cs };
    StbdsStringBlock {
        next: (!cs.next.is_null()).then(|| Box::new(unsafe { c_to_r(cs.next) })),
        storage: cs.storage.map(|byte| byte as u8),
    }
}

pub unsafe fn r_to_c(rs: &StbdsStringBlock) -> *mut stbds_string_block {
    Box::into_raw(Box::new(stbds_string_block {
        next: rs
            .next
            .as_deref()
            .map_or(::std::ptr::null_mut(), |next| unsafe { r_to_c(next) }),
        storage: rs.storage.map(|byte| byte as ::std::os::raw::c_char),
    }))
}

Figure 5: C-to-Rust and Rust-to-C conversion functions between the two data structures in Figure 2. IDEAS generates these helpers together with the translation in Figure 2. They must use “unsafe”, because they cross the C FFI boundary, but are not part of the final translation. The conversion functions were generated using GPT-5.6 Sol.

 

#[test]
fn round_trip_non_null_next() {
    let mut child = stbds_string_block {
        next: ::std::ptr::null_mut(),
        storage: [9, 10, 11, 12, 13, 14, 15, 16],
    };
    let original = stbds_string_block {
        next: &mut child,
        storage: [1, 2, 3, 4, 5, 6, 7, 8],
    };

    let back = unsafe { r_to_c(&c_to_r(&original)) };
    let back_child = unsafe { (*back).next };
    assert_eq!(unsafe { (*back).storage }, original.storage);
    assert_eq!(unsafe { (*back_child).storage }, child.storage);
    assert!(unsafe { (*back_child).next.is_null() });

    unsafe {
        drop(Box::from_raw(back_child));
        drop(Box::from_raw(back));
    }
}

Figure 6: Roundtrip conversion test between C-to-Rust-to-C (C-R-C) conversion of the C data structure and its Rust translation. This type of test is generated by IDEAS together with the converter functions themselves. Note that only the C-R-C roundtrip is tested, because the R-C-R roundtrip involves a “relaxation” from Rust to C that can always be performed without losing information. The roundtrip tests were generated using GPT-5.6 Sol.

 

Figure 7 shows IDEAS successfully building the relevant C context such that GPT-5.6 Sol can consistently (10/10 retries) specialize a void pointer in the translated Rust data structure shown in Figure 8.

// The C data structure to translate
typedef struct {
    size_t length;
    size_t capacity;
    void *hash_table;
    ptrdiff_t temp;
} stbds_array_header;

// Helper data structure (indirect in the use graph)
typedef struct {
    char *temp_key;
    size_t slot_count;
    size_t used_count;
    size_t used_count_threshold;
    size_t used_count_shrink_threshold;
    size_t tombstone_count;
    size_t tombstone_count_threshold;
    size_t seed;
    size_t slot_count_log2;
    stbds_string_arena string;
    stbds_hash_bucket *storage;
} stbds_hash_index;

// C use function that brings the helper data structure into play
static ptrdiff_t stbds_hm_find_slot(
    void *a,
    size_t elemsize,
    void *key,
    size_t keysize,
    size_t keyoffset,
    int mode
) {
    void *raw_a = ((char *)(a) - (elemsize));
    stbds_array_header *header = (stbds_array_header *)(raw_a) - 1;
    stbds_hash_index *table = (stbds_hash_index *)header->hash_table;
    // Functionality that uses `table`...
}

Figure 7: The C context collected by IDEAS to translate the data structure (top): its void pointer field `hash_table` is of type `stbds_hash_index` (middle), but this is only inferred through a function that does the concrete specialization (bottom).

 

pub struct stbds_array_header {
    pub length: usize,
    pub capacity: usize,
    pub hash_table: Option<Box<stbds_hash_index>>,
    pub temp: isize,
}

Figure 8: The safe Rust translation of the original C data structure. The auxiliary data structure was already translated by IDEAS in a previous step and included in the translator’s context (not shown here).

Result: Translating Large-Scale Codebases

We used IDEAS to translate the official libgit2-cli example from C to safe Rust. We opted for not using its existing test suite during translation to get an answer to the following question:

“Can a human developer repair an automated, memory-safe LLM rewrite with low cost and effort?”

Using Claude Opus 4.6, IDEAS translated the C code (~112,000 input lines) to safe Rust (~116,000 output lines). There were ~4,600 symbols that IDEAS translated in dependency graph order, including a cycle of 133 symbols (functions and data structures) that was jointly translated. The average per-symbol translation cost was approximately 55 cents.

The final translation did not contain any “unsafe” blocks and built successfully, because the #![forbid(unsafe_code)] attribute was present at the top of all modules. This may seem impressive, but it serves more as proof that the Rust compiler developers wrote error messages that are crisp and helpful for developer experience, which transfer well to LLMs—at no point during translation did an attempt fail more than four times in a row before successfully compiling when merged with the previously translated code.

However, the final translation was not functionally equivalent, despite compiling. While the basic `git help` subcommand was executing correctly, other subcommands like `git blame` were crashing immediately (albeit, with proper panic unwinding).

We found that fixing the initial translation was surprisingly inexpensive and straightforward with VS Code Copilot and Claude Sonnet 4.6. After an additional cost of approximately 6.5 USD and 8 cumulative person-hours across four prompting sessions, we were able to run the official libgit2-cli benchmark script without crashing and validate bit-exact functionality of the `git blame` subcommand when executed on the `git` repository itself.

The agent harness + developer guidance (with careful reading of reasoning traces) was able to navigate the 120,000 lines of Rust code, identify, and fix the following issues in the translation:
 

  • An incorrect pointer (memory address) comparison was replaced with its corrected value-based comparison, that compares the pointed-to data structures. This was a clean, one-liner fix shown in Figure 9 that enabled the `git blame` command to progress.
  • Several file system related functionalities were left stubbed out during the original IDEAS translation and were identified and shimmed in by the repair agent harness.
  • The object database (ODB) backend was missing the `writestream` and several other functionalities which also had to be shimmed in.

 

Figure 9: A one-liner fix identified by Claude Sonnet 4.6 in VSCode “Agent” mode when prompted to identify why the `git blame` subcommand crashed immediately when run on any file in a repository. The fix replaces an incorrect address comparison with content comparison because `spec` implements the `PartialEq` trait. Both are memory-safe in Rust, but the former led to incorrect functional behavior (always-false result).

Result: Translating Projects With Memory-safety Sensitive Aspects

We translated the second DARPA TRACTOR battery of projects. These are a set of 80 curated C projects by the TRACTOR MIT Lincoln Labs evaluation team: they have 150-3,000 lines of code per project that exercise specific memory-safety sensitive aspects: void-pointer dereferencing, malloc/free, raw pointer comparison, or function pointers.

Table 1 compares the performance of two LLMs: Claude Sonnet 4.6 and GPT-5.4. In the second and fourth columns, we have the percentage of individual tests that pass across all projects. Some projects may only pass a subset of tests. In the third and fifth columns, the metric is stricter: a project is successful only if it passes all its tests after translation.

Table 1 also shows the added value of translation-time test feedback, which improves the performance of both models. GPT-5.4 benefits more from test feedback and completes seven additional projects, as well as bringing more projects into a partially correct translation state. Even though the test pass percentage only increases by 0.4% for Claude Sonnet 4.6, this helps it complete two more successful translations.

Table 1. End-to-end translation results when running IDEAS on the second DARPA TRACTOR evaluation battery, using translation-time test feedback from expert-written C/Rust equivalence tests. The number of tests varies significantly per project (some projects have as few as a single test), and tests only check for the equivalence of defined behavior.

LLM Single shot [% tests] Single shot [projects / total] Translation-time test feedback [% tests] Translation-time test feedback [projects / total]
GPT-5.4 87.8% 63 / 80 92.1% 70 / 80
Claude Sonnet 4.6 99% 73 / 80 99.4% 75 / 80

Result: C/Rust Equivalence Test Generation

The translations in Table 1 used well-curated tests for each C project as a feedback signal to the LLM. These tests were conveniently designed for C/Rust interoperability and plug-in testing.

However, some C libraries (like the very same libgit2 we talked about earlier) may have fragmented test suites that do not achieve complete branch coverage. Because C has no standardized testing framework, converting C tests to work on Rust translations (even if backwards compatible) is difficult in general.

IDEAS circumvents the missing-test problem by self-generating C/Rust equivalence tests from scratch. Test generation is decoupled from translation and uses a fully autonomous agent implemented in the KISS framework: the agent is equipped with four tools (Read, Write, Edit, Bash), fed a single prompt describing the requirements and test tooling, and launched without root privileges in an isolated Docker environment where it only has visibility into a single C project.

Figure 10 shows branch coverage results obtained when running Claude Mythos Preview (accessed as part of Intel’s participation in Project Glasswing) on the 68 libraries from the same DARPA TRACTOR second evaluation battery mentioned earlier.
 

Figure 10: The distribution of per-project branch coverage at the end of each test generation agent session using Claude Mythos Preview. A total of 68 C libraries were used, each in its own isolated Docker environment. The test generation agent was dispatched on the C code (linked into Rust through the C FFI), separate from any translation taking place. The custom harness, prompt, and instrumentation used by the agent are available online.

In all cases, Claude Mythos Preview was asked to achieve 100% branch coverage without exercising undefined behavior (our instrumentation would catch it) and by only exercising externally linked (public) C functions.

In many cases, this is an impossible ask. The two low-coverage outliers in Figure 10 are correct early stops: manual inspection reveals that the two specific libraries cannot be covered any further through public functions alone.

Overall, an average (median) branch coverage of 88.28% (91.18%) was achieved at an average (median) cost of 13.2 (4.2) USD per project. The most surprising finding is that Claude Mythos Preview can consistently perform few-shot test generation: it confidently stops early, with a median number of only two batch test writes per project. This shows its potential for targeting isolated modules of a large codebase.

When using these tests as equivalence feedback during translation of the same projects in the previous section, we see no degradation of the results in Table 1, indicating they are as informative to a translator as expert-written ones.

Perhaps the most important developer lesson for us was that Rust itself is a very good environment for C/Rust equivalence testing: it has standardized and modern test runners that guarantee per-process isolation, a robust C FFI, and tools that can measure branch coverage in hybrid C/Rust codebases.

Open Challenges

How would we translate low-level C firmware to memory safe Rust? Any assembly or low-level hardware library call means that a safe Rust translation is no longer possible. For example, unsafe Rust is used in the Linux kernel together with SAFETY comments, which require developers to read and obey the invariants they encode.

Memory safety requires a defense-in-depth approach that combines hardware and software techniques. While safe Rust guarantees are lost when interacting with low-level hardware, a complementary hardware approach like Intel Memory Tagging Technology (MTT) is another layer of security that mitigates overlapping classes of issues, such as, but not limited to buffer overflows or use-after-free.

Some bare metal platforms may not support the Rust standard library or the Rust compiler itself. Rust has cross-compilation support and the `no_std` functionality to disable the standard library, as well as a 3-tier platform support specification that continues to evolve, but supporting exotic platforms may require in-house efforts.

File-related functionality stubbed out in the large-scale translation is a symptom of a larger problem. The only feedback signal for the LLM was compiler feedback, so function stubs can easily pass compilation in general. Solving this problem without access to test cases for each function remains a challenge, but it could be addressed through additional static and dynamic verification, as well as cost-effective LLM judges (e.g., operating on batches of symbols).

The agent-based test generation experiment used projects with at most 3,500 lines of code, with cjson arguably being the most difficult to cover. Scaling to millions of lines of code requires careful thought and the design of an agent harness that can partition a project in a reliable way. Existing harnesses use sub-agents, but these are usually dispatched on a per-file/module basis: how existing harnesses scale when faced with an amalgamated code source is interesting to think about.

IDEAS is open-sourced and available to experiment with today:
 

Acknowledgments

We’d like to thank Marcela Melara, Weilin Xu, Marcin Spoczynski, and Anjo Vahldiek-Oberwagner, and Pradeep Dubey from the Intel CTO Team for their technical contributions and advice. Thanks to Dan Brown and Cindy Polsky from Intel Government Technologies for their project management support. Thanks to Jason Fung, Tony Martin, Carlos Rozas, and Asmae Mhassni from the INT31 team for their support and guidance.

This material is based upon work supported by the Defense Advanced Research Projects Agency (DARPA) Translating All C-to-Rust (TRACTOR) program under Agreement No. HR00112590134.

Share Your Feedback

We want to hear from you. Send comments, questions, and feedback to the INT31 team.

About the Authors

Marius Arvinte (marius.arvinte@intel.com) is a Research Scientist in the Intel CTO Team. His research background is in applied generative modeling. He is Intel's Principal Investigator in the DARPA TRACTOR program. He holds a Ph.D. degree in Electrical and Computer Engineering from The University of Texas at Austin.

Cory Cornelius (cory.cornelius@intel.com) is a Principal Engineer in the Intel CTO Team. His research interests lie at the intersection of machine learning, code generation, and adversarial resilience. He holds a Ph.D. degree in Computer Science from Dartmouth College.