# GnuZero: A Compiler-Based Zeroization Static Detection Tool for the Masses

Pierrick Philippe, Mohamed Sabt, Pierre-Alain Fouque

## ► To cite this version:

Pierrick Philippe, Mohamed Sabt, Pierre-Alain Fouque. GnuZero: A Compiler-Based Zeroization Static Detection Tool for the Masses. DSN 2025 - 55th Annual IEEE/IFIP International Conference on Dependable Systems and Networks, IEEE/IFIP, Jun 2025, Naples, Italy. pp.1-14. hal-05065294

HAL Id: hal-05065294

https://inria.hal.science/hal-05065294v1

Submitted on 13 May 2025

HAL is a multi-disciplinary open access archive for the deposit and dissemination of scientific research documents, whether they are published or not. The documents may come from teaching and research institutions in France or abroad, or from public or private research centers.

L'archive ouverte pluridisciplinaire HAL, est destinée au dépôt et à la diffusion de documents scientifiques de niveau recherche, publiés ou non, émanant des établissements d'enseignement et de recherche français ou étrangers, des laboratoires publics ou privés.

Public Domain
# GnuZero: A Compiler-Based Zeroization Static
Detection Tool for the Masses

Pierrick Philippe

Univ Rennes, CNRS, IRISA

pierrick.philippe@irisa.fr

Mohamed Sabt

Univ Rennes, CNRS, IRISA

mohamed.sabt@irisa.fr

Pierre-Alain Fouque

Univ Rennes, CNRS, IRISA

pierre-alain.fouque@irisa.fr

*Abstract*—Coding standards for secure programming recommend “scrubbing” sensitive data once it is no longer needed; otherwise, secrets may be recovered, as illustrated in the Heartbleed attack. Despite being an effective software-based countermeasure, zeroization, i.e., overwriting with zeroes, turns out to be challenging and error-prone. Current verification approaches suffer from scalability or precision issues when applied to production software in practice. In this paper, we put forward the GCC Static Analyzer (GSA), which is a symbolic execution engine for error finding. Specifically, we extend the GSA to build GnuZero; our automated tool that detects missing zeroization for all stack/heap variables storing sensitive data, either directly or by derivation. Our experiments confirm GnuZero efficiency and effectiveness in verifying real-world benchmarks. In particular, GnuZero passes all the relevant Juliet’s test programs, namely associated to the MITRE’s CWE-244 and CWE-226. In addition, GnuZero succeeds in identifying new vulnerabilities in open-source cryptographic modules.

*Index Terms*—GCC Static Analyzer, Zeroization, Tainting.

## I. INTRODUCTION

**Context.** Zeroization of sensitive data remains a daunting issue. Indeed, secure systems must resist attackers being able to observe or provoke a system into “leaking” or revealing secret data, such as credentials and secret cryptographic keys [1]. Leaving sensitive data in memory increases the damage caused by memory disclosure vulnerabilities [2]. This is particularly important for any cryptosystem developed in memory-unsafe programming language (e.g., C), or aiming to provide forward secrecy [3]. Thus, several coding standards and guidance documents call for sensitive data to be scrubbed when no longer required [4]. However, they offer little advice on how this is to be achieved or verified, especially given the complexity of programming languages and modern compilers. This is unfortunate, especially since the American National Institute of Standards and Technology (NIST) has defined two CWEs, aka Common Weakness Enumerations, that are directly linked to uncleared sensitive data: CWE-226 (stack zeroization) [5] and CWE-244 (heap zeroization) [6]. A third CWE to be considered is CWE-14 (dead store elimination) [7] that might be the cause of the aforementioned CWEs.

At first sight, scrubbing sensitive data might seem simple: just explicitly overwrite the data with zeros and carry on, right?

It turns out that proper zeroization of sensitive data is far from being trivial. Yang et al. in [8] have shown that real-world programs still contain uncleared sensitive data, due to simple programmer errors, as well as poor interactions between compilers and zeroization functions. Indeed, as pointed out by [3], “it is basically impossible to get it right using this coding pattern”, especially that “it would be difficult to find all the places where 'zeroization' should be inserted”.

Moreover, as highlighted in [9], it is very tricky in practice to ensure that a secure zeroization function works well. Indeed, zeroization of sensitive data is a textbook example of Dead Store Elimination (DSE); most compilers recognize that sensitive data will never be read again, and thus silently discard the call to the scrubbing function. This is due to the fact that zeroization is a non-functional property of the executed program. Incorrect, or even missing, implementations of zeroization do not break any functionality, thereby being more difficult to be caught by automatic regression tests. Now that this problem has been identified, already with `memcpy()` in 2002 [10], multiple ad-hoc “hacks” to work around this problem have been developed; however, none of them is entirely straightforward or foolproof. Previous work has empirically studied the most popular “`secure_zeroization()`” implementations in the real-world to assess their effectiveness [8], [11]. They identified some successful implementations, but we argue that the scope of their conclusions is rather limited to a given set of compilers versions. In both [9] and [12], authors agree that it is extremely difficult to guarantee that a call to `secure_zeroization()` will never be optimized out by increasingly smart compilers in a portable and platform-independent way. Compilers are allowed to act as aggressively as they wish during optimization, as long as they conform to the C specifications. In other words, the only reliable way for real-world projects to test whether a zeroization has occurred or not is to manually check the generated binary, as questioned in an issue raised by the Bitcoin Core project [13]. This manual checking is to be repeated whenever the compiler version has changed, which can be time-consuming. This difficulty highlights the need for compiler-based tools to automatically examine code for uncleared sensitive data. Furthermore, the designed tool should be friendly for developers. As a matter of fact, while designers and implementers of cryptographic software are highly aware of the problems caused by missing zeroization, awareness runs low outside this community.

This work is partially funded by Direction Générale de l'Armentour through CREACH LABS under the project CrypRust, with the support of Région Bretagne, and take part in the France 2030 program managed by the French National Research Agency under grant agreement No. ANR-22-PECY-0006.
Thus, leakage can be unexpectedly found even inside security related programs, since real-world projects consist of various components with unverified handling of sensitive data, as was recently illustrated by the vulnerabilities found in the PAM module [14] and the KeePass Password Manager [15] in 2023.

**Contributions.** In this paper, we tackle this problem by designing GnuZero; a static compiler-based tool to identify CWE-226 and CWE-244 in practice. In other words, our tool automatically examines the source code for any missing zeroization of sensitive data.

Our design is based on the GCC Static Analyzer (GSA). At its heart is a full-featured symbolic engine that is built to analyze C programs, including the Linux kernel. The GSA is a static tool directly integrated into the GCC compiler. The implication is that GnuZero runs on the source level, but it operates on GIMPLE, which is the architecture-independent intermediate representation (IR) of GCC. GnuZero, as the GSA, runs quite late in the compilation stage, way after various optimizations including Constant Propagation and DSE. In contrast, as confirmed by the LLVM project in [16], the Clang Static Analyzer (CSA) has no means to act on the Clang IR after optimization. Thus, our GSA-based tool permits a balanced compromise, since developers interact with the source code for annotation and backtrace interpretation, while the actual analysis is done at the GIMPLE level that is quite close to the actual optimized binary. Backed with this approach, GnuZero is both expressive and precise. Indeed, we do not overlook compilers optimizing away zeroization (e.g. `memcpy()`), while we avoid overtainting by stopping the propagation when sensitive operations are optimized to constant functions (e.g., $x - x = 0$).

To build GnuZero, we have augmented the GSA with four capabilities. First, we leverage the GCC system of extending the C language in order to add two new attributes: the `to_scrub` attribute to identify sensitive variables, and the `scrubber` attribute to identify an effective zeroization function. Thus, unlike other static tools such as Binsec/Rel [11], developers can easily designate local variables as sensitive. Second, we provide the ability to track sensitive variables to raise a warning whenever they are not cleared at the end of their lifetime: when their enclosing function ends for stack variables, or when they are deallocated through `free` or `realloc` for heap variables. GnuZero, unlike existing approaches, does not need to wait for the program termination to spot a leaked secret. Third, we define a tainting policy in order to keep tracking sensitive data throughout the abstract interpretation of the code. Our policy models how sensitive data are used in a program by disassociating their declaration, their initialization and their use. Indeed, sensitive variables are declared (and annotated with the `to_scrub` attribute), then initialized from different sources (e.g., files, users' inputs, random number generators), to be finally used inside various functions calls. Here, sensitivity propagates down the called functions, so that no copied secrets, with a byte granularity, are left uncleared. Our propagation also concerns the anonymous

variables created by GCC to temporarily store sensitive data. This approach constitutes a compromise between working on the code source, but still taking into consideration variables that are not visible on the source level, partially addressing the problem raised in Section 2.3 of [1] because GIMPLE does not work on the hardware registers level. Fourth, we define an expressive reporting system that traces an issued warning to an exact program variable alongside all source code and line numbers that are at the origin of the problem. This allows developers to quickly spot missing zeroization without being expert about the analyzed code.

Finally, we put GnuZero to test, and run it on all the Juliet Test Suite concerning CWE-226 and CWE-244, namely 144 tests. The obtained detection rate is 100% true-positive, with no false-positive nor false-negative. Then, we used GnuZero to analyze real-world projects for missing zeroization, including the libsodium, the Linux kernel and the wpa_supplicant WiFi driver. The purpose of our analysis was not to systematically and entirely explore these projects. Instead, we wanted to examine both the limits and the effectiveness of our approach when GnuZero runs on complex projects. The obtained results are promising. Indeed, our automatic investigation reveals that the `wpa_supplicant` project still relies on `memcpy()` to zeroize sensitive data, ironically confirming that dead store elimination is still considered harmful. Therefore, GnuZero raises a warning only when we run the analyzer with at least 01 as optimization level. While reviewing the GnuZero report, we found that the leaked data is related to the pre-shared key in the IBSS (Independent Basic Service Set) mode. As for the Linux kernel, we study several of its cryptographic modules in which zeroization is seriously considered. Nevertheless, we observe that they apply zeroization to sensitive data selectively; typically only to sensitive data in the stack frame of the calling functions. Sensitive data copied or propagated into stack frames down the call stack is not scrubbed, following the pattern that was manually identified in libsodium in [1], and which we automatically find. Thus, we found that unnecessary copies of cryptographic keys are scattered all over the stack of some called functions without being sanitized.

To sum up our design, GnuZero is built to help developers that might: (1) simply forget to scrub sensitive data, (2) incorrectly implement a zeroization function that compilers shall not optimize out, or (3) pass their sensitive data to some untrusted code that may improperly handle secrets.

**Artifact** In a bid to promote open science, we released an archive on Zenodo [17] containing: (1) the modified version of GCC, (2) utilities to build from source a Debian package containing the compiled modified version of GCC (though an already built package is present), (3) the adapted sources of the Juliet test suite, alongside SARIF files from the analysis and a Python script to run the tests, and (4) the source code of GnuZero and the different tests described in Section V.

## II. THE GCC STATIC ANALYZER

GCC [18] is a compilers suite widely known for being the historical compiler for important projects, such as the
GNU/Linux kernel. Alongside GCC-10, the first version of its internal static analyzer (GCC SA, aka GSA) has been introduced [19]. This newcomer in the suite is a read-only pass intervening in the late stages of the middle-end, enhancing the compiler ability to statically detect issues in the code at compile-time. The SA framework, written in C++, heavily abstracts its internal structures and classes in order to be able to virtually perform any static code analysis.

The GSA acts on GIMPLE, which is the GCC intermediate representations (IR) that is target-independent. More specifically, the GSA deals with a variant of GIMPLE under the static single assignment form (GIMPLE-SSA). The main advantage of this position in the pipeline is that the code has already been through many optimization passes, hence performing an optimization-dependent analysis to detect issues introduced or suppressed by the compiler. This is a great advantage of the GCC SA compared to the Clang SA, especially that the Clang SA is unable to detect problems caused by DSE [16].

The GSA is a complex framework that is ill-documented and "is neither sound nor complete" [20]. It has not yet been thoroughly studied, so an empirical and formal analysis of its code coverage would provide a different perspective, making an interesting contribution to the field. Below, we only present the required notions for this paper.

## A. Region-based Memory Model

Similar to LLVM, the GSA relies on the region-based memory model [21]. Mainly designed for the C language, this memory model defines how to represent variables, namely their l-value and r-value. L-values are defined as the memory location of the variables associated memory regions. A region defines a contiguous memory block that is allocated to a memory object of a program. This does not only represent declared named variables, but also memory objects without identifiers (e.g., an element of an array, a pointer-arithmetic expression, or even a stack frame). Regions form a tree-like hierarchy with different memory space regions (stack, heap and global), holding variables' memory. The GSA defines three main types of regions: (1) Primitive for primitive types (e.g., an integer), (2) Pointer for pointers, and (3) Array for both C-arrays and structures. The Array region type is defined as the pair `<Region, x>`, where $x \in \mathbb{N}$ is the offset to the array elements or the structure members. As for r-values, they are defined as the values associated with a given region at a program point. Note that r-values depend on the region type; e.g., the pointer regions have another region as its r-value.

The GSA extends the region-based memory model with symbolic expressions to denote r-values. Symbolic expressions include symbols of symbolic values (*svalue*). Each *svalue* is delimited by an abstract domain that depends on the region type. Indeed, abstract domains are divided into a numerical abstract domain (e.g., integers) and a pointer abstract domain (i.e., region identifiers). A domain can be concrete (e.g., for constants), symbolic (i.e. depending on other svalues), or unknown (e.g., unbounded parameters).

By implementing the abstract region model with symbolic expressions, the GSA can uniformly represent the abstract storage for all kinds of memory objects during its static analysis. In addition, this approach allows the GSA to handle three relations between memory variables: hierarchical, point-to, and valued-logic relations [21]. Therefore, the GSA is able to perform static analyses that are (field, points-to, context)-sensitive. Below, we focus on the GSA path-sensitive analysis.

## B. Path-Sensitive Analysis

Recall that path-sensitivity is about taking conditional branches into account during abstract interpretation. Despite being complex to achieve, this property is desired, because it significantly limits the number of false alarms (i.e. false positives) produced by the analyzer. The GSA implements path-sensitivity through two components: the exploded graph and the reporting system.

1) *Exploded Graph*: During the abstract interpretation process, the analyzer attempts to enumerate all possible execution paths, while noting the associated *path constraints* for each traversed execution path. For this, the analyzer relies on a data structure called the exploded graph [22] to represent the internal state of the analysis. Each node of this graph is a (program point, program state) pair. A *program point* determines the current location in the program, similarly to an instruction pointer. The *program state* holds, among others, mappings between memory regions and symbolic expressions, in addition to path constraints over symbols collected during exploration. The exploded graph allows the analyzer to consider the same basic block with possibly different incoming paths and different output paths, while still being able to distinguish every case. In order to avoid path explosion, the SA implements some cost reduction by pruning infeasible paths, and merging nodes that are considered equal.

2) *Reporting System*: The purpose of this component is to report issues from the line triggering them back to their sources. To this end, the reporting system leverages bug reports (BR), which are a straight line graph representing the paths to the raised issue. Here, three tasks are performed. First, diagnostics are deduplicated, so that different paths that lead to the same bug only generate one diagnostic. Indeed, if an issue is found on multiple paths, only the shortest one is presented to avoid flooding the user with many diagnostics pointing to the same error. Second, it looks for the shortest and feasible path within the exploded graph leading to the issue regarding the associated context. The reporting system proceeds as follows: it walks backwards through all nodes in the BR to combine their constraints in some formula. There are several outcomes when the GSA checks this formula satisfiability using its built-in SMT solver: 1) no feasible path is found, which leads to discard the considered issue, 2) a feasibility limit is hit (to overcome the termination problem), also discarding the issue, 3) a feasible path is found, leading to the diagnostic being emitted. This post-process approach can eliminate a large number of false positive reports, while
performing resource-consuming SMT operations only on a
limited number of identified paths.

Third, the reporting system generates a diagnostic report to
show the path leading to each identified bug for developers.
Because it might be challenging to understand the reasons
behind certain reports, the reporting system highlights the
events within the displayed path. A diagnostic consists of two
types of events, each of which pointing to a given location
in the analyzed code source: 1) state change events (e.g., a
pointer going from allocated to freed), and 2) final state events
(e.g., dereferencing a freed pointer). Thus, the GSA generates
a diagnostic report showing the line triggering the issue as well
as all the involved lines related to the occurred events. This
event-oriented approach is a great help for quickly deciding
whether a warning is false positive or a real error.

C. The GSA Checks

The GSA performs checks that are categorized into “warn-
ings”. The native set of warnings covers a variety of analysis
targeted at finding security and API usage bugs, dead code, and
other logic errors [23]. Importantly, developers can leverage
the GSA symbolic execution engine to run new static analysis
for user-defined warnings. This is possible via two internal
components: the state machine and the state map.

1) *State Machine:* To run their own analysis, developers should implement an abstract class (*state_machine*) and register it to the GSA. This class contains a set of virtual functions called by the GSA engine at different points during the abstract execution of the code. Of particular interest, a virtual function is called on each *GIMPLE* statement with read-only access to the old program state (i.e., before the symbolic execution of the current instruction), and to the new program state. The *state_machine* relies on a state map, mapping svalues to states. In general, any analysis that could be modeled as a state machine just needs its own instantiation of the required virtual functions and the appropriate states for its state map. Note that even native warnings are implemented as state machines. Interestingly, the GSA does not distinguish between its own native state machines and user-defined ones. This allows adding new analyses without recompiling GCC.

2) *State Map:* Mentioned before, the state map associates an svalue to a data structure consisting of two fields: a state (an abstract class) and an svalue being the origin of this entry. This information might come in handy when building the path leading to an issue for some analysis. Unlike our paper, to the best of our knowledge, none of the native state machines in GCC 13.0.1 use the origin information. Reasons are twofold. First, analyses are mostly unrelated to the svalues themselves, but rather on their logical state (e.g., allocated, freed). Secondly, state machines leverage another mechanism being part of the reporting system just described above.

A state machine relies on the abstract notion of state and
defines each state as needed. Essentially, a state machine is fi-
nite and associates its state with variables at a precise program
point provided by the GSA. Each association state/variable is
an element of the state map. The GSA offers state machines

<figure><img src="image_1.png" /><figcaption>Fig. 1. Zeroization example</figcaption></figure>

to reflect all statements (assignments, calls, conditions, etc...)
on the state map, and possibly triggers (1) one or more state
transitions (e.g., a variable will now be associated with the
destination state of the transition), or (2) a warning if a sink is
defined with the pointed state(s) for the concerned variable(s).

III. ZEROIZATION ANALYSIS

Leveraging symbolic execution with a C source code simulator, the GCC Static Analyzer (GSA) can find various problems at compile-time, such as null-pointer dereference and out of bounds, even in complex software. When it is relevant, the GSA maps its compile-time warnings to software weaknesses, as described in the Common Weakness Enumeration (CWE) maintained by the MITRE corporation [24]. Weaknesses are mostly about security; a CWE may manifest itself as an actual software vulnerability. Unfortunately, weaknesses related to clearing sensitive data are still missing. We argue that such weaknesses are notably important for the GSA, since it was developed with the purpose of scanning the Linux kernel. Indeed, due to the kernel nature, uncleared data may last for a long time in the kernel memory, which might increase the impact of a kernel compromise. In this paper, we focus on automatic detection for two zeroization weaknesses:

*   CWE 226 (stack zeroization) [5]: sensitive information in resource not removed before reuse,

*   CWE 244 (heap zeroization) [6]: improper clearing of heap memory before release,

Let us define the scope of our analysis.

A. Problem Statement

**Motivating Example.** As indicated previously, good practice
in secure programming recommends erasing sensitive data as
soon as they are no longer needed. Zeroization enforces such a
principle by overwriting sensitive data with zero after use. Very
often, this is implemented using the code pattern in Figure 1.

The program (1) defines a secret buffer in the stack, for
instance, (2) fills it with some secret data, by user inputs or
a random number generator, (3) performs some computations
using this secret data, and eventually (4) overwrites the content
of the secret buffer with 0 using zeroize_buffer(). What
can go wrong in this simple example? Three things [8]:

1) Developers might forget to call zeroize_buffer().
One might just blame developers for this negligence.
According to [3], the root cause of this omission comes
from the fact that the programming languages do not
have the right semantics to define sensitive variables.
2) We, often wrongly, suppose that, while performing computations, no copy of sensitive data outside its original buffer is done. A copy could be direct by (partial) store on another buffer (heap or stack), or indirect through individual store or taint propagation.

3) The compiler removes calls to `zeroize_buffer()` after optimization.

**Leakage Model.** In this paper, we extend the GSA to detect CWE 226 and CWE 244, namely when data identified as sensitive is not cleared. Our leakage model differs from the one defined by Daniel et al. [11]. Indeed, they consider that a leakage occurs when secrets remain in the memory at the end of a program. This leakage model is quite restrictive, since it assumes that the analyzed program ends, which is not the case for many deployed programs, such as drivers and kernel modules. For instance, according to their model, uncleared sensitive data inside the kernel would leak only after shutdown. In our model, we rely on the concept of memory lifetime. The C standard specifies that every memory object (e.g., variables) exists over a portion of program execution known as lifetime. For stack variables, lifetime expires at the end of their scope (e.g., at the end of the function). As for heap objects, the lifetime ends when `realloc` or `free` is called. Because accessing an object outside its lifetime is undefined behavior, our model requires that no sensitive data remain uncleared at the end of their lifetime. This implies that we do not detect sensitive heap-allocated memories causing memory leakage, i.e., that are never freed. Moreover, in our paper, we do not consider missing zeroization because sensitive data was not identified as so. Instead, we consider zeroization being 'forgotten' or 'omitted' despite being aware of the sensitivity of the data.

In what follows, we describe how we extend the C language to annotate sensitive data using GCC attributes. Then, we introduce how we design our GSA-based tool to detect missing zeroization of sensitive data after their lifetime, whether being on the heap or on the stack respectively. Our design presented in this section solves the issues discussed above, except for the indirect copy problem, which we deal with in Section IV.

## B. The Scrub Attributes

Once the zeroization problem has been identified, the issue of determining which data needs zeroization remains. A quick response would be “only sensitive data are required to”, but the C language has no means to identify variables as sensitive. In addition, sensitivity could be context-dependent. For example, an address might be sensitive for the Linux kernel-space, but not in cryptographic code. This motivates our work to define an annotation C syntax for sensitive data, so that developers can annotate their own code. To this end, we rely on the GNU attribute system that is often used to extend the C semantics.

Similar to LLVM, the compilation process of GCC can be augmented through user-provided components called plugins. A plugin is a shared object file passed through a command-line option to be loaded at runtime. To be recognized and properly loaded, it must implement a specific interface defined by the GCC plugin API. The scope of plugins is quite wide.

For instance, they may implement a compiler pass for any purpose or add a new GCC attribute (e.g., type, function). This diversity is handled by defining different events, in our case `PLUGIN_ATTRIBUTES` for a new attribute. When loaded, plugins register a function callback that is later called by GCC when the associated event occurs. The advantage of this approach is that it does not require recompiling GCC.

Our plugin introduces two attributes: (1) `to_scrub`, a local variable attribute that identifies data as sensitive, and (2) `scrubber`, a function attribute indicating a zeroization function, i.e. it destroys the sensitivity of its input parameter.

Regarding the source of sensitive data, we only consider local variables with no linkage, since it is not recommended to use variables whose lifetime lasts the whole program execution, which concerns global variables as well as local variables with the keyword `static`. Moreover, we only consider concrete variables, excluding parameters in functions declaration. Indeed, the semantic of sensitive parameter is quite different; a function should not be expected to scrub its pointers-type parameter(s) unless it is a scrubbing function, namely all functions decorated with the `scrubber` attribute. We consider such functions as sinks, and therefore our tool does not step down to analyze further.

The internals of our system of annotation depends on the region-based model leveraged by the GSA. Indeed, at the source level, we only decorate the sensitive variable with the `to_scrub` attribute. However, within GnuZero, the annotated element in the state map depends on the region type associated to the decorated variable:

1) the memory region for Primitive regions,

2) the `svalue` for Pointer regions,

3) the super region for Array regions.

Note that we define the same attribute for both stack and heap-allocated variables, even though we analyze them differently. Below, we present the core of our analysis.

## C. Heap Memory Zeroization

Since our design leverages the symbolic execution engine of the GCC Static Analyzer (GSA), our tool is implemented as a state machine registered to the GSA (refer to Section II-C).

A state machine designed to detect the non-zeroization of sensitive data would consist of a finite-state machine with two states: (1) *unscrubbed* and (2) *scrubbed*. Regarding sensitive data allocated on the heap, which implies using the libc's `malloc/free` API in C, the state machine could use calls to this API as an initial transition for a variable (e.g., `malloc`) or as a sink (e.g., `free`). However, treating all memory objects as if they were sensitive would add unnecessary overhead through zeroing [1]; similarly, tracking all allocated data as sensitive would generate many false positives. Hence, on the one hand, the initial transition to the *unscrubbed* state only occurs for variables decorated with the `to_scrub` attribute or initialized from a variable already tracked as such. On the other hand, the only way for a variable in the *unscrubbed* state to move into the *scrubbed* state is to be passed as an argument to a scrubbing function. Warning should be emitted by the state
machine if, and only if, a variable in the *unscrubbed* state is freed (i.e., at the end of its lifetime).

**The Special Case of Realloc.** During the implementation of our state machine, the particular case of realloc arose. Five different outcomes can result from a call to realloc (without considering undefined calls); we detail them here by looking at the different C standards (from C89 to C23), alongside details of glibc 2.40 implementation and how a zeroization analysis would be impacted.

1) *Failure*: since C89, realloc should leave its input memory object unchanged upon failure. Glibc's implementation is compliant: it returns a null pointer and sets `errno` accordingly. A warning should be emitted regarding zeroization analysis if the input memory object is not *scrubbed* during error handling.

2) *Equivalent to free*: in C89, if its input memory object is not null and the requested size is zero, realloc is equivalent to a call to free. There is no precision regarding the expected behavior for C99 and C11, and since C23, such a call is considered an undefined behavior. Nonetheless, the glibc's implementation kept the behavior of the C89. Since such a call is equivalent to free, a warning should be emitted by a zeroization analysis if its input memory object is not *scrubbed*.

3) *Equivalent to malloc*: since C89, if its input memory object is null, realloc is equivalent to a call to malloc with the requested size. The Glibc implementation complies with the standard, and a zeroization analysis may want to consider such a call equivalent to malloc to catch all possible outcomes of realloc. However, our implementation does not take this case into account, because it seems unlikely to rely on this specific behavior instead of directly using malloc.

4) *Success*: realloc might (1) resize its input memory object in place (i.e., its address does not change, and no new object is allocated) or (2) a new memory object is allocated, and the content of the input memory object is copied into the new object up to the size of the input object. Although the C standards do not mention the moving behavior, the terms “old” and “new” are used. The Glibc documentation specifies this moving behavior, and those two cases have different outcomes for the zeroization analysis.

a) *Success with no move*: in this case, the call to realloc has no side effect on the analysis. Undeniably, the zeroization analysis does not need any transition as the address returned by realloc is the one of its input memory object.

b) *Success with move*: since C99, the old memory object has to be freed, and the address of the new memory object is returned. The Glibc implementation is compliant, and if the resizing cannot be done in place, internal documentation states a “malloc-copy-free sequence” is executed [25]. This specific behavior is a real issue when considering sensitive data, as there is no safe way, namely free of undefined behaviors, to scrub the sensitive data inside the old memory object upon realloc execution. The zeroization analysis should emit a warning specifying that this call is a problem only if the data are copied into a new memory object.

Close to our findings, we highlight different programming guidelines: recommendations regarding sensitive data usage

with realloc in the CERT C coding standard [26], the description of CWE-244 [6] for the “malloc-copy-free” mechanism, and in the ANSSI (the French authority for information systems' security) C guidelines [27] for the behavior equivalent to free. To resume, reallocating memory objects known to contain sensitive data should only be done if needed and with caution. As realloc might or might not leave unscrubbed sensitive data on the heap, a custom implementation of realloc should clean the old memory object, especially if it is sensitive.

## D. Stack Memory Zeroization

When considering the nature of sensitive data in stack-based variables, the scope of our analysis focuses mainly on structures and buffers (i.e., arrays and pointers allocated through a call to alloca). The `to_scrub` attribute has for granularity all the memory object of the decorated composite type being an array or a structure. Even though such a behavior does make sense for sensitive arrays, structures might be composed of both sensitive and non-sensitive elements. In the worst case, this might trigger false positives if not the whole structure was scrubbed. More granularity is added for tainting variables, which is explained in Section IV. For our tool, there are two main differences compared to heap variables: sink and tracking. Regarding the scrubbing mechanism, we apply the same method as for heap-allocated variables, i.e., only a call to a function decorated with the *scrubber* attribute will initiate a state transition for its argument to the *scrubbed* state.

Related to the sink, recall that our leakage model expects our tool to issue a warning if there exist some variables at the *unscrubbed* state at the end of their lifetime. For stack-allocated variables, this happens at the end of their function scope. More precisely, the lifetime of variables with automatic storage duration is equal to their scope. Since C99, we are allowed to define variables at any point of the function with their own block scope (i.e., inside `{...}`). In GIMPLE, all such variables are moved to the beginning of their functions, and therefore their lifetime always expires at the end of the function. Here, we extend our state machine to examine the state of local variables whenever the GSA pops the frame of the current function during the abstract interpretation.

As for tracking, unlike heap-based variables, stack variables are not always allocated by the returned value of a function call (e.g., malloc). This raises the following problem: the GSA does not call our state machine on statements consisting of variable declaration, since they are not considered as actual GIMPLE statements. Thus, the previously described state machine must be modified in order to track such stack-based variables correctly. Our design considers the two possible initialization methods for stack variables: (1) by reference (e.g., its address might be passed to an initialization function), and (2) through direct assignment (potentially from the returned value of a function call). Let us discuss both.

1) *By reference*: such initialization mechanisms make sense only when considering structures or stack arrays. For this case, the state machine needs a new initial transition that runs on function calls, except for some specific functions (e.g., malloc
or `memset`). For each function call, we look into the passed arguments; if they are decorated with `to_scrub`, we trigger their initial transition to the *unscrubbed* state.

2) *By direct assignment:* in this case, the state machine also needs a new initial transition, despite being very similar to the heap-allocated variables. The analysis does have to look at the left-hand side (LHS) of every single GIMPLE assignment and track its memory object as *unscrubbed* if it is decorated.

## IV. ZEROIZATION WITH TAINTING ANALYSIS

In his thorough analysis "zeroing buffers is insufficient" [3], Percival points out that zeroization of code-source variables does not necessarily result in scrubbing every location where sensitive data might be stored. In other words, sensitive data might propagate outside their original memory space, including into memory objects that do not map to any source-level variables. The copy may either come from developers or from the compiler. Indeed, developers may initialize some explicitly-declared local variables with sensitive data. This applies if two or more sensitive variables are combined in some way to get some derived variable. Moreover, compilers may make copies of data into other places that we see in GIMPLE as implicitly-declared and initialized local variables. These variables are anonymous, since they do not appear in the source code, but in the GIMPLE representation. In the best case, these anonymous variables are optimized out while compiling GIMPLE to RTL, which is the low-level GCC intermediate representation. However, in the worst case, the final RTL code, and so is the binary, might keep the value of a sensitive variable into an implicitly allocated temporary variable on the stack, or even on a CPU register.

One might expect that the situation with sensitive data left behind in temporary variables is less problematic, since such resources are liable to be reused more quickly; but in fact this can be even worse. For instance, the CVE-2023-32784 [15] affecting KeePass was caused by some leftover strings that, once dumped, offer likely password characters for each position in the password. In [1], authors find that data derived from sensitive data and contained in stack frames further down the call stack is not scrubbed. They give the libsodium's ChaCha20 API function as example, where some secret key is explicitly copied by developers on the stack of some called functions processing that secret key. Then, they show that such a residual sensitive data presents an exploitable behavior that may leak the secret key.

The many ways in which propagate sensitive data make zeroization a daunting task. Indeed, real-world projects are made of many components that programs designers did not develop. Unfortunately, it is not uncommon that such components handle sensitive data poorly, which results in leakage even in specialized security applications and libraries. This limits the interest of traditional static and dynamic analysis techniques, since they are typically limited in scope to a single variable, and cannot deal with more than one function call. This difficulty underscores the need for tools to aid examining

programs for missing zeroization automatically by stepping down called functions and tracking copied data.

In this section, we address data leak through explicit and implicit propagation, which requires more extensive modification to the GSA. Our design involves two central elements: first, introducing the tainted state, and, second, deciding how to propagate that state over the code abstract interpretation. We examine each of these topics below.

### A. The Tainted State

The term "tainting" in GCC is traditionally referred to denote data coming from an untrusted source. When the GCC function attribute `tainted_args` is used, potential vulnerabilities are then discovered by determining whether tainted data reaches a sensitive sink [28]. This of course differs from our use of tainted sensitive data, even though the underlying mechanisms are the same.

In our model, variables are identified as sensitive either because they were defined as so, or they were derived from other sensitive variables. Thus, in addition to the *untracked* variables, we take the *unscrubbed* state of tracked variables and split it into two other states: source and tainted. Both states indicate sensitive data that must be moved to *scrubbed* if no leakage shall be found. The "source" is distinguished by being decorated with the `to_scrub` attribute and only destroyed through a call to a scrubbing function. Tainted variables follow the same rules as source decorated ones to determine which element (i.e., rvalue or lvalue) will be associated with a state, which is type-dependent: svalue for pointers, and lvalue (or regions) for all other region types.

We now need to define sensitivity inheritance, propagation and destruction semantics for both types of tracked variables.

### B. The Taint Propagation

Tainting occurs through code interpretation. Loosely defined, the GIMPLE language includes numerous operators that apply on sensitive data. In this paper, we only consider a subset of all possible GIMPLE statements causing state propagation:

1) We limit our analysis to the C language, since it is the only language that the GSA can handle. This eliminates any GIMPLE statement linked to other programming languages (e.g., C++ constructors).

2) Except for functions calls, control-flow statements do not have any effect on state propagation.

Thus, the operations that we consider are identified as either functions calls or GIMPLE assignments, including pointer arithmetic as well as array and pointer access.

Now, let us define how variables get tainted. Below, we will first consider the intraprocedural case (i.e., from local to local), and then the interprocedural case (i.e., from caller's local to callee's parameter). Related to GIMPLE statements, functions calls are part of the interprocedural analysis, whereas assignments are part of the intraprocedural analysis.

**Intraprocedural** Given the following statement $A \leftarrow B$, we answer this simple question: what is the state of A provided B's? We say that A is the left-hand side (LHS) variable,
while *B* is the right-hand side (RHS) variable. Regarding LHS, we distinguish two cases. First, if the LHS is in the *source* state, then there is no modification in its state. In other words, it does not lose its taint or move to the *scrubbed* state regardless of the RHS's state or value. Recall that only a *scrubber* function can impact a *source* variable. Second, the LHS is not *source*: it can be potentially an anonymous variable created by the compiler. Here, the LHS takes the taint of the RHS. Namely, the RHS might taint, untaint, or leave the LHS state unchanged. Unlike the *source* variables, tainted variables are (field/element)-sensitive, allowing our analysis to report a subset of a non-decorated array as being sensitive instead of reporting the whole array. Table I resumes all the different outcomes for an assignment.

The propagation policy becomes less straightforward when we consider SSA variables. Indeed, we would always have had leak if zeroization of tainted variables be performed through a simple assignment. By definition, SSA variables are assigned in exactly one location in the program, and naturally multiple assignments to the same variable create new versions of that variable. The policy of tainting all variables, including SSA ones, is clearly too conservative and errs on the side of tainting too much. Our design choice was to balance the desire to preserve any possibly interesting taints against the need to minimize false positives. When considering an assignment for state propagation (i.e., RHS is tainting LHS), LHS is either: (1) an anonymous SSA, then we track the state for the SSA variable itself, or (2) an SSA instance of a source-code declared variable. In the latter, we track the actual variable declaration to allow detection of any future assignment considered for scrubbing. At the same time, if the RHS has a single operand and is an anonymous SSA variable, we chose to clear its state in order to avoid generating many false-positive cases.

**Interprocedural** There are three cases to take into account regarding a GIMPLE call statement: (1) the body of the function is within the compilation unit, (2) the function has external linkage, or (3) the actual function is behind a function pointer. For the last case, the GSA, through different heuristics, attempts to select the best candidates for a call through a function pointer. Hence, if candidates are found, the exploded control-flow graph will make a direct call for each candidate, which will then fall under the other two cases, i.e., whether the body is within the compilation unit or not. Regarding the second case, i.e., if the function body is not accessible, then our analysis will either (1) start tracking the decorated variable passed as an argument if it is seen for the first time, or (2) stop tracking any tracked argument if the function identifier matches a function decorated with the *scrubber* attribute. The last remaining case is quite transparent regarding tracked pointers, since the GSA will propagate their state. However, the analysis has to detect pointers taking the address of the caller's decorated stack variables, as this could be the first time this address is taken. The statements of the callee are not different from the caller's from the state machine point of view

<table><caption>TABLE I
STATE PROPAGATION THROUGH ASSIGNMENT.</caption><thead><tr><th colspan="2">Current state</th><th>New state</th></tr><tr><th>LHS</th><th>RHS</th><th>LHS</th></tr></thead><tbody><tr><td>source</td><td>any</td><td>source</td></tr><tr><td rowspan="3">untracked</td><td>untracked</td><td>untracked</td></tr><tr><td>source</td><td rowspan="2">tainted</td></tr><tr><td>tainted</td></tr><tr><td rowspan="3">tainted</td><td>untracked</td><td>untracked</td></tr><tr><td>source</td><td rowspan="2">tainted</td></tr><tr><td>tainted</td></tr></tbody></table>

due to the exploded control-flow graph. Thus, once inside the called function, we apply the intraprocedural approach, with the information relative to the call pushed to the program state.

## V. EVALUATION

### A. Research Questions

The purpose of GnuZero is to detect the non-zeroization of sensitive data. In order to assess its effectiveness, we investigate three main research questions:

* **RQ1. Usability and Optimization Sensitivity.** Is GnuZero easy-to-use and able to adapt its analysis according to the optimization level of the compiler?

* **RQ2. Finding Vulnerabilities.** How effective GnuZero is in finding vulnerabilities in programs?

* **RQ3. Analyzing Complex Projects.** Is GnuZero able to scale to analyze complex real-world projects?

* **RQ4. Complexity and Overhead.** How efficient is GnuZero when analyzing real-world code?

To answer our research questions, we conduct various experiments that we detail below.

### B. Experiment Setup

Experiments were performed on a laptop with an 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz processor and 16GB of RAM. Unlike related work (e.g., secretgrind [29] and Binsec/Rel [30]), there is no need to start the analysis from the beginning of the main function, since we abstractly interpret the target code per compilation unit. For complex projects where the target code requires multiple dependencies from other translation units, we produce our evaluation artifact in a way to simplify reproduction of our experiments. Thus, the different source code files have been pre-processed manually, so that GnuZero runs on one standalone translation unit with no further compilation dependency. For more readability, irrelevant code has been commented out from the original file.

**Implementation.** We implemented GnuZero on top of the GCC Static Analyzer (GSA) version 13.0.1¹. Hence, all analyzed programs were compiled with this version of GCC independently of the target architecture (i.e., with the flag -c). We run the tool using the following command line `gcc -fanalyzer -fplugin=/path/to/plugin.so -c`.

<footnote n="1">¹Commit: 0963cb5fde158cce986523a90fa9edc51c881f31</footnote>
```c
1 void heap issue(void) {
2     unsigned size = 64;
3     char *TOend buf
4     = (int *)malloc (size);
5     if (buf)
6     {
7         char *_guard = buf;
8         buf = realloc(buf, 256);
9         // in case realloc fails
10        if (!buf)
11            buf = _guard;
12     else
13         size = 256;
14     memcpy(buf, 0, size);
15 }
16 free(buf);
17 }
```

<caption>Fig. 2. Heap-related sensitive data usage - Realloc issue</caption>

```c
1 extern SCRUBBER
2 void scrubber_fn(void *);
3 extern void init(char *);
4 extern char some_cond;
5 void foo(char *ptr) {
6     char x = *ptr;
7     char y = x;
8     y += 1;
9     y -= x;
10    use (&y);
11    x = 42;
12 }
13 void stack issue(void) {
14     char TOend buf[256];
15     init(buf);
16     if (some_cond)
17         foo(buf);
18     scrubber_fn(buf);
19 }
```

<caption>Fig. 3. Stack-related sensitive data usage</caption>

## C. RQ1: Usability and Optimization Sensitivity

► Methodology. We apply GnuZero on short programs that we craftily write to evaluate our leakage model as well as our tainting strategy. We vary the nature of the variable to scrub, heap or stack, insert different branches and functions calls in order to trigger different zeroization problems leaking the same original secret through different tainted variables. In some cases, we introduce code that can be simplified or removed by the compiler optimization. For the sake of brevity, we include two representative programs of our tests (Figures 2 and 3 for CWE-244 and CWE-226 respectively), and discuss the GnuZero's outputs.

► Findings.

1) Heap Example (Figure 2): Illustrating CWE-244, this example has a weakness due to the “malloc-copy-free” behavior of realloc. We notice that the code attempts to carefully handle realloc by still scrubbing the data in case of a failure (through the use of _guard lines 7 and 11). However, the case in which the data are moved someplace else on the heap might still happen. In addition to this realloc-based leak, a dead-store elimination-based leak is detected because of the compiler optimization discarding the call to set (line 14), since the compiler identifies the source pointer as not being read until the end of its lifetime (call to free line 16). Such a leak is only detected when the program is compiled with optimization. Despite working on the source level, the GSA, and therefore GnuZero by design, runs after all the GIMPLE SSA-based optimization (i.e., machine-independent ones), such as DSE. Surprisingly, this simple code cannot be detected by Binsec/Rel, since it does not support dynamic memory allocation

2) Stack Example (Figure 3): Illustrating CWE-226, this program does contain a weakness because sensitive data within buf might leak through the local variable y of the function foo. This data leak happens if some_cond is true, so that foo gets called. Sensitive data are correctly scrubbed by assignment for local x (line 8), and by call to a scrubbing function for the source buf (line 2). The particularity of this program is that it does leak only when it is compiled without optimization. Indeed, the compiler detects that the final value of the local y (line 9) must be 1, and therefore is independent of the tainted variable x. This optimization, known as constant function, automatically removes taint when the concerned variables become constant. This illustrates the importance of systematically checking for missing zeroization each time the program is compiled.

3) Ease-of-use: From our experiments, we notice three main features that highlight the usability of our tool. First, running GnuZero on a given program, or more precisely a translation unit, is as simple as compiling it using GCC. Second, identifying sensitive data is as simple as adding GCC attributes to the concerned variables only once during their declaration. Third, the generated report is expressive, allowing developers to quickly understand the issue to patch it. Every report constitutes a unique different problem. In addition, reports can be viewed directly on VS Code, since they support SARIF (Static Analysis Results Interchange Format).

## D. RQ2: Finding Vulnerabilities

► Methodology. Now, we would like to empirically assess how effective GnuZero can detect missing zeroization. Here, we proceed as follows. First, we apply GnuZero on a standardized testing set of vulnerable programs. The test programs are rather short, but contain uncleared sensitive data in various ways. We compute the detection rate of GnuZero over this set, since the ground truth of these tests is known. Second, we consider GnuZero on libsodium, which is a real-world cryptographic library. In particular, we run our tool on the ChaCha20 implementation in which the authors of [1] have
manually detected uncleared sensitive data down on the stack of some called function. We would like to examine if GnuZero can automatically detect such a leakage.

▷ Findings.

1) **Juliet Test Suite:** The NIST Software Quality Group maintains the Software Assurance Reference Dataset (SARD) [31], which is a vast collection of test programs in different programming languages with documented weaknesses. The Juliet test suite, part of this dataset, consists of standalone C programs illustrating numerous CWEs. Among them, 144 tests target CWE-226 and CWE-244¹, split into 72 tests per CWE. These tests are the results of 18 control-flow variants of 4 different programs with weaknesses and mitigations. Given the 144 tests, we semi-automatically annotate the sensitive variables in the different source files. Here, we went through the code and noticed that the sensitive variables were always named 'password'. We used 'sed' to annotate all the code, and quickly verified that the annotation had been correctly placed. Then, we run GnuZero that generates all the reports in around 40 seconds. Finally, we manually review all the obtained reports in order to compare their findings with the ground truth of these tests. Our results are promising: 100% true-positive detection rate and 0% false-positive or negative. This makes GnuZero, to the best of our knowledge, the first static tool that passes all the Juliet test suite related to both CWE-226 and CWE-244.

2) **ChaCha20's API:** In [1], authors point out that the libsodium's ChaCha20 API implementation leaks some secret cryptographic key. The API is implemented in "crypto_stream/chacha20/ref/chacha20_ref.c", and follows a particular coding pattern. Indeed, it declares as global an instance of a structure exposing static function definitions. Four different ones are exposed in this compilation unit; they differ in their IV setup, but lead to the same leaking function.

The code is as follows: (1) the secret key is passed as an argument and copied in a local context structure (only holding a sixteen `uint32_t` wide array) through an initialization function `chacha_keysetup`, copying the key into parts of the context's inner array (indexes 4 to 11), (2) this context becomes then the argument in a call to `chacha20_encrypt_bytes`, and finally (3) all the elements of the context's inner array are used to initialize sixteen local variables that are never zeroized from the function stack.

## E. RQ3. Analyzing Complex Projects

▷ Methodology. This research question involves analyzing real-world projects for missing zeroization. The scope of this investigation is too large to be contained in a single paper. For the purpose of this work, we limited the scope to non-trivial projects that are both complex and hard to examine dynamically or in the binary level. Here, we chose two projects handling secrets in the kernel, namely the Linux cryptographic modules and the wpa_supplicant WiFi driver.

Note that state-of-the-art tools, such as Binsec/Rel, cannot analyze these two projects, because they associate leakage and program termination. For each project, we do not intend to conduct a comprehensive exploration. Rather, the purpose of our experiments is to find new missing zeroization issues within these projects. Success or failure assess either the limitations or the effectiveness of our approach when GnuZero is applied to complex real-world projects.

▷ Findings.

1) **Linux Crypto Modules:** We run GnuZero on two cryptographic modules from the Linux kernel<fnref n="2" /> [32]: Tiny Encryption Algorithm (TEA) and Data Encryption Standard (DES). Regarding **TEA** [33], and its extended version (xTEA) [34], we analyze their API functions defined in the file "crypto/tea.c". GnuZero detects leaks in local variables of the encryption and decryption functions, with and without optimization activated (up to -O3). Similarly, as for DES and 3DES, we analyze their API functions within the Linux kernel (file "lib/crypto/des.c"). We focused on the functions expanding cryptographic keys for DES and 3DES (respectively, `des_key` and `dkey`), which are similar except for the return value of the DES key expansion that may be used to detect weak key usage. Our analysis successfully detects uncleared sensitive data in these two functions. An insight of our analysis is that zeroization is often forgotten when sensitive data propagates into stack frames further down the call stack, even in projects that consider zeroization seriously.

2) **WPA Supplicant - Kernel Driver:** From its manual, it is "an implementation of the WPA Supplicant component, i.e., the part that runs in the client stations", and "acts as the backend component controlling the wireless connection". The analysis was focused on the `wpa_supplicant_set_wpa_none_key<fnref n="3" />` [35], a function used for ad-hoc network. The function relies on a local buffer to copy a pre-shared key from its parameters, and a call to `memcpy` for zeroization is present at the end of the function. The leakage is detected when the call to `memcpy` is suppressed at the first optimization level (-O1).

## F. RQ4. Complexity and Overhead

▷ Methodology. We now want to assess how GnuZero impacts compilation time empirically. We used the hyperfine [36] benchmarking tool to execute GnuZero, the GSA and GCC with the same parameters for each real-world code.

▷ Findings.

Table II depicts the results from hyperfine. We can see that overhead is not much, except for DES and libsodium without optimization. We suspect the reporting system to be the main cause of this overhead. Indeed, in Section II-B2, we explained how the reporting system execution time is correlated to the number of paths and their complexity, leading to warning(s) reported by state machines: more complex paths induce longer execution time. The results illustrate

<footnote n="1">¹https://samate.nist.gov/SARD/test-cases/search?flaw[]]=CWE-226&flaw[]]=CWE-244&language[]]=c&limit=25&status[]]=accepted</footnote>

<footnote n="2">²Commit tagged "v6.12"</footnote>

<footnote n="3">³Commit: f91680c15f80f0b617a0d2c369c8c1bb3dcf078b</footnote>
this behavior. The best case to illustrate this is the case of libsodium: GnuZero execution time is 99% faster when optimization turned on. The analyzed function has several local variables ($j \times x$) aliasing the sensitive input, which is never erased or overwritten in the source code. However, no issue is raised with the optimization version. This is due to the fact that GCC optimizes the use of local variables through reuse, thus overwriting their memory space with other temporary variables. No issue implies no computation on path feasibility, which decreases execution time.

## VI. RELATED WORK

Already in 2005, Chow *et al.* [2], identified the problem of sensitive data unnecessarily remaining in memory: the longer sensitive data resides in memory the greater its chances of being leaked by various means. Several similar discussions are found in the literature. In particular, Percival [3] provides some insights of how hard to force a (smart) compiler to keep zeroization of sensitive data. Chapman [4] goes through a set of recognized guidance and coding rules for secure programming that call for sensitive data zeroization. He discussed both their scope and their limitations. In what follows, we split existing research work into two categories: enforcing zeroization, and detecting missing zeroization, which our paper furthers.

**Enforcing Zeroization.** The core idea is to build programs that scrub their sensitive data without manually calling the appropriate zeroization function. Researchers have developed many solutions to address this on various levels. Chow *et al.* handle this on a system level by applying OS-based secure deallocation [2], [37], while Chapman [4] defines a language-specific solution mostly in the context of the Ada and SPARK programming languages. Different research work focused on the Java programming language. Indeed, Hansen *et al.* [38] propose the first definition of secret erasure in the Java language in the context of smart cards to guarantee that secrets are unavailable after Java Card program termination in the card. Pridgen *et al.* [39] aim at reducing encryption keys in Java programs that run on both Oracle and OpenJDK Java Virtual Machines, Lee *et al.* [40] suggest changes to the Android codebase to mitigate the problem of having uncleared secret keys in Android's TLS, and in [41] they focus on solving the problem of password retention in the Android platform. Given the versatility of the Java language, these papers show that the problem of uncleared sensitive data concerns all computing devices: smart cards, smartphones, personal computers and servers, but they all context-dependent solutions, instead of having a universal approach. Therefore, there has been significant interest in solutions based on compilers, since they can either formally prove the flow-preservation program transformation for volatile objects as in CompCert [42], or to insert stack (not heap) zeroization during program compilation (e.g., Clang [43], Jasmin [1]). Another approach, described in [44], is to protect any code related to sensitive data from being discarded by aggressive optimization, including dead store elimination. In practice, GCC is the only compiler that officially ensures stack zeroization; it

defines the *strub* attribute, so that the annotated functions can have their stack frames zeroed-out [45]. Finally, some works suggest storing sensitive data in secure locations, instead of removing data from memory. Secure locations arguably include cloud storage [46], CPU registers [47], and specialized hardware extensions like ARM TrustZone [48] as well as Intel MPK [49]. Unfortunately, such solutions rely on features that are not universally available.

**Detecting Missing Zeroization.** Instead of enforcing zeroization, another line of research considers, like ours, protecting sensitive data by detecting vulnerable applications overlooking data zeroization. Here, we do not include related work, such as [50], that only study detecting dead store elimination independently of the data sensitivity, even though this might accidentally lead to detecting missed zeroization. Dynamic analysis techniques, such as data-flow analysis [51], and password tracking [52], have been introduced to detect data leakage. In addition, Secretgrind [29], a tool built upon Taintgrind [53] that is based on Valgrind's MemCheck, is a runtime analysis tool that uses taint to track sensitive data in memory. Secretgrind propagates taint in two ways: direct assignment and arithmetic operations. Following the same direction, namely binary analysis, Binsec/Rel [11], built upon the symbolic execution engine Binsec/SE [30] of the binary-analysis platform Binsec [54], can be used to analyze binaries for zeroization sensitive data. The closest work to ours is Binsec/Rel, since we both rely on static analysis and symbolic execution to detect missing zeroization. However, GnuZero differs from Binsec/Rel in three main aspects. First, GnuZero works on the (C) source level, while Binsec/Rel works on the binary level. While binary tools are more precise, they cannot take programming abstractions into consideration. For instance, Binsec/Rel can hardly track local variables for leakage. In addition, GnuZero does detect vulnerabilities related to optimizations, because the GCC Static Analyzer interprets GIMPLE after DSE. Second, both tools diverge in their leakage model. Indeed, Binsec/Rel checks for leakage only at the program termination, thereby excluding kernels and drivers modules, while GnuZero identifies leakage more locally by looking at variables lifetime. Thus, Binsec/Rel was mainly used to automatically check CWE-14, namely the preservation of zeroization functions by compilers, to further the work of [8] in which verification was done manually. As for GnuZero, it can be used to analyze complex programs for missing zeroization caused either by compilers or developers, i.e. CWE-226 and CWE-244. Third, GnuZero reports all changes to state, thereby allowing deeper inspection of the error, for example the source line numbers alongside the code which caused tainting. Binesec/Rel suffers from reports expressiveness, since it works on the binary level.

## VII. FUTURE WORK

### A. Internals

GnuZero would benefit from both new usability features and expanded capabilities to analyze more complex real-world code. We identify taint propagation and automatic annotation.
<table><caption>TABLE II
EXECUTION TIME MEASUREMENTS GET THROUGH THE USAGE OF HYPERFINE [36], A BENCHMARKING TOOL. WE RAN IT ON EACH REAL-WORLD CODE TO COMPARE THE COMPILATION TIME OF USING GSA AND GNUZERO OVER GCC FOR OPTIMIZATION LEVELS -O0 TO -O2. THE ROW 'RELATIVE' CORRESPONDS TO AN EXECUTION TIME RATIO TO THE QUICKEST COMMAND (VALUE '1.0').</caption><thead><tr><th colspan="10">Case</th></tr><tr><th colspan="10">Libsodium</th></tr><tr><th colspan="10">DES</th></tr><tr><th colspan="2">Optimization</th><th colspan="3">No (-O0)</th><th colspan="3">Yes (-O2)</th><th colspan="3">No (-O0)</th><th colspan="3">Yes (-O2)</th></tr><tr><th>Command</th><th>GCC</th><th>GSA</th><th>GnuZero</th><th>GCC</th><th>GSA</th><th>GnuZero</th><th>GCC</th><th>GSA</th><th>GnuZero</th><th>GCC</th><th>GSA</th><th>GnuZero</th></tr></thead><tbody><tr><td>Mean [ms]</td><td>57.4 ± 5.6</td><td>2932.2 ± 241.8</td><td>3592.7 ± 147.3</td><td>28.8 ± 3.6</td><td>28.3 ± 2.6</td><td>29.7 ± 2.7</td><td>120.8 ± 16.2</td><td>7021.6 ± 54.9</td><td>51385.5 ± 2701.0</td><td>192.3 ± 9.1</td><td>1082.0 ± 95.0</td><td>52273.2 ± 5300.2</td></tr><tr><td>Min [ms]</td><td>49.1</td><td>2513.7</td><td>3456.3</td><td>21.8</td><td>21.3</td><td>22.5</td><td>97.0</td><td>6952.1</td><td>48652.6</td><td>181.5</td><td>999.6</td><td>45709.8</td></tr><tr><td>Max [ms]</td><td>70.2</td><td>3319.8</td><td>3865.4</td><td>41.0</td><td>32.9</td><td>37.3</td><td>182.5</td><td>7099.9</td><td>56223.7</td><td>212.3</td><td>1329.6</td><td>59964.2</td></tr><tr><td>Relative</td><td>1.00</td><td>51.10 ± 6.53</td><td>62.62 ± 6.63</td><td>1.02 ± 0.16</td><td>1.00</td><td>1.05 ± 0.14</td><td>1.00</td><td>58.14 ± 7.80</td><td>425.50 ± 61.19</td><td>1.00</td><td>5.63 ± 0.56</td><td>271.81 ± 30.40</td></tr></tbody><thead><tr><th colspan="10">Case</th></tr><tr><th colspan="10">TEA</th></tr><tr><th colspan="2">Optimization</th><th colspan="3">No (-O0)</th><th colspan="3">Yes (-O2)</th><th colspan="3">WPA Supplicant</th><th colspan="3">Yes (-O2)</th></tr><tr><th>Command</th><th>GCC</th><th>GSA</th><th>GnuZero</th><th>GCC</th><th>GSA</th><th>GnuZero</th><th>GCC</th><th>GSA</th><th>GnuZero</th><th>GCC</th><th>GSA</th><th>GnuZero</th></tr></thead><tbody><tr><td>Mean [ms]</td><td>44.9 ± 3.5</td><td>73.3 ± 4.5</td><td>170.6 ± 6.0</td><td>83.0 ± 8.9</td><td>89.0 ± 7.1</td><td>153.2 ± 9.6</td><td>36.3 ± 4.0</td><td>39.9 ± 4.1</td><td>41.9 ± 3.0</td><td>30.7 ± 1.7</td><td>32.8 ± 2.8</td><td>35.2 ± 3.5</td></tr><tr><td>Min [ms]</td><td>36.5</td><td>65.5</td><td>159.9</td><td>73.2</td><td>76.4</td><td>131.5</td><td>28.5</td><td>31.3</td><td>32.9</td><td>28.1</td><td>29.3</td><td>31.0</td></tr><tr><td>Max [ms]</td><td>55.4</td><td>86.6</td><td>179.9</td><td>119.8</td><td>103.9</td><td>171.4</td><td>47.9</td><td>57.3</td><td>47.9</td><td>35.8</td><td>53.4</td><td>55.5</td></tr><tr><td>Relative</td><td>1.00</td><td>1.63 ± 0.16</td><td>3.80 ± 0.32</td><td>1.00</td><td>1.07 ± 0.14</td><td>1.85 ± 0.23</td><td>1.00</td><td>1.10 ± 0.17</td><td>1.15 ± 0.15</td><td>1.00</td><td>1.07 ± 0.11</td><td>1.14 ± 0.13</td></tr></tbody></table>

## Taint Propagation.

* Field granularity to the *to_scrub* attribute.

* A function attribute to track its return value or parameter(s) as a source of sensitive data. Such a feature would simplify analyzing API functions without creating some execution context.

* A function attribute to declare a memcpy-like function, propagating a taint from a given argument to another.

* An analysis parameter as a propagation depth threshold, which, when hit, would stop propagation from the associated source. This parameter would especially benefit the analysis of cryptographic-related functions (e.g., encryption) because we will automatically stop tainting ciphertexts that no longer leak anything.

**Automatic Annotation.** GnuZero requires manual annotation of sensitive data sources (passwords, cryptographic keys, etc.). Techniques from [55] or [56] to automatically identify secrets could benefit the analysis. Though automatic annotation is no panacea, existing techniques are specialized for a given class of secrets (e.g., crypto keys). A tradeoff might rely on functions, assuming that secrets mostly undergo a specific flow (e.g., key derivation). Thus, manually annotated functions will automatically annotate the associated secrets.

## B. Extending to Different Compilers

**Different GCC versions.** Technically, GnuZero includes two components: the first is a GCC plugin that can be easily ported since it leverages the standard GSA API. The second component involves modifications within the GSA, which makes portability more engaging, as it involves merging our code with the GSA of the target GCC.

**LLVM.** GnuZero relies on the GSA based on the GCC GIMPLE IR, which binds it to GCC, even though the LLVM IR and GIMPLE IR share similarities. There are three ways to port GnuZero to LLVM:

* A GSA-like module in the LLVM toolchain, which is challenging because LLVM is primarily designed for optimizations rather than program analysis.

* a plugin within the Clang SA (not LLVM), which is limited because it misses zeroization violations caused by DSE as those are performed within the LLVM (not Clang) optimization phase.

* a GIMPLE to LLVM IR transpiler (e.g., wyrm [57]).

Since we seek a compiler-based approach, we do not consider already existing works (e.g. pitchfork [58]) built upon symbolic execution engines (e.g., haybale [59] or KLEE [60]) that perform secrets tainting on LLVM IR.

# VIII. CONCLUSION

**Summary.** The effective management of sensitive data within software systems is critical for better security. This paper introduced GnuZero, a compiler-based tool that detects missing zeroization in stack and heap variables containing sensitive data. Through its integration with the GCC Static Analyzer, GnuZero achieves precision and efficiency, as demonstrated in its application to real-world benchmarks and well-known cryptographic modules. Our evaluation assess GnuZero's ability to detect vulnerabilities, including those caused by dead store elimination, with no or little false positives. These results highlight the potential of GnuZero and the advantages of a compiler-based tool approach:

* **Automatic Detection:** GnuZero successfully identifies CWE-226 and CWE-244 instances, improving manual and ad-hoc techniques for ensuring zeroization.

* **Taint Tracking:** By leveraging the GSA symbolic execution, GnuZero introduces tainting semantics that track sensitive data through intraprocedural and interprocedural flows, thereby detecting leakage in any stack frame.

* **Optimization-Aware:** Operating at the GIMPLE level, GnuZero considers relevant compiler optimizations, such as dead store elimination, to underline vulnerabilities introduced during compilation.

* **Scalable:** GnuZero analyzed several real-world codes from different types of open-source projects: libsodium, Linux kernel crypto modules and wpa_supplicant.

**Acknowledgement.** We would like to thank the GCC SA maintainer, David Malcolm [61], for his insights and guidance regarding this work despite his busy schedule.
REFERENCES

[1] S. A. Olmos, G. Barthe, R. Gonzalez, B. Grégoire, V. Laporte, J. Léchenet, T. Oliveira, and P. Schwabe, "High-assurance zeroization," *IACR Trans. Cryptogr. Hardw. Embed. Syst.*, vol. 2024, no. 1, pp. 375-397, 2024.

[2] J. Chow, B. Pfaff, T. Garfinkel, K. Christopher, and M. Rosenblum, "Understanding data lifetime via whole system simulation (awarded best paper!)," in *USENIX Security Symposium*, pp. 321-336, USENIX, 2004.

[3] Daemonic Dispatches - Musings from Colin Percival, "Zeroing buffers is insufficient." http://www.daemonology.net/blog/2014-09-06-zeroing-buffers-is-in insufficient.html, 2014.

[4] R. Chapman, "Sanitizing sensitive data: How to get it right (or at least less wrong...)," in *Ada-Europe*, vol. 10300 of *Lecture Notes in Computer Science*, pp. 37-52, Springer, 2017.

[5] Common Weakness Enumerations, "Cwe-226: Sensitive information in resource not removed before reuse." https://cwe.mitre.org/data/definitions/226.html, 2024.

[6] Common Weakness Enumerations, "Cwe-244: Improper clearing of heap memory before release ('heap inspection')." https://cwe.mitre.org/data/definitions/244.html, 2024.

[7] Common Weakness Enumerations, "Cwe-14: Compiler removal of code to clear buffers." https://cwe.mitre.org/data/definitions/14.html, 2024.

[8] Z. Yang, B. Johannesmeyer, A. T. Olesen, S. Lerner, and K. Levchenko, "Dead store elimination (still) considered harmful," in *USENIX Security Symposium*, pp. 1025-1040, USENIX Association, 2017.

[9] Daemonic Dispatches - Musings from Colin Percival, "Erratum." https://www.daemonology.net/blog/2014-09-05-erratum.html, 2014.

[10] Michael Howard, "Some bad news and some good news." https://learn.microsoft.com/en-us/previous-versions/ms972826(v=msdn.10), 2002.

[11] L. Daniel, S. Bardin, and T. Rezk, "Binsec-rel: Symbolic binary analyzer for security with applications to constant-time and secret-erasure," *ACM Trans. Priv. Secur.*, vol. 26, no. 2, pp. 11:1-11:42, 2023.

[12] E. Eide and J. Regehr, "Volatiles are miscompiled, and what to do about it," in *EMSOFT*, pp. 255-264, ACM, 2008.

[13] Github - Bitcoin Core, "Memory zeroization improvements." https://github.com/bitcoin-core/secp256k1/issues/185, 2015.

[14] Github - Linux PAM, "Memory erasure followups." https://github.com/linux-pam/linux-pam/pull/599, 2023.

[15] National Vulnerability Database, "Cve-2023-32784 detail." https://nvd.nist.gov/vuln/detail/CVE-2023-32784, 2024.

[16] Github - LLVM Project, "Clang's static analyzer should be able to warn when memset() / memmove() / memcpy() are optimized away by dead store elimination." https://github.com/llvm/llvm-project/issues/61098, 2015.

[17] "GnuZero Artifacts." https://doi.org/10.5281/zenodo.14277842.

[18] Free Software Foundation, "GCC Website." https://gcc.gnu.org/.

[19] Free Software Foundation, "GCC 10 Changes." https://gcc.gnu.org/gcc-10/changes.html.

[20] GSA Webpage. https://gcc.gnu.org/wiki/StaticAnalyzer, 2024.

[21] Z. Xu, T. Kremenek, and J. Zhang, "A memory model for static analysis of C programs," in *ISoLA (1)*, vol. 6415 of *Lecture Notes in Computer Science*, pp. 535-548, Springer, 2010.

[22] T. W. Reps, S. Horwitz, and S. Sagiv, "Precise interprocedural dataflow analysis via graph reachability," in *POPL*, pp. 49-61, ACM Press, 1995.

[23] Free Software Foundation, "GCC Static Analyzer Documentation." http://gcc.gnu.org/onlinedocs/gcc/Static-Analyzer-Options.html.

[24] MITRE. https://www.mitre.org/, 2024.

[25] Free Software Foundation, "GCC Static Analyzer Documentation." http://sourceware.org/glibc/wiki/MallocInternals#Realloc_Algorithm.

[26] Multiple Contributors, "SEI CERT C Coding Standard." https://wiki.sei.cmu.edu/confluence/x/VNcxBQ.

[27] ANSSI, "Rules for Secure C Language Software Development." https://cyber.gouv.fr/sites/default/files/2022/04/anssi-guide-rules_for_secure_c_language_software_development-v1.4.pdf#section*.162, 2022.

[28] GCC Online Documentation, "Common function attributes." https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-tailed_005fargs-function-attribute, 2024.

[29] "Secretgrind: a valgrind analysis tool to detect secrets in memory." https://github.com/lmrs2/secretgrind, 2014.

[30] R. David, S. Bardin, T. D. Ta, L. Mounier, J. Feist, M. Potet, and J. Marion, "BINSEC/SE: A dynamic symbolic execution toolkit for binary-level analysis," in *SANER*, pp. 653-656, IEEE Computer Society, 2016.

[31] SAMATE project - Software Quality Group, "NIST Software Assurance Reference Dataset." https://samate.nist.gov/SARD/.

[32] M. Contributors, "Linux - github repository." https://github.com/torvalds/linux.

[33] D. J. Wheeler and R. M. Needham, "Tea, a tiny encryption algorithm," in *FSE*, vol. 1008 of *Lecture Notes in Computer Science*, pp. 363-366, Springer, 1994.

[34] D. J. Wheeler and R. M. Needham, "Tea extensions." https://www.cix.co.uk/~klockstone/xtea.pdf, 1997.

[35] M. Contributors, "wpa_supplicant_set_wpa_none_key - GitHub Repository." https://github.com/michael-dev/hostapd/blob/f91680c15f80f0b617a0d2c369c8c1bb3dcf078b/wpa_supplicant/wpa_supplicant.c#L158.

[36] D. Peter, "hyperfine." https://github.com/sharkdp/hyperfine, Mar. 2023.

[37] J. Chow, B. Pfaff, T. Garfinkel, and M. Rosenblum, "Shredding your garbage: Reducing data lifetime through secure deallocation," in *USENIX Security Symposium*, USENIX Association, 2005.

[38] R. R. Hansen and C. W. Probst, "Non-interference and erasure policies for java card bytecode," in *6th International Workshop on Issues in the Theory of Security (WITS '06)*, USENIX Association, 2006.

[39] A. Pridgen, S. L. Garfinkel, and D. S. Wallach, "Present but unreachable: Reducing persistent latent secrets in hotspot JVM," in *HICSS*, pp. 1-10, ScholarSpace / AIS Electronic Library (AISel), 2017.

[40] J. Lee and D. S. Wallach, "Removing secrets from android's TLS," in *NDSS*, The Internet Society, 2018.

[41] J. Lee, A. Chen, and D. S. Wallach, "Total recall: Persistence of passwords in android," in *NDSS*, The Internet Society, 2019.

[42] X. Leroy, "Formal verification of a realistic compiler," *Commun. ACM*, vol. 52, no. 7, pp. 107-115, 2009.

[43] L. Simon, D. Chisnall, and R. J. Anderson, "What you get is what you C: controlling side effects in mainstream C compilers," in *EuroS&P*, pp. 1-15, IEEE, 2018.

[44] S. T. Vu, A. Cohen, A. de Grandmaison, C. Guillon, and K. Heydemann, "Reconciling optimization with secure compilation," *Proc. ACM Program. Lang.*, vol. 5, no. OOPSLA, pp. 1-30, 2021.

[45] GCC Online Documentation, "Common type attributes." https://gcc.gnu.org/onlinedocs/gcc/Common-Type-Attributes.html#index-strub-typ-e-attribute, 2024.

[46] Y. Tang, P. Ames, S. Bhamidipati, A. Bijlani, R. Geambasu, and N. Sarda, "Cleanos: Limiting mobile data exposure with idle eviction," in *OSDI*, pp. 77-91, USENIX Association, 2012.

[47] T. Müller, F. C. Freiling, and A. Dewald, "TRESOR runs encryption securely outside RAM," in *USENIX Security Symposium*, USENIX Association, 2011.

[48] N. Zhang, K. Sun, W. Lou, and Y. T. Hou, "Case: Cache-assisted secure execution on ARM processors," in *IEEE Symposium on Security and Privacy*, pp. 72-90, IEEE Computer Society, 2016.

[49] M. Kolosick, B. A. Shivakumar, S. Cauligi, M. Patrignani, M. Vassena, R. Jhala, and D. Stefan, "Robust constant-time cryptography," *CoRR*, vol. abs/2311.05831, 2023.

[50] Y. Du, O. Alrawi, K. Z. Snow, M. Antonakakis, and F. Monrose, "Improving security tasks using compiler provenance information recovered at the binary-level," in *CCS*, pp. 2695-2709, ACM, 2023.

[51] W. Enck, P. Gilbert, B. Chun, L. P. Cox, J. Jung, P. D. McDaniel, and A. Sheth, "Taintdroid: An information-flow tracking system for realtime privacy monitoring on smartphones," in *OSDI*, pp. 393-407, USENIX Association, 2010.

[52] L. P. Cox, P. Gilbert, G. Lawler, V. Pistol, A. Razeen, B. Wu, and S. Cheemalapati, "Spandex: Secure password tracking for android," in *USENIX Security Symposium*, pp. 481-494, USENIX Association, 2014.

[53] Wei Ming Khoo, "A taint-tracking plugin for the valgrind memory checking tool." https://github.com/wmkhoo/taintgrind, 2023.

[54] A. Djoudi and S. Bardin, "BINSEC: binary code analysis with low-level regions," in *TACAS*, vol. 9035 of *Lecture Notes in Computer Science*, pp. 212-217, Springer, 2015.

[55] T. Faingnaert, W. V. Iseghem, and B. D. Sutter, "K-hunt++: Improved dynamic cryptographic key extraction," in *Checkmate@CCS*, pp. 22-29, ACM, 2024.

[56] C. Meijer, V. Moonsamy, and J. Wetzels, "Where's crypto?: Automated identification and classification of proprietary cryptographic primitives in binary code," in *USENIX Security Symposium*, pp. 555-572, USENIX Association, 2021.

[57] Jeremy Rifkin, "Wyrm: a gimple to llvm ir transpiler." https://github.com/jeremy-rifkin/wyrm, 2024.
<footnote>[58] S. Cauligi, C. Disselkoen, K. von Gleissenthall, D. M. Tullsen, D. Stefan, T. Rezk, and G. Barthe, "Constant-time foundations for the new spectre era," in *PLDI*, pp. 913–926, ACM, 2020.</footnote>

<footnote>[59] PLSysSec, "Github - Haybale." https://github.com/PLSysSec/haybale, 2019.</footnote>

<footnote>[60] C. Cadar, D. Dunbar, and D. R. Engler, "KLEE: unassisted and automatic generation of high-coverage tests for complex systems programs," in *OSDI*, pp. 209–224, USENIX Association, 2008.</footnote>

<footnote>[61] David Malcolm author's page. https://developers.redhat.com/author/david-malcolm, 2024.</footnote>