SecV: Secure Code Partitioning via Multi-Language Secure Values

Trusted execution environments like Intel SGX provide \emph{enclaves}, which offer strong security guarantees for applications. Running entire applications inside enclaves is possible, but this approach leads to a large trusted computing base (TCB). As such, various tools have been developed to partition programs written in languages such as C or Java into \emph{trusted} and \emph{untrusted} parts, which are run in and out of enclaves respectively. However, those tools depend on language-specific taint-analysis and partitioning techniques. They cannot be reused for other languages and there is thus a need for tools that transcend this language barrier. We address this challenge by proposing a multi-language technique to specify sensitive code or data, as well as a multi-language tool to analyse and partition the resulting programs for trusted execution environments like Intel SGX. We leverage GraalVM's Truffle framework, which provides a language-agnostic abstract syntax tree (AST) representation for programs, to provide special AST nodes called \emph{secure nodes} that encapsulate sensitive program information. Secure nodes can easily be embedded into the ASTs of a wide range of languages via Truffle's \emph{polyglot API}. Our technique includes a multi-language dynamic taint tracking tool to analyse and partition applications based on our generic secure nodes. Our extensive evaluation with micro- and macro-benchmarks shows that we can use our technique for two languages (Javascript and \python), and that partitioned programs can obtain up to $14.5\%$ performance improvement as compared to unpartitioned versions.


Introduction
Trusted execution environments (TEE) use hardware cryptography to enforce confidentiality, authenticity and integrity of a memory zone called an enclave.TEEs are at the basis of confidential computing, and major CPU vendors have introduced them in their processors, e.g., Intel's SGX [13], AMD's SME [19], and ARM's TrustZone [3,48], to provide security guarantees for cloud-based applications.
Minimising the trusted computing base (TCB) of a TEEenabled program is crucial for improving security.Achieving this goal is especially difficult for managed languages because they come with a runtime that contains large system libraries (e.g., Java, Python, R, etc.).As such, leveraging tools such as SCONE [4], SGX-LKL [49], or Graphene-SGX [56] that fully embed the language runtime inside the TEE is not satisfactory.For example, the Java library shipped with OpenJDK18 contains 47,146 Java classes for a total of 5,224,426 lines of Java code.Adding such a large code base inside a TEE increases the size and the attack surface of the TCB to an unacceptable degree.
To minimise the TCB size of applications written in managed languages, the code and the data of the application and the runtime must be partitioned into trusted parts and untrusted parts, which run inside and outside of the enclave, respectively.Since manually partitioning a large code base is complex and error-prone, several middleware technologies have been developed which permit developers to automatically partition their code prior to deployment in the cloud.
Unfortunately, partitioning tools only exist for a few languages: Java with Civet [57], Montsalvat [66] and Uranus [18], Go with GOTEE [14], and C with Glamdring [23].These tools depend heavily on the language semantics and cannot be reused for other languages.To support the next programming language (e.g., Python, R, JavaScript, Ruby, etc.), one must entirely re-implement a new automatic partitioning tool, which is time consuming and error-prone.
In this paper, we propose to decouple the partitioning tool from the language semantics, and to implement the former once and for all languages.To that end, we leverage Truffle [61], a Java library for building high-performance language interpreters.It provides a generic abstract syntax tree (AST) composed of nodes that represent various syntactic elements of a program: i.e., expressions (e.g., function calls or arithmetic operations), program values (e.g., literals or variables), control flow (e.g., if-else or for loops), etc. Truffle provides support for popular programming languages like Python, JavaScript, R, Ruby, C/C++, amongst many others.To implement the partitioning tool once and for all, we first introduce a new multi-language AST node type, called a secure node.This node contains a secure value corresponding to a sensitive value that has to be secured in the enclave.Then, we leverage the polyglot interoperability protocol provided by Truffle [15], which allows the declaration of a secure value from any language with an expression as simple as x = polyglot.eval("secV","sInt(42)").Finally, we develop a generic dynamic taint tracking tool, PolyTaint, which instruments Truffle ASTs to track the data flow of sensitive values from secure nodes so as to determine the portions of a program (e.g., functions) to be shielded inside the enclave.PolyTaint then partitions the program into two parts, trusted and untrusted, to be executed respectively inside or outside the enclave.
In summary, we propose the following contributions: 1. Generic AST secure nodes to specify sensitive data in any Truffle language.2. PolyTaint, a Truffle instrumentation tool which performs dynamic taint tracking on generic polyglot programs and partitions the programs based on the use of secure values.3.An extensive experimental evaluation demonstrating the effectiveness of our approach via micro-benchmarks in JavaScript and Python, as well as real-world applications: PageRank [2] and linear regression [64].Our analysis of partitioned programs shows we can reduce the size of the TCB (i.e., improved security) and improve performance (up to 14.5%) at the same time.The rest of the paper is organised as follows.§2 provides background concepts.We present our threat model in §3.§4 describes the architecture and workflow of SecV, followed in §5 by an extensive experimental evaluation.We discuss the limitations of SecV in §6, and provide ideas for further use cases in §7.Related work is discussed in §8, while we conclude and hint at future work in §9.

Background
2.1 Intel software guard extensions (SGX) Intel SGX [13,63] is a set of hardware instructions to create secure memory regions called enclaves.Enclaves provide strong confidentiality and integrity guarantees for code and data processed on a malicious node whose privileged software (e.g., kernel, hypervisor, etc.) is potentially compromised.Enclave data is stored in an encrypted portion of DRAM, the enclave page cache (EPC).EPC pages are transparently decrypted by a memory encryption engine (MEE) only when loaded into a CPU cache line.Enclaves typically have limited memory resources: 128 MB or 256 MB per socket in the more popular first generation SGX-enabled CPUs.The SGX Linux kernel driver supports paging from EPC memory to regular DRAM to accommodate enclaves larger than the EPC, but this comes at a performance cost [59].
Software that leverages Intel SGX is usually split in two parts: a trusted part which executes in enclave mode, and an untrusted part which executes in non-enclave mode.The Intel SGX SDK permits interaction between both parts via ecalls (from untrusted to trusted code), and ocalls (from trusted to untrusted).Intel SGX enclaves operate only in user mode and OS services (i.e., system calls) cannot be executed directly within the enclaves [4,56].They must instead be relayed to the untrusted part via ocalls.Both ecalls and ocalls trigger expensive context switches in the CPU [59] (accounting for up to 13,500 CPU cycles).Reducing the number of ecalls and ocalls is therefore key in designing efficient enclave software.
To facilitate the deployment of code in enclaves, several tools [4,49,53,56] make it possible to run unmodified applications inside SGX enclaves.While this approach is relatively straightforward to use for developers, it increases the chance of introducing security vulnerabilities into the enclave (due to the large TCB).Some language-specific alternatives [18,23,57,66] have been proposed recently to partition programs and keep only sensitive code and data within enclaves.

GraalVM
GraalVM is a high-performance JDK distribution that can execute programs implemented in a wide range of high-level languages, e.g., JavaScript, Python, Java, Kotlin, etc. [6,66].At the heart of GraalVM is the Graal compiler [47], a dynamic just-in-time (JIT) compiler that produces highly optimised machine code.
GraalVM native images.GraalVM provides a tool that can compile ahead-of-time (AOT) programs implemented in JVMbased languages (e.g., Java, Scala, etc.) to native executables, called native images.This tool leverages static analysis [60]  to determine which program elements (classes, methods, fields) are reachable at run time.These reachable elements, together with runtime components (i.e., the garbage collector, support for thread scheduling and synchronisation, etc.) are AOT-compiled into the final native image.This significantly lowers the memory footprint when compared to programs interpreted in a regular JVM, making native images very suitable for TEEs.In addition, GraalVM native images can run initialisation code at build time instead of at run time [60], which leads to faster start-up times in native images compared to other runtime environments.
Truffle.It is a framework provided by the GraalVM ecosystem to build tools and programming language implementations as self-modifying Abstract Syntax Tree (AST) interpreters [15,43].At low level, Truffle provides a base Node class that is leveraged by language implementers to build other AST nodes representing the semantic constructs of their programming language, e.g., an addition operation, a variable write, etc. Essentially, every node in a Truffle AST is executed (via an execute method) at runtime to produce a result.In Fig. 1 for example, the integer addition node will be executed at runtime to produce the sum of the left and child node values, which are computed by calling the execute methods of the left and right nodes respectively (depth-first traversal).
The key advantage of Truffle is that all programs implemented in a supported language are parsed to a common AST representation (i.e., the Truffle AST), which is then manipulated in a language-agnostic fashion.Truffle languages include JavaScript (JS) [32], Ruby [46], R [30], Python [31], LLVM-based languages [42], and more.
Polyglot API.Truffle allows developers to build polyglot applications that combine code written in different languages.This interoperability is provided by the polyglot interoperability protocol, a set of standardised messages (polyglot API ) implemented in every Truffle language [15,39].This API allows to transfer objects from one language scope to another as Truffle values, i.e., an instance of Value class.AST rewriting Figure 2. Instrumenting a variable write with a wrapper node.The event node's onReturnValue method could be used to register the name of the variable being written, as well as the corresponding value.9 } Listing 1. Java application accessing an object from a JS language scope via the polyglot API.
Listing 1 shows the transfer of an array object created in a JS language scope into the Java language scope via the polyglot API (line 6).The first parameter to the polyglot.evalcall is the truffle language id, a unique string identifier for Truffle guest languages, while the second parameter is the guest source code to execute.Such cross-language object exchange is possible in all Truffle languages, which simplifies the implementation of multi-language applications.
In this paper, the language accessing the objects generated by another interpreter will be referred to as the host language (Java in Listing 1), and the language used to generate the objects accessed by the host language is called the guest language (JS in Listing 1).Objects of the host language will be referred to as regular objects while those of the guest language will be referred to as foreign objects.
Truffle instrumentation agents.The Truffle framework provides an API to dynamically intercept the execution of nodes in the AST [15,58,62].This API was used to implement a program profiler [27], a debugger [33], and a taint-tracking tool [21].In PolyTaint, we leverage this API to implement our partitioning tool.
To intercept the execution of nodes, the developer has to implement an instrumentation agent [58].The agent intercepts the execution of nodes by leveraging syntactic tags associated to the AST nodes.These tags give the semantics of the nodes (e.g., call tag, variable write tag etc.).
Fig. 2 illustrates how an instrumentation agent works.When the agent is loaded, it associates an event node to a tag [29].In our example, the agent attaches the event node  to any AST node with the variable write tag.At execution, when Truffle visits an AST node with the instrumented tag for the first time, it replaces the node by a wrapper node [44] connected to the event node.In our example, when Truffle visits the node  for the first time, it wraps  in  and connects  with .
A wrapper node implements a special execute function.Upon a call, it first calls onEnter on the associated event node, then execute on the wrapped node, and finally onReturnValue on the associated event node.These functions  can collect metrics, modify the wrapped node, and even replace the wrapped node by another node.

Threat model
We consider a powerful adversary with full control over the software stack, including privileged software (i.e., host OS, hypervisor) with access to the physical hardware (i.e., DRAM, secondary storage, etc.).The adversary's goal is to disclose sensitive data or damage its integrity.The SecV workflow assumes enclave program development, taint tracking and program partitioning, as well as final enclave code building and signing are all done in a trusted environment, to prevent malicious code tampering disclosing sensitive information at runtime.The integrity of the trusted partition can be ensured via remote attestation [9,13].
The adversary cannot open the CPU package to extract decrypted enclave secrets.The final enclave code does not intentionally leak sensitive information (e.g., encryption keys).We do not consider denial-of-service (DoS) and side-channel attacks [7,52], for which mitigations exist [16,26].

Design and workflow of SecV
SecV is a framework that analyses applications and partitions them into trusted and untrusted parts for architectures with TEE support.It supports applications written in any Truffle language, and introduces secure values to specify sensitive data.Fig. 3 presents an overview of SecV's workflow, which comprises 4 phases: (1) identification of sensitive variables that contain secure values (see §4.1), (2) dynamic taint tracking with PolyTaint (see §4.2 and §4.3), (3) program partitioning with the partitioner (see §4.4), and (4) native image building with the image generator and final application creation (see §4.5).We illustrate our system's workflow by considering a simple linear regression program written in Python (see Listing 2).We consider a security scenario where we wish to keep the learned model (i.e., m and c) confidential [17,25,50,65].In the following, we will show how one can use SecV to enforce this scenario.

Identifying and specifying sensitive data
We propose a multi-language approach to specify sensitive data via the use of newly-introduced secure nodes in a Truffle AST.All values associated with these secure nodes are  referred to as secure values and they will represent sensitive information which must be kept inside the enclave.To make this possible, we leverage the Truffle framework to build a secure node generator which injects secure nodes into any Truffle AST via the polyglot API.This secure node generator is implemented like a regular Truffle language but provides primarily special AST nodes which comprise secure information.Our preliminary prototype provides secure node types: sInt, sDouble, sBoolean, and sArray to contain secure int values, secure double values, secure boolean values, and secure array object values respectively.For example, Listing 3 shows how a JavaScript program can inject a secure integer value into its AST.
Listing 3. Injecting a secure integer node with value 4 into a JavaScript program via the polyglot API.
Going back to our illustrative example, since our goal is to secure our ML model (represented by m and c), we can specify m and c as secure values using the Truffle polyglot API for Python.To do this, we change the variable assignments for m and c to the code shown in Listing 4 (lines 2 and 3).
At run time, the Truffle AST corresponding to the Python program will have SecV nodes associated with m and c once the polyglot.evalcall is executed.SecV then analyses the program's AST to determine which portions of the program access the secure values m and c.
Support for other secure node types.Implementing more secure types, e.g., lists, maps, etc. in SecV is straightforward.It involves: defining a new Truffle node for the type, implementing its internal representation, defining type operations and conversions, and integrating the new type node into SecV's language grammar.To simplify the development of complex types, Truffle's design allows the use of existing Java types, e.g., arrays, sets, etc. as building blocks when defining the new node.

Taint tracking
SecV includes PolyTaint, a Truffle code instrumentation tool that supports applications written in any Truffle language.PolyTaint is designed as a taint tracking tool.It monitors the creation of secure nodes in a polyglot application and marks a node that uses a value from a secure node (i.e., secure value) as secure itself.
Assumptions.Our preliminary implementation of SecV assumes a procedural program structure where the application to be partitioned is organized as a group of  functions:  1 ,  2 , . ..,   .Most programming languages support this paradigm.The goal of SecV is thus to determine the set of program functions to be put inside the enclave, and those to be kept out of the enclave, and partition the program into two parts based on this information.We assume that the inputs used in dynamic analysis are sufficiently exhaustive to cover all legitimate production configurations; this mitigates leakage of sensitive data at runtime due to incomplete code coverage during analysis.

Taint propagation.
In the context of our work, taint propagation defines how secure values flow from the secure nodes into subsequent parts of the program.PolyTaint performs taint propagation via explicit information flow [11].That is, an untainted variable y becomes tainted if an already tainted variable x is directly involved in the computation of y's value.In other words, there is a direct data-flow dependency between x and y.For example, the statement y = x + 2 marks y as tainted if x is tainted, as shown in Fig. 5. Poly-Taint extends this idea to functions by marking them as tainted if tainted variables are manipulated in their bodies.PolyTaint implements taint propagation via AST instrumentation.This is done by wrapping variable reads/writes, function calls (see §4.3), etc. and testing for nodes that access secure values.For example, in our illustrative example (Listing 4), the variable assignments at lines 2 and 3 will be wrapped and evaluated by PolyTaint to obtain the corresponding variables (i.e., m and c) that receive secure values (foreign objects in Truffle terminology) from secure nodes.We refer to such variables as taint sources.
Program function classification.The primary goal of taint propagation in PolyTaint is to determine which program functions access secure values (directly or indirectly from the taint sources) at run time, and which functions do not.As such, PolyTaint classifies functions into three categories: trusted, untrusted and neutral.
Trusted functions.These are functions that manipulate secure variables explicitly within their bodies i.e., instantiate secure variables (i.e., taint sources) or modify the values of secure variables.We define a secure variable as a program variable which explicitly receives its value from a SecV node via the polyglot API (e.g., m and c in Listing 4) or a program variable which gets tainted following PolyTaint's taint propagation rules.As such, function trainModel in Listing 2 will be tagged as a trusted function by virtue of the variable write: Y_pred = m * X + c which uses secure variables m and c inside trainModel's body.
Trusted functions are included fully inside the trusted/enclave part of the partitioned application.These functions have proxy functions in the untrusted part.We define the proxy function of a program function f as a function which has the same signature as f but whose body is stripped and replaced with an enclave transition (i.e., ecall or ocall) which transfers execution control to f.Following this logic, the proxies of trusted functions perform ecall transitions.The primary rationale for this design choice is security: that is, secure variables that contain sensitive information cannot be accessed in the untrusted side.
Neutral functions.These are functions which access secure variables only through their input parameters and do not explicitly access already tainted (i.e., not considering the tainted inputs) secure variables within their bodies.In other words, a neutral function does not access a tainted value when none of its arguments is tainted, and may access tainted values if at least one of its arguments is tainted.In Listing 2 for example, arraySum is considered a neutral function by virtue of the tainted inputs Y -Y_pred and X * (Y -Y_pred).Moreover, untrusted code could also perform array sums without sensitive inputs.
Neutral functions are included fully in both the trusted and untrusted partitions; there are no proxy functions involved.The rationale behind this design choice is twofold: the approach allows for good security and performance.That is, enclave code can access the functionality of the neutral function without leaking sensitive information, via input parameters, to the outside (security), and the code out of the enclave does not need to perform an expensive ecall transition to access the functionality of the neutral function (performance).Utility functions such as those that calculate generic sums, products, etc. are examples of neutral functions.
Untrusted functions.These are functions which do not have any access to secure variables during the lifetime of the program.Untrusted functions are included fully only in the untrusted partition.However, they have proxies in the trusted side which perform ocall transitions to access the functionality in the untrusted side of the partitioned application.The rationale for this design choice is TCB size reduction, and thus security, i.e., code which is not sensitive need not be in the enclave following the principle of least privilege [51].

AST instrumentation
To identify the trusted ( ), neutral ( ) and untrusted ( ) functions, PolyTaint uses a dynamic analysis technique.It executes the program once during the development phase.During this execution, it identifies the secure values, and then deduces the state of the functions, i.e., trusted, untrusted, and neutral.During the in-vitro execution, PolyTaint performs taint propagation by leveraging the Truffle instrumentation framework, which makes it possible to intercept the execution of AST nodes.To instrument the Truffle AST, PolyTaint intercepts the executions of these node types: variable read/write, object field read/write, array element read-/write and function call nodes (see §2.2).PolyTaint provides an event node class for each of the node types to be wrapped, as well as a base event node class PolyTaintNode which all the event node classes inherit from.PolyTaintNode implements common functionality which is used by the event node classes.
PolyTaint maintains a map data structure called the taint map which tracks program symbols (e.g., variables, functions, etc.) that access secure values.A unique string identifier is associated to each program symbol such as e.g., a variable or function.The taint map associates string identifiers to taint labels.We have 3 taint labels: 1 for tainted/trusted nodes, 2 for neutral nodes, and 0 for untrusted nodes (i.e., representing untrusted functions).During instrumentation, PolyTaint leverages the onReturnValue callback of event nodes to test for tainted program symbols in a node's AST.This is done by recursively traversing the node's child nodes and checking if the child nodes are associated with tainted symbols.As illustrated in Algorithm 1, PolyTaint considers that a child node is tainted if (i) the child node corresponds to a call to the polyglot API with the SecV language identifier (i.e., "secV") or (ii) the child node is a value that was tainted before.In the remainder of this section, we outline how PolyTaint performs taint tracking for the different intercepted nodes.
The Truffle instrumentation API provides a generic standard tag operation StandardTags.WriteVariableTag [41] to identify nodes corresponding to variable write statements in all Truffle languages.At run time, PolyTaint wraps every variable write node and creates a corresponding variable write event node.The Truffle API makes it possible to obtain a node object descriptor [37] which provides the unique name of the program variable being written to, as well as the corresponding AST node.In the onReturnValue callback of the variable write event node, PolyTaint leverages node object descriptors to obtain the exact names of the target program variables being written to.For example, for the 3 aforementioned variable write statements, the variable names are respectively m, c and Y_pred.
PolyTaint uses the variable names to construct the unique String identifiers for these variables.Still in the onReturnValue callback, PolyTaint checks the taint map for the presence of m, c and Y_pred.If they are present, the onReturnValue callback returns.Otherwise, PolyTaint leverages the Truffle API to obtain the right-hand side (rhs) expression node associated with the variable write statements.Algorithm 1 traverses the child nodes of the rhs expressions to check for the presence of any tainted nodes.
As explained previously, the presence of child nodes corresponding to the polyglot.evalcall as well as the "secV" string literal as one of its input arguments tests positive for a taint source.This will mark variables m and c as tainted in the taint map, i.e., taint labels of 1 for their identifiers.Similarly, Y_pred will be marked as tainted due to the presence of tainted variables m and c in the rhs expression involved in the assignment of Y_pred.If a tainted variable is written in a function, this function is also tagged as tainted in the taint map.For example, the variable write node corresponding to the statement Y_pred = m * X + c is tainted as a result of the presence of tainted variables m and c in the statement, therefore the enclosing function  is marked as tainted in the taint map.PolyTaint calls getName on the root node object [45] to obtain the unique name of the enclosing function/method for any instrumented node.
Variable read: This is a statement that reads the value of a program variable.Variable read operations could occur in an if/else statement, a for loop, a function call via parameters, etc.The Truffle instrumentation API provides a generic standard tag StandardTags.ReadVariableTag [41] to identify nodes that correspond to variable read statements in all Truffle languages.
At run time, PolyTaint wraps every variable read node and creates a corresponding variable read event node.The event node's onReturnValue callback is used to obtain the unique name of the variable being read from the node object descriptor, and the taint map is checked for the presence of the variable's identifier.If a tainted variable is read in a function (e.g., while(i < tainted_variable)), this function is also tagged as tainted in the taint map.If a tainted function argument is read (e.g., reading the value of Y_pred in arraySum(Y -Y_pred) on line 24), the function node is tracked as a neutral function in the taint map (i.e., taint label = 2) if it has not been tagged as tainted.In other words, a taint label of 1 for tainted/secure function is preferred for the function node over a taint value of 2 for neutral.As such, arraySum is tagged as a neutral function in the taint map .
Object field write: This operation assigns a value to an object's field or property.At the time of this writing, the Truffle API does not provide standard tags to identify generic object field write nodes but Truffle languages typically provide language-specific tags to identify such nodes.For example, Truffle-JS provides the WritePropertyTag [36] to identify object property writes.In some languages like JS, the global scope is an object and global variables are properties of this object.As such, global variable writes in JS are instrumented via the WritePropertyTag.Object field writes are instrumented with object field write event nodes in a similar fashion to variable writes.
Object field read: This operation reads an object's field or property.Similarly to object field writes, since there are no generic standard tags yet in the Truffle API to identify object field read nodes, Truffle languages typically provide language-specific tags like ReadPropertyTag [36] in Truffle-JS to identify an object field read node.Object field reads are instrumented with object field read event nodes in a similar fashion to variable reads.
Function call: This is an expression that passes control (and possible arguments) to a function or method in a program.The Truffle instrumentation API provides a generic standard tag StandardTags.CallTag [41] to identify language nodes that correspond to guest language functions and methods in all Truffle languages.At run time, PolyTaint wraps every function call node and creates a corresponding call event node.The call event node's onReturnValue callback is used to test first for tainted arguments.
Truffle languages usually provide methods to obtain argument nodes during instrumentation, e.g., getArgumentNodes provided by Truffle-JS (JSFunctionCallNode [35]) and Truffle-Python (PythonCallNode [40]).If an argument node for a function corresponds to a tainted symbol, e.g., a tainted variable, this function is tagged as neutral in the taint map if it has not been tagged as tainted.For example, arraySum is tagged as neutral by virtue of the presence of the tainted variable Y_pred in the argument expression.
PolyTaint maintains a list (seen list) that comprises all functions/methods that have been visited during execution at run time.Every function/method in the list is identified as a PolyTaintFunction object, which is an instance of PolyTaintFunction class.This class defines attributes representing the actual function node (i.e., Node object), a list representing the different argument types passed to the function (e.g., int for trainModel, Object for arraySum, etc.), a String representing the return type (e.g., double for arraySum, void for trainModel, etc.) of the function, and an integer value representing the function's taint label.The onReturnValue callback of every event node contains a VirtualFrame [34] parameter comprising the actual argument values passed to the instrumented node, e.g., a function call node, and an Object parameter representing the actual result received after the node is executed.PolyTaint leverages these two parameters to obtain the corresponding argument types passed (if they exist) to every function call node and return types, respectively.The input and return types are used later on during program partitioning ( §4.4).
At the end of program instrumentation with PolyTaint, all application functions which have been executed at run time (in the seen list), but have not been tagged as trusted/tainted or neutral in the taint map are considered untrusted functions.As such, for our illustrative example, after instrumentation, we should have set  : trainModel, set  : arraySum, and set  : readXData and readYData.This information is then passed to the program partitioner which builds two programs representing the trusted and untrusted partitions of the instrumented program.

Program partitioning
The aim of the partitioning stage is to separate the original program into two parts: a trusted part which executes inside the enclave and comprises functions  ∪  , and an untrusted part which executes outside the enclave and comprises  ∪ .By removing the  functions from the enclave, we reduce the size of the TCB.Furthermore, eliminating  functions also eliminates any functions of the language runtime's system libraries invoked by  , which are often very large (as stated in the introduction).
In order to interpret the ASTs of the  and  functions inside the enclave at runtime, we have to also embed the corresponding Truffle language interpreter in the enclave.However, Truffle is written in Java, which means that we need a Java runtime inside the enclave to execute the Truffle interpreter.Since embedding a full JVM with its system library inside the enclave would increase the size and the attack surface of the TCB to an unacceptable degree, we choose to base the design of our partitioning tool on native images ( §2.2).GraalVM's Native Image technology makes it possible to include only reachable program elements (i.e., methods, classes, and objects) into the resulting native image, making it suitable for restricted environments like Intel SGX enclaves.Any unused classes in the Truffle interpreters or GraalVM's runtime components (e.g., garbage collector) are pruned out of the native images.The remainder of this section describes our partitioning approach.4.4.1 Generated functions.GraalVM AoT only supports compiling Java applications to create native images (i.e., no JS, Python, etc.), therefore, we need to find a way to run the partitioned guest application code (e.g., JS, Python, etc.) via native images.To achieve this, the partitioner leverages Truffle's polyglot API to embed the guest code inside two Java programs: Trusted.java for the trusted partition and Untrusted.javafor the untrusted partition.This is done by creating static Java methods that correspond to each of the methods seen during taint analysis (i.e.,  ,  , and  ).These static methods simply execute the corresponding ASTs of the guest functions they represent.
For instance, Listing 5 illustrates how the AST of a JS function multi can be executed from within a Java application, and how a Java method hello can be executed from within a JS program.Using the same idea outlined in Listing 5, Trusted.javatherefore comprises static Java methods for trainModel, arraySum, readXData and readYData.The static Java methods for trainModel and arraySum execute the corresponding AST code, i.e., the actual guest source code for those functions, while the static Java methods for readXData and readYData (proxy methods) perform ocall transitions to execute the real methods in the untrusted partition (see below).The actual source code of a function is obtained via the getSourceSection method [45] of the Truffle Node class.This is illustrated by Listing 6.  getMember("static").getMember("arraySum"); 6 ctx.eval("python", "//trainModel code").Listing 6. Trusted.java after partitioning Similarly, Untrusted.javacomprises static Java methods for trainModel, arraySum, readXData and readYData.The static Java methods for readXData, readYData and arraySum execute the corresponding AST code for those functions while the static Java method for trainModel performs an ecall transition to execute the method in the trusted partition.Listing 7 exemplifies this.With our program partitioned, we must allow communication between the two partitions.That is, how does a Java method in Trusted.java(i.e., the trusted partition) invoke another Java method in Untrusted.java(i.e., the untrusted partition)?
To achieve this, we leverage GraalVM C entry points [28,66].These are special Java methods in a native image program which are callable from a regular C/C++ program or another native image.As such, C entry point methods can act as relays between the trusted and untrusted partitions.That is, a proxy function in partition- can invoke the corresponding real static method (m) in the opposite partition- if there is a C entry point method for m in partition-.
For the trusted partition (i.e., Trusted.java), the program partitioner generates C entry point methods corresponding to all the trusted functions.These entry point methods are the target of ecalls from the untrusted code calling a trusted function in the final SGX application.Similarly, for the untrusted partition (i.e., Untrusted.java),C entry point methods are generated for the untrusted functions.These entry point methods are the target of ocalls from the trusted partition calling an untrusted function.

Serialisation.
Because objects cannot be sent across an enclave boundary [12,66], any static methods that have non-primitive input or return types (e.g., arrays, strings, etc.) to be transferred across the enclave serialise the input or return values into a byte array.The byte array is then marshalled across the enclave boundary in the corresponding ecall or ocall, and deserialised on the opposite side into a Java Object.Serialisation and deserialisation are done using Java's ObjectOutputStream and ObjectInputStream [38] classes, respectively.

Execution of a function.
Thanks to native image, the trusted and the untrusted partitions execute the (binary) compiled version of the corresponding Truffle guest language interpreter at runtime.At runtime, the guest language interpreter parses the string that represents the guest code, builds the AST, and executes it.When the AST of a function f contains a call to another function g, the symbol table of the interpreter may not contain the symbol g.In that case, the Truffle API leverages Java reflection to find a static Java method that corresponds to the symbol g and executes it.As an example, in Listing 5, Java reflection is used to expose the Java function javaHello (or the JS function multi) to Truffle interpreter when executing jsHello.

Building native images and the SGX program
The aim of ImageGenerator is to AoT compile Trusted.java and Untrusted.javainto relocatable object files (trusted.oand untrusted.orespectively).When building a native image, GraalVM makes it possible to provide which guest language implementations (i.e., the Truffle interpreters) should be made available in the resulting native image.In the case of SecV, all guest languages involved in the partitioned program are provided as inputs to the native image generator.This tool performs static analysis (see §2.2) [60] to determine the reachable Java elements, i.e., the classes, methods, and objects from the Java program that is being compiled and the Truffle language implementations that are required to run the corresponding Java programs.These reachable components are then AoT compiled into native images: trusted.oand untrusted.o.
It is worth noting that the native image builder does not AoT compile guest language code (e.g., JS, Python, etc.) that is embedded in the Java programs.Indeed, the guest language code will be interpreted or JIT compiled by GraalVM at run time.The ecall and ocall definitions are compiled into object files and linked with trusted.o and untrusted.o,as well as SGX C/C++ library code, to create the final Intel SGX application.A small shim library is included inside the enclave which seamlessly relays unsupported system calls (e.g., read, write, etc.) to the untrusted runtime via ocalls, which perform the real system calls and return the results back to the enclave.

Evaluation
The experimental evaluation of SecV seeks to answer the following research questions: RQ1: What is the implementation effort for a multilanguage tool (i.e., SecV) as compared to languagespecific tools?( §5.

Experimental setup
Our evaluation is conducted on a server equipped with a quad-core Intel Xeon E3-1270 CPU clocked at 3.

Implementation efforts (RQ1)
. What is the implementation effort for a multi-language tool as compared to language-specific tools?Table 1 reports the total lines of code (LoC) of various enclave code partitioning tools, as well as the LoC for the underlying frameworks used by these tools.We observe that SecV's code base is ≈ 1.41× and ≈ 1.02× smaller as compared to the full code bases of Civet and Glamdring, respectively.We argue that the SecV approach is better in terms of code simplicity (leverages a single self-contained framework) and efficiency (achieves the desired functionality with fewer lines of code).Furthermore, SecV provides a single extensible multi-language system in relatively fewer LoC, contrary to the other tools which are language specific.

Overhead of SecV nodes (RQ2). What is the cost of injecting SecV nodes into a program's AST?
The goal of this experiment is to measure the overhead caused by the use of secure nodes in a polyglot program.In this regard, we generate synthetic programs in JS and Python that comprise functions with varying numbers of variables that receive secure values (e.g., secure int, double values) via the polyglot API.We compare the run time of the functions with a similar setup where the functions define regular values, i.e., no injection of SecV nodes via the polyglot API.Fig. 6 shows the results obtained for different value types used.
Observation.For JS programs, using sInt variables is ≈ 1.05× slower on average as compared to using regular integer variables without the polyglot API.Moreover, using sDouble variables is ≈ 1.04× slower on average as compared to using regular double type variables without the polyglot API.For Python programs, using sInt variables is ≈ 1.14× slower on average as compared to using regular int type variables without the polyglot API.Furthermore, using sDouble variables in Python is ≈ 1.11× slower on average as compared to using regular double type variables.
Discussion.For both JS and Python, using SecV nodes involves calls to the Truffle polyglot API, as opposed to defining regular program variables for which there is no intermediate API involved.This explains the additional cost when introducing secure values in the program.In practice, programs will typically define few secure variables leading to a small overhead relative to the total runtime cost of the full program Take-away 1 : The cost of injecting secure nodes into a program's AST is small.

Overhead of taint tracking (RQ3). What is the cost of taint tracking with PolyTaint?
In this experiment, we aim to measure the performance of taint tracking with PolyTaint.To that end, we generate synthetic polyglot programs in JS and Python.The synthetic programs consist of a varying number of functions that all perform a bubble sort on a SecV array of 1,000 randomly generated numbers.We chose a bubble sort algorithm here because it performs many variable read/write operations, which trigger the creation of taint tracking instrumentation nodes in PolyTaint.The programs run on GraalVM using their corresponding Truffle interpreters, with or without PolyTaint taint tracking enabled.Fig. 7 shows the comparative performance of both setups.Observation.The experimental results show that the JS program running with PolyTaint taint tracking enabled is ≈ 1.56× slower when compared to the JS program running without taint tracking enabled.Similarly, for the Python program running with PolyTaint taint tracking enabled, it is ≈ 1.31× slower on average when compared to the variant without taint tracking enabled.
Discussion.For both JS and Python programs, PolyTaint introduces more nodes (i.e., wrappers and execution event nodes) to their ASTs during instrumentation.As seen in §4.3, those nodes perform operations to track tainted variables, which explains the performance decrease (i.e., longer runtime) when taint tracking is enabled, as compared to the variants where the programs run without any taint tracking.
Take-away 2 : Taint tracking via AST instrumentation introduces overhead at run time.This overhead is due to the additional operations performed by instrumentation nodes introduced in the program's AST.

Effect of partitioning on performance (RQ4). How does partitioning affect application performance?
To study the performance impact of partitioning on application performance with SecV, we first use a synthetic polyglot benchmark written in JS.We then partition two realworld applications that implement the PageRank algorithm [2] and the linear regression ML algorithm [64].
Synthetic benchmark.Our synthetic benchmark is a JS program that comprises 100 functions that each perform a bubble sort on a local array variable of size 100.We leverage secure values (secure array or regular array) in a varying percentage of the functions, analyse the polyglot programs with PolyTaint and partition these programs for enclaves.The purpose of our synthetic benchmark is to highlight the effect of partitioning on a generic program.Fig. 8 shows the results.
We observe that as more functions are partitioned out of the enclave, the overall performance of the program improves.This is explained firstly by the reduced number of libc-related ocalls performed by the embedded GraalVM runtime components (language interpreters, GC, etc.), and secondly by the fact that we have less overhead due to enclave data encryption-decryption operations to and from the EPC by the MEE [66].
Take-away 3 : Enclave performance improves as more computations are delegated to the untrusted side.This is due to less expensive enclave context switches, e.g., via ocalls, as well as less expensive enclave-related cryptographic operations.
We have observed the effect of partitioning on a generic synthetic program.We now leverage SecV to partition realworld applications.For these applications, we evaluate 3 modes for running the programs: entirely inside the enclave (no-part), as a normal polyglot native image without SGX (native), and as a partitioned native image using SecV (part).The partitioning scheme adopted for each application is not necessarily realistic, but aims to highlight and explain the performance improvement obtained after partitioning.
PageRank.PageRank [2] is a popular algorithm that is used in graph processing frameworks [22] to weigh the relative importance of nodes (i.e., node ranks) in a directed graph.One could envision a scenario where the node ranks are to be secured within an enclave.We leverage SecV to partition a PageRank program with the node rank data structure (a secure array) tagged as a secure variable.The PageRank program comprises a function to generate a directed graph and obtain the corresponding adjacency list (graph pre-processing), and other functions which use the adjacency list to perform the PageRank algorithm.The graphs are generated using the RMAT algorithm [8], and all graphs with  vertices have 2 •  edges.Fig. 9 (a) shows the results obtained when running partitioned and unpartitioned versions of PageRank with varying graph sizes.
Observation.The partitioned version of the PageRank program is ≈ 1.12× faster on average (i.e., about 10.8% performance improvement) as compared to the unpartitioned version.Furthermore, we have on average ≈ 2.53× fewer ocalls in the partitioned program as compared to the unpartitioned one.The native version of the program (i.e., no SGX) is ≈ 6.85× and 6.17× faster on average as compared to the unpartitioned and partitioned versions, respectively.Discussion.After partitioning the program, the functions responsible for graph generation and pre-processing are moved to the untrusted partition, while those that perform the PageRank algorithm to obtain node ranks remain in the trusted partition.At runtime, the pre-processed graph (in the form of an adjacency list) is serialised in the untrusted partition and marshalled into the enclave (see §4.4.3) via an ocall.It is then deserialised to recreate the adjacency list object which is used by the in-enclave PageRank algorithm to compute the final page ranks.The computations outside the enclave runtime do not incur expensive context switches via ocalls, or SGX-related cryptographic operations.That explains the performance improvement as well as the reduced number of ocalls in the partitioned version as compared to running the full program inside the enclave.This also explains why the native application is fastest: no SGX-related cryptographic operations, no enclave context switches, and less memory restrictions (the enclave has only about 93 of memory).However, it is the least secure of all.
Linear regression.Securing ML models with TEEs is common [25,65].We aim to partition a linear regression program for an enclave runtime.Similar to PageRank, a linear regression program typically consists of functions to read and pre-process (i.e., normalisation and standardisation) the input datasets to be used, and other functions which use the dataset to train the ML model.We leverage SecV to specify secure variables (i.e., m and c in Listing 2), analyse and partition the linear regression program.Observation.The partitioned version of the linear regression program is ≈ 1.17× faster on average (i.e., about 14.5% performance improvement) as compared to the unpartitioned version.Moreover, we have on average ≈ 1.36× fewer ocalls in the partitioned program when compared to the unpartitioned one.The native version of the program is ≈ 4.73× and 4.04× faster on average when compared to the unpartitioned and partitioned versions, respectively.
Discussion.After program partitioning, the functions for data generation and pre-processing (which do not access m and c) are partitioned out of the enclave, while the trusted functions that train the model (and hence access the secure values m and c) are part of the enclave partition.At runtime, dataset pre-processing is done in the untrusted partition; this entails standardising both the X and Y datasets.The standardised datasets (arrays) are then serialised and marshalled into the enclave runtime via ocalls, deserialised inside the enclave, and used to train the linear regression model.By offloading the data generation and pre-processing phases to the untrusted runtime, the enclave is relieved of expensive computations, which leads to an overall improvement in application performance when compared to running the full program inside the enclave.Similar to PageRank, partitioning introduces overhead for data serialisation and deserialisation, but this overhead is offset by the performance gain from performing some computations outside (i.e., no expensive ocalls), rather than inside the enclave.
Take-away 4 : The TCB size of programs can be decreased without degrading performance.As a matter of fact, the peformance analysis of both PageRank and linear regression shows that decreasing the TCB size via partitioning can lead to better performance: security and performance are improved at the same time.

Limitations of our design
Limited code coverage.The present design of SecV is based on a purely dynamic program analysis approach, where programs are run once during the development phase to deduce the set of trusted ( ), neutral ( ), and untrusted ( ) functions, which are then used to create the trusted and untrusted partitions.However, this design provides limited code coverage, potentially leaving security-sensitive code out of the enclave; limited code coverage is a fundamental limitation of most dynamic analysis tools [20].To address this problem, static analysis can be applied, but it has major drawbacks.Firstly, it performs an over-approximation of the code to be tainted (due to polymorphism), which leads to a larger TCB.Secondly, Truffle ASTs exist only at runtime, making static analysis infeasible.Ultimately, a mixture of both static and dynamic taint analysis techniques could be a reasonable compromise.We leave this as future work.
Application termination.During analysis, different program runs (with varying inputs) could result in different execution paths being taken at runtime.This could in turn result in different (and possibly conflicting) sets for  ,  , and  .From a security perspective, such a scenario may lead to secure values being leaked to the untrusted partition at runtime.In our prototype implementation, we avoid this problem by checking if a value is secure just before serializing it in in-enclave proxies.Thanks to this check, in the worst case, the application will terminate with an error, but will not let a secure value escape the enclave.Running multiple tests or using symbolic execution could decrease the probability of terminating the program.Similarly, we leave this implementation as future work.

Leveraging SecV for other use cases
SecV's design can be extended to encompass more generic compartmentalization of applications to not only enhance security, but also promote modularity and maintainability.In terms of security, the current design can be extended to support Intel's new TEE technology: trust domain extensions (TDX) [10].This entails running the trusted and untrusted partitions in separate VMs, or "trust domains".
In a context unrelated to security, SecV can also be extended to partition applications into smaller loosely coupled microservices that run in dedicated VMs, hence promoting flexibility and maintainability.

Related work
We classify related work into 3 categories: (i) tools that run full, unmodified applications inside enclaves, (ii) taint tracking tools, and (iii) code partitioning tools.
Running full applications inside enclaves.Various tools like SCONE [4], Graphene-SGX [56], TWINE [24], and SGX-LKL [49] propose solutions to run entire legacy applications inside enclaves.Their approach severely increases the size of the TCB, at the risk of added security vulnerabilities.SecV provides a generic solution to partition applications for enclaves while trying to keep the TCB as small as possible.
Taint tracking tools.Several tools exist for taint tracking.Phosphor [5] is a dynamic taint tracking tool for Java programs, while Dytan [11] performs taint tracking in x86 binaries.TAJ [55] provides efficient static taint analysis for Java applications.[54] provide a tool to extract taint specifications for JavaScript libraries.However, these tools are language-specific (i.e., only for Java, only for x86 assembly code, JS, etc.) and cannot be readily leveraged to analyse code in different languages.SecV bridges this gap with a languageagnostic taint tracking approach.TruffleTaint [21] leverages the Truffle framework to provide a language-agnostic platform to build dynamic taint analysis applications.While we leverage very similar instrumentation techniques, we provide a novel way to specify sensitive values through the introduction of secure AST nodes, and leverage these to partition code for the enclave runtime in a language-agnostic fashion.
Language specific partitioning tools.Several tools have been proposed to partition code written in specific languages for enclaves.Glamdring [23] provides a technique to automatically partition C applications, while Montsalvat [66], Civet [57], and Uranus [18] propose solutions to partition Java applications for Intel SGX enclaves.Among those partitioning systems, none provide a language-independent way to partition enclave programs.SecV solves this problem by introducing a multi-language technique to partition programs for enclaves.

Conclusion and Future Work
In this paper, we presented SecV, a multi-language approach to analyse and partition programs for Intel SGX enclaves.SecV provides generic secure nodes that encapsulate sensitive program data, and that can be injected into the ASTs of programs written in a wide range of programming languages.SecV provides a dynamic taint tracking tool, PolyTaint, which tracks the flow of sensitive data from secure nodes in a program at run time, and partitions the program into trusted and untrusted parts which are executed in and out of the secure enclave, respectively.Our evaluation of SecV shows it can reduce the program's TCB size without any performance degradation.
Future work.We plan to extend SecV along two directions: More data types.This entails extending SecV's secure node generator to cover more data types and data structures, e.g., maps, lists, etc., as described in §4.1.
Support for more languages.PolyTaint can be extended to fully cover more Truffle languages like TruffleRuby, FastR, etc. Regarding LLVM-based languages like C/C++, the Truffle framework provides an LLVM interpreter, Sulong [42].This interpreter can also be extended to run programs in Go, by leveraging tools like GoLLVM [1].Alternatively, Truffle's We-bAssembly (Wasm) implementation can be leveraged to provide a common compilation target for supported languages: Rust, Go, Kotlin, etc. Adding support for these languages in SecV mainly involves incorporating wrapper nodes to handle language-specific semantic constructs and AST nodes in the new language.There is in-depth documentation online to facilitate these extensions.

Figure 1 .
Figure 1.Integer addition node executed in the AST to produce the sum of the values of left and right child.

Figure 4 .
Figure 4.The Truffle framework can be leveraged to provide an AST generator that produces secure nodes which can be injected in any Truffle AST via the polyglot API.

Figure 5 .
Figure 5. Taint propagation due to explicit data-flow dependencies with secure values.

Figure 6 .
Figure 6.Cost of using secure values in JS and Python programs.

Figure 7 .
Figure 7. Cost of taint tracking with PolyTaint for JS and Python.

Figure 8 .
Figure 8.Effect of program partitioning on a generic synthetic program.

Figure 9 .
Figure 9. Results for partitioning the PageRank and Linear Regression programs.

Fig. 9 (
b) shows the results when the linear regression model is trained for 100 training iterations for partitioned and unpartitioned versions of the program.
Algorithm 1 -Pseudo-code to check for tainted nodes in AST 1: procedure TraverseAST(node)

Table 1 .
Total lines of code for existing partitioning tools and their underlying frameworks.
80 GHz, and 64 GB of DRAM.The processor has 32 KB L1 instruction and data caches, a 256 KB L2 cache, and a 8 MB L3 cache.The server runs Ubuntu 18.04.1 LTS 64 bit and Linux 4.15.0-142.We run the Intel SGX platform software with version 2.16 of the SDK and and version 2.14 of the driver.The EPC size on this server is 128 MB, of which 93.5 MB is usable by enclaves.The enclaves have maximum heap sizes of 8 GB and stack sizes of 8 MB.All native images are built with a maximum heap size of 4 GB.We use GraalVM version 22.1.0.All reported measurements are averaged over 5 runs.