Preventing Timing Side-Channels via Security-Aware Just-In-Time Compilation

by   Qi Qin, et al.

Recent work has shown that Just-In-Time (JIT) compilation can introduce timing side-channels to constant-time programs, which would otherwise be a principled and effective means to counter timing attacks. In this paper, we propose a novel approach to eliminate JIT-induced leaks from these programs. Specifically, we present an operational semantics and a formal definition of constant-time programs under JIT compilation, laying the foundation for reasoning about programs with JIT compilation. We then propose to eliminate JIT-induced leaks via a fine-grained JIT compilation for which we provide an automated approach to generate policies and a novel type system to show its soundness. We develop a tool DeJITLeak for Java based on our approach and implement the fine-grained JIT compilation in HotSpot. Experimental results show that DeJITLeak can effectively and efficiently eliminate JIT-induced leaks on three datasets used in side-channel detection



page 1

page 2

page 3

page 4


Towards Constant-Time Foundations for the New Spectre Era

The constant-time discipline is a software-based countermeasure used for...

TaDA Live: Compositional Reasoning for Termination of Fine-grained Concurrent Programs

We introduce TaDA Live, a separation logic for reasoning compositionally...

Determinating Timing Channels in Compute Clouds

Timing side-channels represent an insidious security challenge for cloud...

A Brief Overview of the KTA WCET Tool

KTA (KTH's timing analyzer) is a research tool for performing timing ana...

Timing Covert Channel Analysis of the VxWorks MILS Embedded Hypervisor under the Common Criteria Security Certification

Virtualization technology is nowadays adopted in security-critical embed...

Efficient Detection and Quantification of Timing Leaks with Neural Networks

Detection and quantification of information leaks through timing side ch...

Fine-Grained Static Detection of Obfuscation Transforms Using Ensemble-Learning and Semantic Reasoning

The ability to efficiently detect the software protections used is at a ...
This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1. Introduction

Timing side-channel attacks allow an adversary to infer secret information by measuring the execution time of an implementation, thus pose a serious threat to secure systems (Kocher, 1996). One notorious example is Lucky 13 attack that can remotely recover plaintext from the CBC-mode encryption in TLS due to an unbalanced branch statement (AlFardan and Paterson, 2013).

Constant-time principle, which requires the execution time of an implementation being independent of secrets, is an effective countermeasure to prevent such attacks. However, writing constant-time programs is error-prone. For instance, even though two protections against Lucky 13 were implemented in AWS’s s2n library, a variant of Lucky 13 can remotely and completely recover plaintext from the CBC-mode cipher suites in s2n (Albrecht and Paterson, 2016). Therefore, various approaches have been proposed for automatically verifying constant-time security of high-/intermediate-level programs, e.g., ct-verif (Almeida et al., 2016) and CacheAudit (Doychev et al., 2013) for C programs, CT-Wasm (Watt et al., 2019) for WASM programs, Blazer (Antonopoulos et al., 2017) and Themis (Chen et al., 2017) for Java programs, and FaCT (Cauligi et al., 2019) for eliminating leakages.

However, constant-time programs may be still vulnerable in practice if the runtime environment is not fully captured by constant-time models. For instance, static compilation from high-/intermediate-level programs to low-level counterparts can destruct constant-time security (Barthe et al., 2018; Barthe et al., 2020; Barthe et al., 2021); constant-time executable programs are vulnerable in modern processors due to, e.g., speculative or out-of-order execution (Kocher et al., 2019; Lipp et al., 2018; Cauligi et al., 2020); JIT compilation makes constant-time bytecode vulnerable (Brennan et al., 2020a, b), called JIT-induced leaks hereafter. In this work, we focus on JIT-induced leaks.

JIT compilation has been used in numerous programming language engines (e.g., PyPy for Python, LuaJIT for Lua, HotSpot for Java, and V8 for JavaScript) to improve performance. However, JIT compilation can break the balance of conditional statements, e.g., some methods are JIT compiled or inlined in one branch but not in the other branch, or one branch is speculatively optimized, making constant-time programs vulnerable at runtime as shown in (Brennan et al., 2020a, b). Despite the serious risk of JIT compilation, there is no rigorous approach to eliminate JIT-induced leaks otherwise completely disabling JIT compilation.

In this work, we aim to automatically and rigorously eliminate JIT-induced leaks. Our contributions are both theoretical and practical. On the theoretical side, we first lay the foundations for timing side-channel security under JIT compilation by presenting a formal operational semantics and defining a notion of constant-time for a fragment of the JVM under JIT compilation. We do not model concrete JIT compilation as done by (Flückiger et al., 2018; Barrière et al., 2021). Instead, we leave them abstract in our model and model JIT compilation via compilation directives controlled by the adversary. This allows to consider very powerful attackers who have control over JIT compilation. It also makes it possible to reason about bytecode running with JIT compilation and uncover how code can leak secrets due to JIT compilation in a principled way. We then propose to prevent JIT-induced leaks via a fine-grained JIT compilation and present a type system for statically checking the effectiveness of policies for fine-grained JIT compilation.

On the practical side, we propose DeJITLeak, an automatic technique to generate policies that can be proven to completely eliminate JIT-induced leaks, while still benefiting from the performance gains of JIT compilation; in addition, a lightweight variant of DeJITLeak, DeJITLeak, can eliminate most of the leaks with a low overhead for more performance-conscious applications and is still sound if methods invoked in both sides of each secret branching statement are the same. We implement DeJITLeak as a tool and fine-grained JIT compilation in HotSpot JVM from OpenJDK. We conduct extensive experiments on three datasets used in recent side-channel detection: DifFuzz (Nilizadeh et al., 2019), Blazer (Antonopoulos et al., 2017) and Themis (Chen et al., 2017). Experimental results show that our approach significantly outperforms the strategies proposed in (Brennan, 2020). We report interesting case studies which shed light on directions for further research in this area.

In summary, our contributions are:

  • A formal treatment of JIT-induced leaks including an operational semantics and a constant-time notion under JIT compilation;

  • A protection mechanism against JIT-induced leaks via a fine-grained JIT compilation and an efficient approach to generate policy for fine-grained JIT compilation with security guarantees;

  • A practical tool that implements our approach and extensive experiments to demonstrate the efficacy of our approach.

2. Overview

In this section, we first give a brief overview of the side-channel leaks induced by JIT compilation (Brennan et al., 2020a). We will exemplify these JIT-induced leaks using the HotSpot virtual machine (HotSpot for short) on OpenJDK 1.8. We then give an overview of our approach to identify and eliminate the JIT-induced leaks automatically.

[fontsize=,bgcolor=bg]java boolean pwdEq(char[] a,char[] b) boolean equal = true; boolean shmequal = true; for (int i = 0; i ¡ 8; i++) if (a[i] != b[i]) equal = false; else shmequal = false; return equal;

(a) The pwdEq method
(b) JIT enabled
(c) JIT disabled
(d) Mitigated
Figure 1. The pwdEq method and its execution time with JIT enabled and disabled under Topti

2.1. JIT-Induced Leaks

JIT-induced leaks could be caused at least by the following three JIT compilation techniques, i.e., (1) optimistic compilation, (2) branch prediction, and (3) method compilation.

Optimistic compilation (Topti). Optimistic compilation is a type of speculation optimizations (Barrière et al., 2021). During the JIT compilation of a method, the compiler speculates on the most likely executed branches by pruning rarely executed branches. Therefore, it reduces the amount of time required to compile methods at runtime and space to store the native code. However, there might be a subsequent execution where the speculation fails and the execution must fall back to bytecode in the interpreted mode. To handle this issue, a deoptimization point (known as an uncommon trap in HotSpot) is added to the native code and, when encountered, deoptimization is performed, which recovers the program state and resumes execution using bytecode.

Clearly, executing the native code after compilation is much more efficient if no deoptimization occurs. However, when deoptimization occurs, it will take a longer time to deoptimize and roll back to the bytecode. This difference in execution time induces the Topti timing side-channel even if branches are balanced in bytecode. When the attacker can feed inputs to the program, Topti could be triggered for a conditional statement whose condition relies on secrets. The attacker would then be able to infer the secret information from the difference between execution time.

As an example, consider the pwdEq method shown in Figure 0(a), which is extracted and simplified from the DARPA Space/Time Analysis for Cybersecurity (STAC) engagement program gabfeed_1 (STAC, 2017). It takes strings and with length as inputs, denoting the user-entered and correct passwords, respectively. It checks if they are identical within a loop. The flag equal is assigned by false if two chars mismatch. To balance execution time, the dummy flag shmequal is introduced and assigned by false if two chars match.

The pwdEq method is marked as safe in STAC and would be verified as safe by the timing side-channel verification tools Blazer (Antonopoulos et al., 2017) and Themis (Chen et al., 2017) which do not consider JIT compilation. However, it indeed is vulnerable to Topti. To trigger Topti, we execute pwdEq 50,000 times using two strings “PASSWORD” and “password”. After that, the else-branch is replaced by the corresponding uncommon trap, so the costly deoptimization will perform later. To produce this, we use two random strings and with length 8 such that is ‘p’, is not ‘p’, and the rest is the same. We collect the execution time by executing pwdEq with inputs “password” and “password”, respectively. This mimics the process that an attacker guesses the secret data char-by-char, avoiding guessing the entire secret data simultaneously. The distribution of the execution time is shown in Figure 0(b). As a cross reference, Figure 0(c) shows the distribution of execution time with JIT compilation disabled. We can observe that the difference in the execution time between two branches is much larger when JIT compilation is enabled, allowing the attacker to infer if the first char is correctly guessed.

Branch prediction (Tbran). Branch prediction is a conservative optimization of conditional statements. Instead of pruning rarely executed branches, branch prediction generates native code by reordering the basic blocks to avoid jumps over frequently executed branches and thus improves the spacial locality of instruction cache. However, the reordering of basic blocks unbalances the execution time of branches even if it is balanced in bytecode.

If the attacker can feed inputs to the program, Tbran could be triggered for a conditional statement whose condition relies on secrets, and thus the attacker could be able to infer secret information by measuring the execution time. Although the difference in the execution time between branches via Tbran is small for a single conditional statement, it may be amplified by repeated executions (e.g., enclosed in a loop).

Method compilation (Tmeth). The most fundamental feature of JIT compilation is method compilation, which can be triggered if a method is frequently invoked or some backward jumps are frequently performed. In practice, a sophisticated multi-tier compilation mechanism is adopted (in e.g., HotSpot), where a method may be recompiled multiple times to more optimized native code to further improve the performance. Meanwhile, during compilation, frequently invoked small methods could be inlined to speed up execution.

For a conditional statement with method invocations, if the attacker can enforce some methods in a branch to be frequently invoked in advance so that those methods are (re)compiled or inlined, the execution time of this branch may be shortened. This difference in execution time between branches induces a timing side-channel, called Tmeth.

Concrete demonstrations of Tbran and Tmeth are given in the supplementary material.

2.2. Automatically Eliminating JIT-induced Leaks

In this work, we present an automated, rigorous approach to eliminate the above-mentioned JIT leaks. We do not consider CPU-level speculative leaks (Kocher et al., 2019; Cauligi et al., 2020) and cache-induced leaks (Almeida et al., 2016; Doychev and Köpf, 2017) for which various mitigation techniques have been proposed in literature, e.g., (Wang et al., 2019; Yan et al., 2018; Vassena et al., 2021; Yu et al., 2020; He et al., 2021; Wu et al., 2018). We assume that the bytecode program is of constant-time, which can largely be achieved by existing work (e.g., (Agat, 2000; Mantel and Starostin, 2015; Antonopoulos et al., 2017; Cauligi et al., 2019; Chen et al., 2017)). Our goal is to protect constant-time bytecode programs from the JIT-induced leaks discussed before. In general, there are trace-based and method-based JIT compilation approaches (Inoue et al., 2011), and we shall focus on the latter in this work.

A straightforward way to prevent JIT-induced leaks is to simply disable JIT compilation completely or JIT compilation of the chosen methods. Indeed, (Brennan, 2020) proposed the following three compilation strategies, named NOJIT, DisableC2 and MExclude. (1) The NOJIT strategy directly disables JIT compilation (e.g., both the C1 and C2 compilers in HotSpot), so no method will be JIT compiled. This strategy is effective and convenient to deploy, but could lead to significant performance loss. (2) The DisableC2 strategy only disables the C2 compiler instead of the entire JIT compilation, by which the leaks induced by the C2 compiler (e.g., Topti) can be prevented, but not for Tbran or Tmeth. This strategy also sacrifices the more aggressive C2 optimization and hence may suffer from performance loss. (3) The MExclude strategy disables JIT compilation for the user-chosen methods instead of the entire program, by which some leaks (i.e., Tbran and Topti) can be prevented. This strategy cannot prevent from Tmeth, but its main shortcoming is that there is no protection for non-chosen methods, and it is also unclear how to choose methods to disable. In summary, the existing compilation strategies either incur a high performance cost or fail to prevent all the known JIT-induced leaks.

Disabling JIT compilation at the method level is indeed unnecessary. Essentially, we only need to ensure that secret information will not be leaked when the methods are JIT compiled or inlined. An important observation is that secret information can only be leaked when there is a conditional statement whose condition relies on secret data, and at least one of the following cases occurs, namely,

  1. (Tmeth leaks) a method invoked in a branch is JIT compiled or inlined;

  2. (Tbran leaks) the conditional statement is optimized with the branch prediction optimization;

  3. (Topti leaks) the conditional statement is optimized with the optimistic optimization;

Based on the above observation, we propose a novel approach DeJITLeak, to eliminate JIT-induced leaks. To the best of our knowledge, this is the first work to prevent all the above JIT-induced leaks without disabling any compiler in HotSpot, which is in a sharp contrast with the compilation strategies proposed in (Brennan, 2020).

In a nutshell, DeJITLeak automatically locates secret branch points (program points with conditional statements whose conditions rely on secret data) by a flow-, object- and context-sensitive information flow analysis of Java bytecode (Volpano et al., 1996). The conditional statements at those secret branch points should not be optimized via branch prediction or optimistic compilations. It then extracts all the methods invoked in those conditional statements and identifies those methods that should not be JIT compiled or inlined. Based on these, we put forward a fine-grained JIT compilation.

We introduce a type system to prove the soundness of the fine-grained JIT compilation, i.e., under which the resulting program is free of the aforementioned JIT-induced leaks if its bytecode version is leakage-free. To this end, we introduce a JVM submachine and formulate an operational semantics with JIT compilation. We also provide a notion of JIT-constant-time to formalize timing side-channel security of programs under JIT compilation. We show that a constant-time program remains constant-time under our fine-grained JIT compilation if the program is well-typed under our type system. Note that our approach does not guarantee that all the identified branch points or methods are necessary, but the precision of our approach is assured by the advanced information flow analysis and is indeed validated by experiments in Section 6.

Finally, the fine-grained JIT compilation is implemented by modifying HotSpot. Our experimental results show that our approach is significantly more effective than DisableC2 and MExclude, and is significantly more efficient than NOJIT.

2.3. Threat Model

In this work, we focus on timing side-channel leaks induced by branch prediction, optimistic compilation and method compilation. We assume that the adversary is able to influence how bytecode is JIT compiled and deoptimized by feeding inputs to programs to trigger branch prediction and optimistic compilation of chosen conditional statements, or method compilation and deoptimization of chosen methods. The time for JVM profiling, JIT compilation and garbage collection is not taken into account, as they are often performed in distinct threads. We do not consider other JIT optimizations such as constant propagation, loop unfolding and dead elimination, which are often difficult to be controlled by the adversary at runtime. To the best of our knowledge, no existing attack leverages these optimizations. We do not consider CPU-level optimizations (such as speculative execution and cache) which have been studied, e.g., (Wang et al., 2019; Yan et al., 2018; Vassena et al., 2021; Cauligi et al., 2020; Yu et al., 2020; He et al., 2021).

3. The Language: Syntax, Semantics and Constant-Time

In this section, we present a fragment of JVM and formalize timing side-channel security via the notion of constant-time.

3.1. The JVM Submachine

We define a fragment JVM of JVM with (conditional and unconditional) jumps, operations to manipulate the operand stack, and method calls. Both bytecode and native code are presented in JVM. Note that this is for the sake of presentation, as our methodology is generic and could be adapted to real instruction sets of bytecode and native code.

Syntax. Let (resp. ) be the finite set of local (resp. global) variables, be the set of values, be a finite set of methods. A program comprises a set of methods, each of which is a list of instructions taken from the instruction set in Figure 2. All these instructions are standard except for the instruction deopt md which is used to model uncommon traps (cf. Section 2) .

::= binop  binary operation on the operand stack
push  push value on top of the operand stack
pop pop value from top of the operand stack
swap swap the top two operand stack values
load  load value of onto the operand stack
store  pop and store top of the operand stack in
get  load value of onto the operand stack
put  pop and store top of the operand stack in
ifeq  conditional jump
ifneq  conditional jump
goto  unconditional jump
invoke  invoke the method
return return the top value of the operand stack
deopt md deoptimize with meta data md
Figure 2. Instruction set of JVM, where is a local variable and is a global variable

For each method , denotes the instruction in at the program point and denotes the formal arguments of . When a method is invoked, the execution starts with the first instruction . We also denote by for the sequence of instructions .

Compilation directive. To model method compilation with procedure inline, branch prediction and optimistic compilation optimizations, we use (compilation) directives which specify how the method should be (re)compiled and optimized at runtime. We denote by the set of directives of the method , and by the resulting version after compilation and optimization according to the directive . In particular, we use to denote no (re)compilation. The formal definition of directives is given in Section 3.2.3.

In general, a method in bytecode is compiled into native code which may be iteratively recompiled later. Thus, we assign to each method a version number , where the bytecode has the version number , and the highest version number is . A directive is invalid if and , otherwise is an invalid directive. Intuitively, the version number indicates the optimized level of the method . JIT recompilation only uses increasingly aggressive optimization techniques, and rolls back to the bytecode version otherwise.

Figure 3. Operational semantics of JVM, where denotes the domain of the partial function

State and configuration. A state is a tuple where

  • is the program counter that points to the next instruction in ;

  • is the current executing method;

  • is a partial function from local variables to values;

  • is the operand stack.

We denote by the set of states. For each function , variable and value , let be the function where for every , if , and otherwise. For two operand stacks , let denote their concatenation. The empty operand stack is denoted by .

A configuration is of the form or , where ch is a code heap storing the latest version of each method; is a (data) heap, i.e., a partial function from global variables to values; is the current state; is the call stack, and is a value. Configurations of the form are final configurations, reached after the return of the entry point. A configuration is an initial one if , is the entry point of the program, and . Let denote the set of configurations, be the concatenation of two call stacks and , and be the empty call stack.

Operational semantics with JIT Compilation. The small-step operational semantics of JVM is given in Figure 3 as a relation , where is an auxiliary relation. Directives apply to method invocations only, thus are associated to the relation only for method invocations. The semantics of each instruction is mostly standard except for the method invocation and deoptimization. We only explain some selected ones. Full explanation refers to the supplementary material.

Instruction return ends the execution of the current method, returns the top value of the current operand stack, either by pushing it on top of the operand stack of the caller and re-executes the caller from the return site if the current method is not the entry point, or enters a final configuration if the current method is the entry point.

Instruction deoptimizes the current method and rolls back to the bytecode in the interpreted mode. This instruction is only used in native code and inserted by JIT compilers. Our semantics does not directly model a deoptimization implementation. Instead, we assume there is a deoptimization oracle which takes the current configuration and the meta data md as inputs, and reconstructs the configuration (i.e., heap , state and the call stack ). Furthermore, the bytecode version of the method is restored into the code heap ch. We assume that the oracle results in the same heap , state and call stack as if the method were not JIT compiled.

The semantics of method invocation depends on the directive . If is then the instructions of in the code heap ch remain the same. If is valid, namely, the optimized version after applying has larger version number than that of the current version , the new optimized version is stored in the code heap ch. After that, it pops the top values from the current operand stack, passes them to the formal arguments of , pushes the calling context on top of the call stack and starts to execute in the code heap.

To define a JIT-execution, we introduce the notion of schedules. A valid schedule for a configuration is a sequence of valid directives such that the program will not get stuck when starting from and following for method invocations. The valid schedule yields a JIT-execution, denoted by , which is a sequence , such that is an initial configuration, is the final configuration, and for every , either or . We require that is equal to the sequence of directives along the JIT-execution, i.e., the concatenation of ’s. A JIT-free execution is thus a JIT-execution . Note that in this work, we assume that the execution of a program always terminates.

In the rest of this work, we assume that each method has one return instruction which does not appear in any branch of conditional statements, as early return often introduces timing side-channel leaks.

3.2. JIT Optimization of JVM

In this section, we first introduce branch prediction and optimistic compilation, then define method compilation as well as compilation directives in detail.

3.2.1. Branch prediction.

Consider a method and a conditional instruction . (We take ifeq as the example, and ifneq can be handled accordingly. ) Let (resp. ) be the instructions appearing in the if-(resp. else-)branch of , and the last instruction of is goto . The first and last instructions of are and respectively.

If the profiling data show that the program favors the else-branch, the branch prediction optimization transforms the method into a new method as shown in Figure 4 for . The formal definition and an illustrating example are given in the supplementary material.

If the profiling data shows that the program favors the if-branch, the branch prediction optimization to the conditional instruction transforms the method into a new method , similar to , except that (1) the conditional instruction is replaced by which is immediately followed by the if-branch ; (2) the else-branch is moved to the end of the method starting at the point and the target point of the last instruction goto  is revised to .

We denote by and the new methods and respectively. It is easy to see that the branch prediction optimization transforms the original program to a semantically equivalent program.

Figure 4. Branch prediction optimization

3.2.2. Optimistic compilation.

Again, consider the conditional instruction with the if-branch and else-branch . (ifneq can be dealt with accordingly.) If the profiling data show that the if-branch almost never gets executed, the optimistic compilation optimization transforms the method into a new method in a similar way to . Here, the if-branch is replaced by an uncommon trap. The method is defined similarly if the profiling data show that the else-branch almost never gets executed. More details refer to the supplementary material.

We denote by and the new methods and after transformation. It is easy to see that the optimistic compilation optimization is an equivalent program transformation under the inputs that does not trigger any uncommon traps.

3.2.3. Method Compilation

At runtime, frequently executed, small methods may be inlined to reduce the time required for method invocations. After that, both branch prediction and optimistic compilation optimizations could be performed. Thus, a compilation directive of a method should take into account procedure inline, branch prediction and optimistic compilation optimizations.

We define a compilation directive of a method as a pair , where is a labeled tree specifying the method invocations to be inlined, and is a sequence specifying the optimizations of branches. Formally, the labeled tree is a tuple , where is a finite set of nodes such that each is labeled by a method and the root is labeled by ; is a set of edges of the form denoting that the method is invoked at the call site of the method . We denote by the new method obtained from by iteratively inlining method invocations in . We assume the operand stack of each inlined method is balanced, otherwise the additional pop instructions are inserted.

The sequence is of the form , where for every , denotes the optimization type to be applied to the branch point in the method with the branch preference . We assume that an index occurs at most once in , as at most one optimization can be applied to one branch point.

Note that in our formalism, the optimistic compilation optimization adds one uncommon trap for each conditional statement. In practice, multiple conditional statements may share one uncommon trap, which is not modeled here but can be handled by our approach as well.

3.3. Consistency and Constant-Time

We assume that each program is annotated with a set of public input variables, while the other inputs are regarded as secret input variables. We denote by if the initial configurations and agree on the public input variables, and denote by if and have the same code heap.

Consistency. The following theorem ensures the equivalence of the final memory store and return value from the JIT-free execution and JIT-execution.

Theorem 3.1 ().

For each initial configuration of the program and each valid schedule for , we have:

iff .

If the output variables are partitioned into public and secret, we denote by that the final configurations and agree on the public output variables.

Theorem 3.2 ().

For each pair of initial configurations of the program with and each pair of valid schedules and for and respectively, we have: , and iff , and .

The theorem states that observing public output variables cannot distinguish secret inputs without JIT compilation iff observing public output variables cannot distinguish secret inputs with JIT compilation.

Constant-time. To model execution time, we define cost functions for bytecode and native code. Let and be the cost functions for instructions from the bytecode and native code, respectively. We assume that, for each pair of instructions, implies that . Namely, the cost equivalence of bytecode instructions are preserved in native code. We denote by the cost of the instruction , which is if it is running in bytecode mode, otherwise . We lift the function cf to states and configurations as usual, e.g., . The cost of a JIT-execution is the sum of all the costs of the executed instructions, i.e., .

A program is constant-time (without JIT compilation) if for each pair of initial configurations of such that and the code heaps of and have the same bytecode instructions, we have:

Intuitively, the constant-time policy requires that two JIT-free executions have the same cost if their public inputs are the same and code heaps have the same bytecode instructions, thus preventing timing side-channel leaks when JIT compilation is disabled.

JIT-constant-time. To define constant-time under JIT compilation, called JIT-constant-time, we first introduce some notations.

Consider a JIT-execution and a method , let denote the projection of the sequence of executed instructions in onto the pairs each of which consists of a program point and a version of the method . A proper prefix of can be seen as the profiling data of the method after executing these instructions, which determines a unique compilation directive of the method after executed . We leave runtime profiling abstract in order to model a large variety of JIT compilations and use to denote the profiling data of after executed instructions of or its compiled versions.

Let us fix a profiler pf, which provides one compilation directive of a method using the profiling data . The schedule is called a pf-schedule if, for each method and proper prefix of , the next compilation directive of after is .

Lemma 3.3 ().

For each pair of initial configurations of with , and each pair of valid pf-schedules and for and respectively, we have:

for every method , every pair of proper prefixes of and respectively, if then .

Intuitively, the lemma ensures that the compilation directives of each method in JIT-executions are the same under the same profiling data.

A program is JIT-constant-time if, for every pair of initial configurations of with and , every pair of valid pf-schedules and for and respectively satisfies

Intuitively, the JIT-constant-time policy requires that two JIT-executions have the same cost if their public inputs and initial code heap are the same and the valid schedules have the same profiler pf for JIT compilation, so it prevents timing side-channel leaks even if the JIT compilation is enabled. We allow the code heaps in and to be mixed with bytecode and native code, because the adversary can run the program multiple times with chosen inputs before launching attacks.

We remark that our definition of pf-schedules considers a powerful adversary who controls executing instructions and thus the compilation directives of methods, which is common in the study of detection and mitigation. In practice, the feasibility of compilation directives depends on various parameters in VM, e.g., whether a method invocation should be inlined depends on its code size, invocation frequency, method modifier, etc.

As argued above, a constant-time program may not be of JIT-constant-time due to JIT compilation. This work aims to prescribe a fine-grained JIT compilation under which a constant-time program is still JIT-constant-time so there is no need to disable JIT compilation completely.

T-Push T-Bop T-Str
T-Pop T-Swap T-Load
T-Put T-If T-Goto
T-Get T-Ifn T-Ret
Figure 5. Typing rules

4. Protect Mechanism and Type System

In this section, we first propose a two-level protection mechanism to eliminate JIT-induced leaks and then present an information-flow type system for proving JIT-constant-time under our protected JIT compilation.

4.1. Protection Mechanism

The first level of our protection mechanism is to disable JIT compilation and inlining of methods which potentially induce leaks. We denote by the set of methods that cannot be JIT compiled or inlined, i.e., these methods can only be executed in the interpreted mode. The second level is to disable JIT optimization of branch points in methods , whose optimization will potentially induce leaks. We denote by the mapping from to sets of branch points that cannot be JIT optimized. When the method is compiled, will be updated accordingly.

From the perspective of the JVM semantics, the compilation directive of any method from is limited to , and the compilation directives of any method can neither inline a method from nor optimize the branch at a program point in .

For a given program , a policy for fine-grained JIT compilation is given by a pair . A pf-schedule that is compliant to the policy is called a -schedule.

4.2. Type System and Inference

We propose an information-flow type system for proving that constant-time programs are JIT-constant-time under a fine-grained JIT compilation with a policy .

Lattice for security levels. We consider a lattice of security levels with , , and . Initially, all the public inputs have the low security level L and the other inputs have the high security level H. We denote by the least upper bound of two security levels , namely, for and .

Typing judgments. Our type system supports programs whose control flow depends on secrets. Thus, the typing rules for instructions rely on its path context pt, which can indicates whether an instruction is contained in a secret branch. We use functions and which map global and local variables to security levels. We also use a stack type (i.e., a stack of security levels) st for typing operand stack. The order is lifted to the functions and the stack type as usual, e.g., if for each .

The typing judgment for non-return instructions is of the form where is the method under typing, is a program point in . This judgment states that, given the typing context , the instruction yields a new typing context . The typing judgment of the return is of the form where ht is the security levels of the global variables and is the security level of the return value.

A security environment of a method is a function where for every program point of , is a typing context if is a return instruction, and otherwise.

Method signature. A (security) signature of a method is of the form which states that, given the typing context , each global variable has the security level and the return value of the method has the security level . Each invocation of should respect the signature of . The signature of the program , denoted by , is a mapping from the methods of the program to their signatures. Since a method invoked in any secret branch cannot be JIT compiled or inlined, we require that, for any , if the path context pt in is the high security level H.

Typing rules. The typing rules are presented in Figure 5, where the key premises are highlighted and means that and for the signature .

The type system only checks bytecode programs, thus there is no typing rule for the deoptimization instruction deopt md. Most typing rules are standard. For example, (T-Push), (T-Pop), (T-Bop) and (T-Swap) track the flow of the secret data via the operand stack, including explicit and implicit flows. Similarly, (T-Str), (T-Load), (T-Put) and (T-Get) track the flow of the secret data via local and global variables. Rule (T-Goto) does not change the typing context.

Rules (T-If) and (T-Ifn) require that the path context of each branch has a security level no less than the current path context and the security level of the branching condition on top of the stack. This allows us to track implicit flows during typing. Furthermore, the branch point should not be optimized by requiring if has the high security level H, otherwise the branches may become unbalanced, resulting in JIT-induced leaks.

Rule (T-Ret) requires that avoids the security levels of the global variables in ht and the security level of the return value are greater than these in the method signature .

Rule (T-Call) ensures that the context of meets the signature , e.g., avoiding that the current path context pt has a security level greater than the excepted one , and avoiding that actual arguments have the security levels greater than that of formal arguments.

Typable methods. The security of a constant-time program under JIT compilation is verified by type inference. To formalize this, we first introduce some notations (Barthe et al., 2013).

Consider a method , for each program point , let be the set of successors of w.r.t. the control flow. Formally, if is , if is or , if is return, and otherwise.

For each branch point , let denote its junction point, i.e., the immediate post-dominator of . (Recall that we assumed there is no early return in branches, thus is well-defined.) We denote by the set of program points that can be reached from the branch point and are post-dominated by . We denote by the set of branch points such that and for any . Intuitively, contains the branch points with the junction point and is not contained by of any other branch point with the same junction point , namely, nested branch points of the branch point are excluded.

A method is typable w.r.t. the signature and policy , denoted by , if there exists a security environment for such that for and one of the following conditions holds for each program point :

  • if is not a junction point, then