A growing set of hardware-security and debugging techniques works by attaching tags — small pieces of metadata — to a program's instructions, which the hardware then checks at runtime to enforce policies like control-flow integrity; but to use any such scheme you must build a custom compiler that generates and encodes those tags, which is tedious and reinvented for every project and architecture; this paper (Binghamton University — David Demicco, Aravind Prakash, and colleagues) presents COGENT, a flexible, reusable LLVM-based compiler toolchain for RISC-V that can tag instructions at any granularity (per function, basic block, or instruction) and width, emit control-flow labels, and comes with a tag-aware disassembler and integrity checker — at only about 1 to 5 percent runtime overhead.
The argument in one glance
-
Problem
Tagging instructions is powerful, but every scheme needs its own custom compiler
A whole family of hardware-security and testing techniques works by attaching tags — small pieces of metadata — to a program, which the hardware reads at runtime to enforce a policy. A tag on an instruction might say what function it belongs to, mark it as a valid target of a jump, or carry information a monitor uses to detect an attack or a bug. This is how techniques like control-flow integrity (which checks that a program only jumps where it is allowed to) and various runtime monitors are built: the compiler encodes domain-specific metadata into the binary, and specialized hardware consumes it. The problem is upstream of the hardware. To use any such tagging scheme on a given architecture, someone has to build a compiler that actually generates and encodes the tags correctly, at the right places in the code and in the right format. That is tedious and time-consuming, and it gets redone from scratch for every new policy, every new tag format, and every new architecture. Researchers who want to explore a tag-based idea spend much of their effort just building the compiler plumbing to emit tags, before they can even test the idea, and there was no flexible, reusable toolchain to do this.
-
What people did
Build a bespoke tag-emitting compiler for each scheme
The usual approach is to hand-build the tagging support for each project. For a given security policy and tag format, researchers modify or extend a compiler to insert the right metadata at the right granularity, then figure out how to carry that metadata through compilation down to the machine code, and write ad-hoc tools to inspect the tagged binary.
-
Why it fell short
Bespoke tagging is rigid, laborious, and hard to trust
A one-off tagging compiler is expensive and inflexible. It is built for one policy at one granularity with one tag width, so exploring a different design — say, tagging per basic block instead of per instruction, or using wider tags, or adding control-flow labels — means more compiler surgery. Getting metadata to survive all the way from the high-level intermediate representation down to the final instructions, without being dropped or misplaced by optimization and lowering passes, is genuinely tricky, and a bug there silently corrupts the policy the hardware is supposed to enforce. And there is usually no principled way to check that the tags in the final binary are correct. So each tag-based project reinvents the same plumbing, in a way that is rigid, error-prone, and hard to verify, which slows down the whole area of tag-driven hardware security and testing. What is needed is a general, adaptable toolchain that can generate tags of varying granularity and width, preserve them faithfully to the binary, and let you check that they are right.
-
This paper's idea
A flexible, reusable compiler toolchain for RISC-V tagging
COGENT is a highly flexible compiler toolchain for generating instruction tags on the RISC-V architecture, so that tag-based schemes can be built without reinventing the compiler each time. At its center is an LLVM-based compiler, supplemented by a tag-aware disassembler and a tag integrity checker. It is adaptable along the axes that matter for these schemes. It can generate tags at one or more granularities — per function, per basic block, or per individual instruction — so a scheme can be as coarse or fine as it needs. It supports variable-width tags of 1 to 32 bits attached to instructions, and arbitrary-width tags attached to each function or basic block, so the metadata can be as small or as rich as the policy requires. It can also emit control-flow labels, which are exactly what is needed to assert control-flow integrity (CFI), a runtime property that helps catch bugs and exploits that violate a program's intended control flow. The toolchain is evaluated for the correctness of the tags it produces, the performance penalty they add, and how well it preserves tags from the intermediate-representation level down to the machine level. To show it is useful, the authors give three example applications built on it — control-flow integrity, adaptive tracing, and hardware-level function tracing — and report that tagged code adds an average cycle-count overhead of only about 5.24 percent in the worst case and 0.94 percent in the best, low enough to make it practical for debugging, testing, and fuzzing.
Why this problem is worth solving
Tag-based hardware techniques are a major direction in security and debugging, and they all depend on a compiler that can emit the tags. Modern hardware increasingly enforces properties about software by attaching metadata to it and checking that metadata as the program runs. This metadata — tags — can ride alongside instructions or data, and it lets the hardware do things software alone cannot do cheaply: mark which locations a jump may legally target (for control-flow integrity), label instructions so a monitor can trace or profile them, or carry policy information the hardware uses to catch attacks and bugs in real time. Because the checks happen in hardware, they can be fast enough to leave enabled in production or during heavy testing, which is what makes tag-based techniques attractive for both security and debugging. But a tag scheme is only as good as the tags in the binary, and those have to be put there by a compiler. The compiler must decide where tags go (which instructions, functions, or blocks), how wide they are, what they encode, and it must carry them faithfully through every optimization and lowering step until they land in the final machine code. If the tags are wrong, missing, or misplaced, the hardware enforces the wrong thing. So the compiler is the foundation of the entire approach, and making it easy, flexible, and trustworthy to generate tags is a prerequisite for the field to build and compare tag-based ideas at all.
The difficulty is that building this tag-emitting compiler has been a bespoke, brittle effort each time. Consider what it takes to try a new tag-based technique today. You pick a policy — say, a form of control-flow integrity — and a tag design, then you have to make a compiler produce those tags. That means intervening in the compiler to attach metadata at the granularity you chose, and then the hard part: ensuring that metadata survives the long journey from the high-level intermediate representation, through the optimizer, through instruction selection and lowering, all the way to the emitted instructions, without being reordered away, dropped, or attached to the wrong instruction. Compilers are aggressive about transforming code, and metadata that is not first-class can easily be lost. You also need tools to look at the tagged binary and confirm the tags are where they should be, which are typically written ad hoc. And every one of these steps is specific to your policy, your tag width, your granularity, and your architecture, so the next project starts over. The result is that the plumbing — generating and preserving tags — consumes effort that should have gone into the actual idea, and subtle plumbing bugs quietly undermine the security or testing guarantee. A general, adaptable toolchain that handles varying granularities and widths, preserves tags to the binary, and can verify them would remove this recurring tax, which is the gap COGENT targets.
What people did before, and where it broke
- Hand-build a tag-emitting compiler per scheme. Modify a compiler to insert the specific tags a given policy needs, then carry them to the binary. Where it broke. It is tedious and time-consuming, and it is redone from scratch for every new policy, tag format, and architecture.
- Fix a single granularity and width. Design the tooling around, say, per-instruction tags of a fixed width. Where it broke. Exploring a different design — per-function or per-basic-block tags, or wider tags, or control-flow labels — requires more compiler surgery rather than a configuration change.
- Rely on metadata surviving compilation informally. Attach tags and hope they reach the final instructions intact. Where it broke. Optimization and lowering can drop, reorder, or misplace metadata that is not carefully preserved, silently corrupting the policy the hardware enforces.
- Inspect tagged binaries with ad-hoc tools. Write one-off scripts to check the tags. Where it broke. There is no principled, reusable way to verify that the tags in the final binary are correct, so errors go unnoticed.
- The gap. No flexible, reusable toolchain generated instruction tags of varying granularity and width on a common architecture, preserved them faithfully to the binary, and provided the means to disassemble and verify them.
How COGENT works
From high-level tag intent to a verified tagged RISC-V binary
-
-
new
-
new
-
new
-
new
-
new
The idea
Attach hardware-consumed metadata to code, and make generating it easy. COGENT is built around the tag model: annotate instructions (or functions, or blocks) with metadata the hardware checks at runtime to enforce a policy. Enriching code with metadata that hardware consumes to enforce a safety or security property is the same principle the work on implementing capability memory protection in GPUs covered elsewhere on this site is built on, where pointers carry hardware-checked bounds and permissions. COGENT’s contribution is not a single policy but the flexible compiler machinery to produce such tags for many policies on RISC-V, removing the per-project cost of emitting them.
Assert control-flow integrity by tagging where control may go. A flagship use of COGENT is control-flow integrity: emit control-flow labels so the hardware can verify that a program only transfers control to legitimate targets, catching bugs and exploits that hijack control flow. Enforcing at runtime that a program’s control flow (and data flow) stays within what the compiler intended is the same protection the work on enforcing C and C++ type and scope at runtime for control flow and data flow covered elsewhere on this site provides, turning compiler-known invariants into runtime checks. COGENT supplies the labels and tags that a hardware CFI mechanism needs, generated flexibly rather than hand-coded.
Provide reusable instrumentation infrastructure so ideas are not blocked by plumbing. The deeper value of COGENT is as adaptable, reusable tooling: a general toolchain that many tag-based schemes can configure, instead of each rebuilding the same compiler plumbing. Offering a unified, reusable framework for instrumenting programs, so researchers configure rather than reimplement the low-level machinery, is the same philosophy the work on a unified instrumentation framework for deep neural networks covered elsewhere on this site embodies, turning bespoke instrumentation into shared infrastructure. COGENT brings that philosophy to RISC-V instruction tagging, with a disassembler and integrity checker so the tags can also be trusted.
Following a scheme through COGENT
Adding control-flow integrity tags to a RISC-V program
An illustrative walk-through, based on the paper's abstract, of how COGENT produces a tagged binary. Exact internals should be read from the full paper.
Step 1 of 5
Choose the tagging scheme
A developer wants control-flow integrity: every legitimate target of an indirect jump should be labeled so the hardware can reject jumps to anywhere else. They configure COGENT to emit control-flow labels at the needed granularity and tag width, without writing a new compiler.
The scheme is expressed as configuration, not custom compiler code.
Why it works
- One configurable toolchain replaces many bespoke compilers. Each tag scheme normally needs its own compiler work → but COGENT exposes granularity and width as configuration → so a new scheme is set up rather than rebuilt from scratch.
- Varying granularity and width fit any policy. Different techniques need per-function, per-block, or per-instruction tags of different sizes → but COGENT supports all of these (1 to 32 bits per instruction, arbitrary width per function or block) → so the metadata matches what the policy requires.
- Preserving IR-level tags keeps the policy intact. Optimization and lowering can drop or misplace metadata → but COGENT carries tags from the intermediate representation to the machine code → so the hardware enforces the property the developer actually specified.
- A disassembler and checker make tags trustworthy. Silent tagging bugs would corrupt security or testing guarantees → but a tag-aware disassembler and integrity checker verify the binary → so the tags can be relied upon.
- Low overhead keeps tagging practical. Heavy tagging cost would rule out real use → but tagged code adds only about 0.94 to 5.24 percent cycle-count overhead → so it is usable for debugging, testing, and fuzzing where it must stay enabled.
What it costs (trade-offs and scope)
- This explainer is from the abstract only. The full paper was not available when this page was written, so the descriptions of the LLVM-based compiler, the disassembler, and the integrity checker are at the level the abstract states; their internal designs and the detailed evaluation are not reproduced here and should be read from the paper.
- It is a toolchain, not a hardware policy. COGENT generates and verifies tags; enforcing the policy still requires hardware (or a monitor) that consumes those tags. It lowers the cost of the compiler side, not the cost of designing or building the tag-checking hardware.
- It targets RISC-V. The toolchain is built for the RISC-V architecture; applying its approach to another instruction set would require re-targeting the compiler and encoding work, even though the tagging concepts are general.
- Overhead and preservation depend on the scheme. The reported average cycle-count overhead (about 0.94 to 5.24 percent) and the tag-preservation results are for the configurations and applications evaluated; a scheme with very dense, wide, or fine-grained tags could cost more, and tag preservation must hold for whatever transformations the target code undergoes.
- Correctness rests on the checker and preservation. The value of the tags depends on COGENT correctly preserving them to the binary and on the integrity checker catching mistakes; these are the guarantees the toolchain provides, within the assumptions the paper makes.
The evaluation
Producing tagged RISC-V binaries for hardware-enforced policies
| Property | Bespoke per-scheme compiler | Fixed-format tagging | COGENT |
|---|---|---|---|
| Reusable across schemes | |||
| Tag granularity | |||
| Tag width | |||
| Preserve and verify tags to the binary | |||
| Runtime overhead |
Check yourself
Check your understanding
1. What is the core problem COGENT addresses?
2. What does COGENT provide, and how is it flexible?
3. Why is preserving tags from the intermediate representation down to the machine code important?
4. Predict: a security researcher wants to try three different control-flow-integrity designs that differ in tag granularity and width. How does COGENT change their workflow compared with the traditional approach?
Sources
The paper is “COGENT: Adaptable Compiler Toolchain for Tagging RISC-V Binaries” by David B. Demicco, Matthew Cole, Gokturk Yuksek, Ravi Theja Gollapudi, Aravind Prakash, Kanad Ghose, and Zerksis Umrigar (Binghamton University), published at ASPLOS 2025, DOI 10.1145/3676642.3736125. It is licensed CC BY.
This explainer is written from the paper’s abstract only; the full text was not available at the time of writing, so this page is marked as a draft. According to the abstract, tags (metadata) enrich software with domain-specific information consumed by hardware to enforce security and testing policies during runtime, but developing custom compilers that encode tags for a target architecture is tedious and time-consuming. COGENT is a highly flexible and feature-rich compiler toolchain for instruction tag generation on the RISC-V architecture, centered on an LLVM-based compiler supplemented with a tag-aware disassembler and a tag integrity checker. It can generate tags at one or more of varying granularity (per function, per basic block, or per instruction), associate variable-width tags (1 to 32 bits) with instructions and arbitrary-width tags with each function or basic block, and emit control-flow labels crucial for asserting control-flow integrity (CFI), a runtime property that aids in detecting bugs and exploits that violate control flow. The authors evaluate the correctness of the tags generated by COGENT’s compiler, the associated performance penalties, and how well COGENT preserves intermediate-representation-level tags at the lower level, and provide three exemplar applications — control-flow integrity, adaptive tracing, and hardware-level function tracing — that leverage COGENT. The tagged code incurs an average cycle-count overhead from 5.24 percent to 0.94 percent in the worst and best cases respectively, making it suitable for debugging and testing applications including fuzzing. No numbers or mechanisms beyond the abstract have been introduced here; the internal designs of the compiler, disassembler, and checker, and the full methodology, should be confirmed in the full paper.