Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Software Compartmentalisation Policies

You can access the slides 🖼️ for this lecture.

We have seen in the introduction that compartmentalising an application broadly involves three main steps:

  1. Determining a compartmentalisation policy.
  2. Implementing the compartmentalisation in the target application using abstractions.
  3. Enforcing at runtime the isolation between compartments using one or several mechanisms.

In this lecture, we cover policies.

Compartmentalisation Policies

What is a compartmentalisation policy? It is a series of choices made at the design stage when compartmentalising. More precisely, it is the definition, for a target application to compartmentalise, of how many compartments there should be, and what bits of the application go into what compartment. Assume we have an existing monolithic application to compartmentalise. As illustrated below, there are different possible compartmentalisation policies, and of course choosing a particular policy will have important consequences on the security and performance of the future compartmentalised application:

Compartment Selection Method

A first important choice regarding the policy is how to organise compartments. There are mainly two choices here:

  • Code-centric or spatial approaches split the source code into different compartments. An example of a code-centric policy would be: put each library of the program within its own compartment. Another example is illustrated in the diagram below: we have a web server to compartmentalise, and choosing to put the main server code in one compartment and the SSL library in another is a code-centric compartmentalisation policy.
  • Data-centric, also called temporal or horizontal, approaches rather place execution flows within their own compartments: for example, each thread or each process of an application is placed within its own compartment. You have an example below with our web server: imagine it spawns a certain number of worker threads, for example to handle requests concurrently. These workers execute more or less the same code, each running within its own compartment.
  • Both methods can be combined into hybrid approaches. In our example below, we can place the main application’s code in one compartment, one library in a second compartment, plus additional compartments, one per worker thread.

Compartment Granularity

The granularity of a compartmentalisation policy denotes how large or small compartments can be:

Coarser granularitiesFiner granularities
ProsReduce compartmentalisation effort, lower performance impactBetter privilege reduction
ConsLow degree of privilege reductionHigher compartmentalisation complexity and performance impact

From coarser to finer-grain we can compartmentalise at the level of library/software packages/subsystem, linkage units, functions, or even pieces of code within functions. Coarse granularity means that each compartment contains a large amount of code: generally this translates into a lower number of compartments, which reduces the compartmentalisation effort and the performance slowdown because there are less security domain switches. The degree of privilege reduction is also limited due to the large size of compartments. Fine granularity means having small compartments, which may translate into a large number of compartments. This is beneficial from the privilege reduction point of view, as it limits what an attacker can access when they subvert a single compartment. On the downside, a large number of compartments requires a lot of engineering to be put in place, and the many security domain crossings it involves at runtime can slow things down significantly.

Automating Policy Generation

As previously discussed, automation is an ideal goal in compartmentalisation. It would be ideal if we could take a monolithic program and give it to a framework, maybe a compiler, that would produce a compartmentalised version fully automatically, without any engineering effort or expert knowledge. In practice, existing production-ready or research approaches at compartmentalisation policy generation present various degrees of automation.

Manual Compartmentalisation. Most compartmentalised applications running currently in production have been partitioned fully manually.This requires an important amount of engineering effort, and entirely relies on the expertise of the programmers. Such approaches are also prone to human error, and it is difficult to prove that the resulting compartmentalisations are entirely correct. Still, one benefit of manual compartmentalisation is that an expert developer can precisely identify the components of an application that have low degrees of trust, and the components that are security sensitive. This leads to high-quality compartmentalisation policies, at the cost of a high engineering effort and correctness concerns.

Guided Manual Compartmentalisation. Such approaches assist developers with tools and feedback loops to reduce errors and improve boundary definition, while most of the policy definition effort stays manual. Typical aspects of the compartmentalisation that can be guided are the identification of security-sensitive or low-trust pieces of code and data.

Policy-refinement methods. With refinement approaches, the developer indicates high-level policies (e.g., “library A and library B should be within the same sandbox”), for example in a configuration file, or by marking untrusted and security-critical data with code annotations and letting the system make sure both categories do not end up in the same compartment. The frameworks supporting this method apply a good deal of automation; however, certain aspects of the job, such as securing interfaces, are still very hard to automate today and may require reworking of automatically generated policies.

  • Full automation
    • Requires no effort from the programmer.
    • Computing data dependencies without manual refinement may lead to a weakened degree of privilege reduction.
  • But full automation also raises concerns about lowered security guarantees.
  • For example, it requires the extensive use of static analysis techniques which tend to overestimate: if such techniques are used to identify data that should be shared between several compartments, this will likely lead to a certain amount of oversharing.

Overall, the more automation is used, the lower the engineering effort is, but also the lower the security guarantees obtained from compartmentalisation will be.

Policy Languages

The ways in which policies can be expressed within the code of the compartmentalised program vary. Many existing compartmentalisation frameworks make use of code annotations, for example compiler attributes. These annotations allow things like marking some data as security sensitive or untrusted, marking data as private to a compartment or shared between multiple compartments, or indicating compartment boundaries. You have an example of annotations on the top right here, with a global reference marked as shared with a compartment, and a password variable marked as private to the containing compartment:

int function(char *parameter) {
  // treat all data as private by default,
  // mark shared data as such
  int __shared(compartment1) *glob_ref = // ...
  // or treat all data as shared by default,
  // mark private data as such
  char __private password[128];
}

Another approach is to use higher-level placement rules, for example the configuration file on the bottom right here places each library within its own compartment, and the rest of the code within an additional compartment:

# libredis, libopenjpg, and libxml each in a
# separate compartment, rest of the code in
# another compartment

default: comp0

libraries:
- libredis: comp1
- libopenjpg: comp2
- libxml: comp3

Overall, compartmentalisation frameworks let you express policy information in the code at various granularities, e.g., variables vs. entire libraries in our examples, and this also makes it more or less easy to enforce certain trust models over others. For example if we can only mark data as untrusted for sandboxing, but cannot mark data as security-sensitive, it’s unlikely that the framework in question supports the safebox isolation model.

Analysis Techniques

For approaches using automation, some analysis techniques must be used to determine permissions, compartment boundaries, and the status of data such as shared or private.

Static analysis is generally the default choice. It scales well to many applications and different policies, and, unlike dynamic analysis, it is complete, which is important. Unfortunately, it overestimates, which leads to issues such as oversharing when used to identify shared data. It can also be quite slow and resource demanding, and may not scale to very large code bases.

Dynamic analysis is, on the other hand, incomplete and underestimates: compartments may be underprivileged. This leads to permission faults at runtime under legitimate behaviour, which is not desirable in production. Overall, dynamic analysis offers good scalability to large programs (they just need to be executed), but poor scalability to many programs/policies (each combination needs to be executed).

A few hybrid methods have been presented, but overall static and dynamic analysis do not compose well. Indeed, when you mix them, you obtain a mix of the drawbacks of both approaches, which is not ideal.

Programming Language Genericity

The vast majority of policy definition methods are not generic and focus on one or a class of programming languages. This is because they need to tackle specific problems, such as pointer aliasing in C, but also because many approaches leverage language-specific features. For example the RLBox framework focuses on C++ and uses the rich type information exposed by the language to partially automate some interface safety checks, something not available in other languages, e.g., C. Another example is the software fault isolation model applied to WebAssembly sandboxes, which is limited to the many languages supported by this runtime.