{'fraction_non_alphanumeric': 0.029249427402095147, 'fraction_numerical': 0.009537040513648481, 'mean_word_length': 5.064207650273224, 'pattern_counts': {'":': 0, '<': 0, '<?xml version=': 0, '>': 0, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
AnyCall: Fast and Flexible System-Call Aggregation

Operating systems rely on system calls to allow the controlled communication of isolated processes with the kernel and other processes. Every system call includes a processor mode switch from the unprivileged user mode to the privileged kernel mode. Although processor mode switches are the essential isolation mechanism to guarantee the system's integrity, they induce direct and indirect performance costs as they invalidate parts of the processor state. In recent years, high-performance networks and storage hardware has made the user/kernel transition overhead the bottleneck for IO-heavy applications. To make matters worse, security vulnerabilities in modern processors (e.g., Meltdown) have prompted kernel mitigations that further increase the transition overhead. To decouple system calls from user/kernel transitions we propose AnyCall, which uses an in-kernel compiler to execute safety-checked user bytecode in kernel mode. This allows for very fast system calls interleaved with error checking and processing logic using only a single user/kernel transition. We have implemented AnyCall based on the Linux kernel's eBPF subsystem. Our evaluation demonstrates that system call bursts are up to 55 times faster using AnyCall and that real-world applications can be sped up by 24% even if only a minimal part of their code is run by AnyCall.


Introduction
General-purpose computing systems rely on memory isolation as the most fundamental security and safety mechanism that enables protection from malicious activities, maintains privacy, and confines faulty programs [1,20]. The necessary counterpart to memory isolation is a well-defined communication interface that provides kernel-level functionality safely to user programs, typically implemented by system calls. The costs of system calls, in particular their executiontime overhead, have been a well-known performance-critical system property for decades [18]. Besides the traditional direct costs of isolation, each transition between user and kernel space inflicts indirect costs as the processor state must be (partially) invalidated [39]. The importance of such indirect costs is expected to grow since caches and hardware buffers are becoming increasingly performance-critical. With the discovery of Meltdown [28] and Spectre [26] the costs of system calls have increased even further, often causing a significant overhead on execution time [35,36] and energy demand [22]. The required flushes of processor-internal buffers enforce isolation, but cause significant overheads, particularly for applications that execute system calls at a high rate. An example for system-call-intensive applications is the Unix find tool that traverses directories and filters files by configurable criteria. Therefore, numerous system calls are executed to read the file-system tree, but the amount of computation in user space, in-between system calls, is often negligible. Experiments on a 1.8 GHz desktop computer show that 70 000 to 726 000 system calls are performed per second when unpacking archives, listing files, and estimating disk usage using the GNU coreutils 1 .
Considering that applications with high system-call rate suffer most from Meltdown-type mitigations, we propose AnyCall, a system where only a single user/kernel transition is required for an arbitrary number of system calls. To reduce the number of transitions between user and kernel space, the control flow migrates to kernel space, calls arbitrary system calls interleaved with application-specific logic, and eventually returns to user space. Isolation of the supplied application logic is enforced by an in-kernel bytecode executor (i.e., eBPF) and static bytecode analysis.
The contributions of this paper are three-fold: • We present an approach to decouple the number of user/kernel transitions from the number of system calls, while maintaining isolation using an in-kernel bytecode executor.
• We extend Linux to support system calls from within eBPF programs. For this, AnyCall enables verifiably safe access to user memory inside eBPF programs to construct system call arguments and access results. • We evaluate AnyCall by comparing the overheads of eBPF-based to traditional system calls.
This paper is structured as follows. Section 2 presents related work and background knowledge. Section 3 discusses the design of AnyCall followed by Section 4 presenting our efficient eBPF-based implementation for the Linux kernel. In Section 5 we evaluate AnyCall using real-world applications and microbenchmarks. Future work is discussed in Section 6. Finally, Section 7 concludes this paper.

Background and Related Work
This section discusses recent hardware and software developments related to the overheads of system calls. We also review existing techniques that avoid the overhead of user/ kernel transitions and give a brief introduction to eBPF.

High-Performance IO
Traditional hard drives with high access latencies are now superseded by low-latency media such as NVME-SSDs and Optane storage. This device latency reduction makes the user/ kernel transition delay more significant for the overall performance of a computing system [19,40]. Similarly, networking hardware is bottlenecked by the software stack [24,38]. Therefore, network-heavy applications directly access network hardware, bypassing the OS Kernel [7,33].

Meltdown and Spectre
Recently, the Meltdown [28] and Spectre [26] hardware vulnerabilities have made it necessary to develop hardware, firmware, and software mitigations. As these vulnerabilities can circumvent the memory isolation between processes, software mitigations at operating system (OS) level have been developed. Although these mitigations are effective in fully or at least partly preventing the exploitation of these vulnerabilities, they come with potentially significant execution time and energy demand overheads [2,22,35].
Especially Linux' mitigation against Meltdown and attacks bypassing KASLR [23], that is Kernel Page Table Isolation (KPTI), can introduce significant overheads for user/ kernel transitions. The most important reason for this are additional TLB flushes before switching to user space. The mitigations' overhead for different variants of Spectre attacks, apply more selectively depending on the workload, but can nevertheless be significant [22].

Avoiding User/Kernel Transitions
In special cases, asynchronous input/output (AIO) APIs, multicall interfaces, or a virtual Dynamically-linked Shared Object (vDSO) can be used to avoid user/kernel transitions.
With asynchronous APIs, user space can submit many jobs to the kernel using a single user/kernel transition [5,8,11,39]. Asynchronous interfaces reduce the system-call overhead as the transition latency can be hidden if an in-kernel thread runs in parallel to the application (i.e., exploiting parallelism). However, the application-specific logic that issues system calls and processes their results runs in user space, requiring data to be communicated between kernel and user space frequently. Multicall interfaces, as implemented by the Xen hypervisor, also suffer from this limitation, constraining their practical use to batched page-table updates and networking hardware control [32]. Furthermore, using AIO efficiently requires a programming model many developers are not familiar with [4].
In Linux, some system calls only read a small amount of information. They can be implemented using vDSO [16], where the data is mapped into user space, making it directly readable without a processor mode switch. However, vDSO is limited to read-only data for security reasons.

Extended Berkeley Packet Filter (eBPF)
Initially, the Berkeley Packet Filter (BPF) was developed to filter network packets directly in the Linux kernel without having to pass the data to user space [29]. This could also be achieved by loading custom modules into the kernel, but using eBPF provides isolation and is more portable. The original BPF instruction set is very limited (e.g., no loops) and intentionally not Turing complete. Extended BPF (eBPF) is also not Turing complete (i.e., guaranteed termination), but has a redesigned instruction set optimized for C interoperability and compilation to native machine code. Also, recent versions allow for safely bounded loops. On most architectures, eBPF instructions directly map to machine instructions. eBPF allows applications to inject small code fragments as event handlers in the Linux kernel. It thus enables detailed but flexible configurability with relatively little overhead. The applications of eBPF include packet filtering [30], tracing [34], seccomp access-control policies [17], file system sandboxing [9], caching [21], and paravirtualization [3].
Registration of an eBPF program involves multiple steps. First, the kernel loads the eBPF program and analyzes it to guarantee memory safety and a bounded execution time. Second, the eBPF program is compiled from bytecode to native machine code and attached to an event (e.g., tracepoints). In the following, the already-compiled eBPF program is invoked whenever the corresponding event triggers.
As of July 2021, the use of eBPF does not support the execution of code from unprivileged sources for security reasons [10]. However, there exists a kernel capability which grants processes not running as root the ability to load and execute eBPF programs.

Design
This section presents the design of AnyCall which allows for fast and flexible system call aggregation using an inkernel bytecode executor. We discuss the usage, execution model, and present methods for making AnyCall available to unprivileged processes.

Usage
To keep the adoption effort small, the programming model of AnyCall must be straight-forward even when using our prototype tooling. To move userspace C code with high system call rates to AnyCall, a programmer (1) embeds the code into our framework, compiling it to eBPF bytecode, and (2) replaces the original code with a single system call to invoke the newly created AnyCall. The eBPF runtime of the Linux kernel allows for very expressive aggregation programs and is extended continuously while our libbpf-based framework provides transparent libc system call declarations. Therefore, modifying the code is only required rarely, for example, when system-call results are to be accessed by reference or when loops are unbounded. An example eBPF program with these issues resolved is displayed in Figure 1. Besides this, one only has to be wary of program size to not exceed the analyzer's limits on eBPF instructions and control flow complexity. However, these can be easily circumvented by splitting code into multiple AnyCalls.
Both detection and transformation of code suited for Any-Call can be performed by modern compilers, making Any-Call transparent to the programmer. We intend to provide further tooling support in future work.

Execution Model
We have considered two alternative execution models, a return-oriented and a call-oriented approach. AnyCall uses a call-oriented approach because it offers better safety and efficiently implements our approachable programming model.
Return-Oriented. In this execution model, eBPF programs are executed in response to system calls which are initiated asynchronously by the application. This model has been proposed for integration in the Linux io_uring subsystem [6,12]. With this model, the control flow is similar to return-oriented programming [37], an exploit technique where chunks are executed sequentially, and the return instruction at the end of an instruction sequence leads to the execution of the following sequence. For system-call aggregation, such sequences are either eBPF programs, or system calls. In this execution model, an eBPF program can call a helper to request a subsequent system call which, on termination, triggers another eBPF program. It thus create a chain of system calls, interleaved with eBPF programs.
Call-Oriented. In a call-oriented execution model, eBPF programs invoke system calls synchronously, using stubs  provided by the kernel and exposed to the eBPF bytecode. Equivalently to function calls, the original eBPF program receives the result upon system-call completion and resumes execution. This model is displayed by example in Figure 2.
Discussion. The return-oriented and call-oriented execution models differ in various aspects, in particular, with respect to progress, safety, and programmability.
Regarding progress, the return-oriented model verifies each individual eBPF program for completion, but proving completion of the whole call chain is practically infeasible. This enables infinite loops involving alternating system calls and eBPF programs. For safety, the system-call aggregation needs to manage its state, but protect it from other contexts. In the return-oriented model, the local state has to be preserved along a call chain, but protected from concurrent call chains. In consequence, this model demands for concurrency control. Furthermore, preserving the state correctly (including garbage collection at each end of the call chain) is non-trivial and cannot be safely ensured by the kernel. Regarding programmability, the call-oriented approach is straight-forward as it resembles the way applications have interacted with the kernel traditionally, which is consequently well-understood by many programmers. In comparison, the return-oriented approach scatters control flow over multiple chunks and embeds control flow decisions in helper calls. In summary, AnyCall implements the call-oriented execution model. In consequence, AnyCall guarantees completion of aggregated system calls, which limits flexibility, because the system-call aggregation as a whole is verified for completion. In consequence, only loops with provable bounds are allowed. As an example, file-system iteration is currently not possible, but future versions may support it via dedicated eBPF helper functions.

Access Privilege
To use AnyCall, an eBPF program which aggregates the desired system calls in an application-defined manner has to be loaded into the kernel. As mentioned in Section 2.4, this is currently only available to privileged or capability-granted processes. Trusted system services can therefore make immediate use of our implementation using the eBPF capability.  However, several use-cases for eBPF from unprivileged programs have been discussed [10,13], besides AnyCall. Our approach may motivate further hardening efforts of the eBPF subsystem to allow safe execution of untrusted user-supplied code in the kernel address space.

Implementation
This sections presents our eBPF-based implementation of AnyCall in Linux. We discuss program loading, system call invocation, methods for accessing user memory, and the programmer effort required for adoption of AnyCall.

Loading and Invocation
Our implementation adds a new anycall eBPF program type to the Linux kernel. Programs of the anycall type can be loaded using the standard bpf() system call and therefore also integrate with existing libraries that simplify eBPF bytecode handling (e.g., libbpf [15]). Usually, eBPF programs are attached to kernel events after loading and are then invoked asynchronously. To enable our synchronous execution model, we create a new system call which invokes the eBPF program referenced by a file descriptor. Program registration is not required. The system call returns once the eBPF program has finished executing.

Kernel Calls
Our approach exposes system call implementations to eBPF programs as helper functions, called kernel calls. A similar but severely limited approach was recently introduced by the kernel developers in the context of code signing [14]. In eBPF bytecode, helpers are identified by numbers replaced with the function address during compilation to machine code. Therefore, the call-time overhead is comparable to a subroutine call in native machine code. Each kernel call is invoked using a dedicated helper function receiving up to five arguments in eBPF registers, which directly map to machine registers on most architectures.

Access to User Memory
Many system calls receive or return values by reference, and the existing code-base of the Linux kernel expect that these references point to user space. The numerous widelyscattered checks that system call arguments do not reference kernel memory are required for security reasons, so disabling or bypassing them is therefore not an option. Hence, Any-Call requires a way to read and write user memory in order to construct system call arguments and process their results. The eBPF static analyzer already has the ability to track fixed-size dynamic memory allocations from helper functions. However, the available helpers allocate kernel memory which is unusable for system call arguments. Therefore, a new mechanism is required. We have considered two alternative solutions, one based on page-faults and the other based on page pinning. AnyCall implements the latter because it is more efficient when user memory is accessed repeatedly and requires fewer modifications to AnyCall code.
Page-Fault Handler. This solution is similar to the way most system calls access data in user space. Because user memory access is architecture-specific, helper functions are called for every memory access. On x86, present user memory is accessed directly but special operations are required on other architectures. If the access triggers a page-fault, the helper functions return an error.
This interface has the disadvantage that it requires an eBPF helper call for every access to user-space memory, we therefore did not pursue this approach. Future work may extend the eBPF-to-native compiler to inline these calls.
Page Pinning. To avoid a helper call for every user memory access we have implemented an alternative solution that only requires a map() and an unmap() helper call for each . Execution time for an AnyCall aggregating a variable number of getpid() system calls. The AnyCall performs 1 to 300 kernel calls, and is invoked 150 times. The variant using traditional system calls issues the equivalent number of system calls from user space. memory area. We pin the pages into memory, thereby preventing page-faults, and map them to kernel virtual addresses to guarantee direct, portable access to the memory from eBPF. The use of this interface and the checks performed by the eBPF static analyzer are illustrated by example in Figure 3.

Evaluation
We evaluate our implementation for the v5.11 Linux kernel [15] on an Intel Core i5-6260U processor with two cores (four hardware threads) running at 1.8 GHz. The system configuration is left to the default values, therefore the vulnerability mitigations Kernel Page Table Isolation (KPTI) and PTE Inversion are active. To make the evaluation reproducible, dynamic voltage and frequency scaling (DVFS) is disabled.
We evaluate AnyCall with KPTI for multiple reasons. First, we expect that there is still a significant number of processors vulnerable to Meltdown deployed, simply because of the large number of affected devices when the vulnerability was disclosed. Second, as the discovery of new Meltdowntype attacks and development of respective mitigations is an ongoing process [23,25,31], it is likely that the latency of user/kernel transitions increases further. This is in line with the general development of an increasing latency of core OS functionalities (e.g., system calls) [36].
Each experiment is executed 20 times, but the first 10 iterations are considered warm-up iterations and thus discarded. In the following we report the average from the last 10 iterations. Error bars are omitted in the plots as the variation is not visible at the displayed scale.

Microbenchmark
To analyze the degree to which AnyCall reduces direct user/ kernel transition overheads, we have measured the execution time of getpid() performed with AnyCall and in user space. The execution time of the AnyCall variant includes the time for loading the eBPF program into the kernel.
Our motivating experiment in the introduction has demonstrated that it is not uncommon for real-world applications to perform 70 000 to 726 000 system calls per second. Therefore, our first experiment, displayed in Figure 4, executes between 150 and 45 000 getpid() calls in user space or using Any-Call (AnyCalls with 1 to 300 kernel calls are invoked 150 times). As expected, the time required for static analysis and to-native compilation of the bytecode causes the user-space variant to be faster if few system calls are performed inside the program. However, if AnyCall performs the equivalent of 25 500 traditional system calls, it is faster than the user space variant. Both variants show a linear relation between the executed work and the execution time. The initial overhead of AnyCall is 22.34 ms to prepare and load the eBPF program into the kernel. Thereafter, the variant using traditional system calls requires 131.8 µs for 150 getpid() calls while AnyCall uses 2.0 µs, it is therefore 55 times faster.
Measuring the number of instruction address translation lookaside buffer (iTLB) misses reveals that each traditional system call triggers one iTLB load miss while kernel calls inside the AnyCall trigger none. The number of instructions per invocation is also reduced by 81 %.

Vector AnyCalls
In real-world applications, it is common to execute the same system call repeatedly on different data [27]. Using AnyCall, one can easily create such vector versions for arbitrary system calls, even incorporating user-defined error handling. To demonstrate this, we have created vector versions of the open() and close() system calls. Our vector open() Any-Call creates a requested number of unnamed temporary files and stores the file descriptors into an array (Opening named files is also possible by passing an array of strings to the AnyCall). The vector close() AnyCall receives an array of file descriptors and executes close() for each. In comparison to the getpid() experiment, more memory is accessed in kernel and user space. Figure 5 displays the aggregated execution time for 150 invocations of the vector open() and close() AnyCalls (each invocation executes the respective system call 1-300 times) and compares it to the equivalent variant using traditional system calls. The initial overhead to load the two eBPF programs is 33.65 ms. The execution time for the two Any-Calls increases by 0.56 ms with each processed file while the runtime of the variant doing traditional system calls increases by 0.87 ms with each file. Therefore, the two Any-Calls are faster by 36 %. In comparison to our getpid() experiment execution-time difference is smaller, as the user/ kernel transition overhead dominates the getpid() execution time. However, the number of calls required to compensate for the loading overhead is smaller because more time is saved per system call. Approximately 16 500 system calls suffice to justify the use of AnyCall.

Real-World File Searching Tool
Many file types use magic values at predefined offsets for identification. To demonstrate that AnyCall can speed up a real-world application, we have applied it to a tool that filters files by such magic values. For this tool, the list of files is received on standard input (generated by find -type f) and each file is opened, seek-ed, read and closed. If the contents read from the offset match the magic value, the file path is written to standard output. In total, we have implemented four variants of the tool. One uses AnyCalls and three use traditional system calls: AnyCall To allow for a larger eBPF program it is beneficial to read in a chunk of file paths and prepare an array of zero-terminated strings to be passed to the eBPF program. The eBPF program checks each path in the chunk using open(), lseek(), read(), and close(). It conditionally calls write() to print matches. sys-burst executes the same algorithm as the AnyCall variant but uses repeated traditional system calls. sys also checks the file contents using traditional system calls, but only reads in one file path at a time. libc checks the file contents using buffered IO based on C-library FILE pointers. Like sys, it processes one file path at a time.
The directory traversal by find and the processing of the input is always performed in user space and included in the execution time. Figure 6 displays the runtime when the Linux v5.0 sources are checked for files with the /bin/sh shebang Figure 6. Execution time when searching all files with the /bin/sh shebang in the Linux source tree using AnyCall and traditional system calls.
using the different implementations. Even though the Any-Call variant issues only a single AnyCall, it outperforms the fastest user-space variant by 24 %.
The best chunk sizes for AnyCall and sys-burst were empirically determined to be 512 and 1024. Chunk sizes above 512 were not possible for AnyCall, because the eBPF program became too large for static analysis. However, sys-burst is already outperformed if the chunk size is set to 4, which translates to only 24 kernel calls per AnyCall.

Future Work
Future work will evaluate our AnyCall implementation on systems without the kernel mitigations for Meltdown and Spectre active [26,28]. We further intend to provide tool support for AnyCalls. Sections of a program to be run using an AnyCall can be detected and transformed by a compiler, making our solution transparent to the programmer. We will demonstrate the advantages of AnyCall on additional real-world applications, in particular, database management systems, backup tools, and other file-system utilities. Generic, re-usable AnyCalls, can be made available to untrusted applications using setuid executables.

Conclusion
This paper has presented AnyCall, which aggregates system calls and application-specific control logic, and executes as eBPF bytecode in the Linux kernel. AnyCall maintains isolation while decoupling the number of user/kernel transitions from the number of system calls. Our run-time environment for AnyCalls provides helper functions to access system calls from within eBPF, as well as memory management. Using a getpid() microbenchmark, we have demonstrated that kernel calls inside the AnyCall environment are up to 55 times faster than system calls from user space. We have measured that vector AnyCalls are 36 % faster than the user space equivalent, and finally, have sped up a real-world file searching tool by 24 %.