Merge remote-tracking branch 'upstream/main' into update Change-Id: Ie9ff689cb05cb5a5d454b9fcfc1b5353127991fa
diff --git a/.clang-tidy b/.clang-tidy index 43eb5ff..9c44d7f 100644 --- a/.clang-tidy +++ b/.clang-tidy
@@ -1,4 +1,4 @@ -Checks: 'clang-diagnostic-*,-clang-diagnostic-error,google-readability-casting,readability-else-after-return,performance-unnecessary-copy-initialization,bugprone-use-after-move,modernize-use-nullptr,modernize-redundant-void-arg,modernize-return-braced-init-list,modernize-use-default-member-init,modernize-use-equals-default,modernize-use-equals-delete,modernize-use-nodiscard,modernize-use-override,cppcoreguidelines-avoid-goto,misc-unconventional-assign-operator,cppcoreguidelines-narrowing-conversions,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-forwarding-reference-overload,bugprone-macro-parentheses,bugprone-macro-repeated-side-effects,bugprone-move-forwarding-reference,bugprone-misplaced-widening-cast,bugprone-swapped-arguments,bugprone-undelegated-constructor,bugprone-unused-raii,cert-dcl21-cpp,llvm-namespace-comment,misc-static-assert,misc-redundant-expression,modernize-loop-convert,readability-non-const-parameter,readability-identifier-naming' +Checks: 'clang-diagnostic-*,-clang-diagnostic-error,google-readability-casting,readability-else-after-return,performance-unnecessary-copy-initialization,bugprone-use-after-move,modernize-use-nullptr,modernize-redundant-void-arg,modernize-return-braced-init-list,modernize-use-default-member-init,modernize-use-equals-default,modernize-use-equals-delete,modernize-use-nodiscard,modernize-use-override,cppcoreguidelines-avoid-goto,misc-unconventional-assign-operator,cppcoreguidelines-narrowing-conversions,bugprone-assert-side-effect,bugprone-bool-pointer-implicit-conversion,bugprone-copy-constructor-init,bugprone-forwarding-reference-overload,bugprone-macro-parentheses,bugprone-macro-repeated-side-effects,bugprone-move-forwarding-reference,bugprone-misplaced-widening-cast,bugprone-swapped-arguments,bugprone-undelegated-constructor,bugprone-unused-raii,cert-dcl21-cpp,llvm-namespace-comment,misc-static-assert,misc-redundant-expression,modernize-loop-convert,readability-non-const-parameter,readability-identifier-naming,readability-braces-around-statements' FormatStyle: file UseColor: true WarningsAsErrors: false
diff --git a/.github/workflows/test-new-xmake-nightly.yml b/.github/workflows/test-new-xmake-nightly.yml new file mode 100644 index 0000000..eac04ab --- /dev/null +++ b/.github/workflows/test-new-xmake-nightly.yml
@@ -0,0 +1,41 @@ +name: Nightly xmake test + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + run-tests: + strategy: + fail-fast: false + runs-on: ubuntu-latest + container: + image: ghcr.io/cheriot-platform/devcontainer:latest + options: --user 1001 + steps: + - name: Checkout repository and submodules + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Build latest xmake + run: | + xmake update dev + sudo apt remove -y xmake + echo ~/.xmake/profile + . ~/.xmake/profile + - name: Build tests + run: | + pwd + echo ~/.xmake/profile + which xmake + xmake --version + cd tests + xmake f --board=${{ matrix.board }} --sdk=/cheriot-tools/ ${{ matrix.build-flags }} + xmake + - name: Run tests + run: | + . ~/.xmake/profile + xmake --version + cd tests + xmake run
diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index d97e2b0..f601025 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md
@@ -79,7 +79,7 @@ - The LLVM-based toolchain with CHERIoT support - The emulator generated from the Sail formal model of the CHERIoT ISA - The xmake build tool - - [Sonata only] u2futils to create images that Sonata's loader can boot + - [Sonata only] uf2utils to create images that Sonata's loader can boot Building LLVM is fairly simple, but requires a fast machine and several GiBs of disk space. Building the executable model requires a working ocaml installation. @@ -224,7 +224,7 @@ # apt install xmake ``` -### Installing u2futils +### Installing uf2utils If you are working with Sonata, you will need to convert the ELF files that the linker produces to [USB Flashing Format (UF2)](https://github.com/microsoft/uf2). The firmware on the RPi 2040 on the Sonata board can then load these files onto the CHERIoT Ibex and run them.
diff --git a/examples/06.producer-consumer/producer.cc b/examples/06.producer-consumer/producer.cc index a4ea318..417505a 100644 --- a/examples/06.producer-consumer/producer.cc +++ b/examples/06.producer-consumer/producer.cc
@@ -16,17 +16,16 @@ void __cheri_compartment("producer") run() { // Allocate the queue - SObj sendHandle; - SObj receiveHandle; + SObj queue; non_blocking<queue_create_sealed>( - MALLOC_CAPABILITY, &sendHandle, &receiveHandle, sizeof(int), 16); + MALLOC_CAPABILITY, &queue, sizeof(int), 16); // Pass the queue handle to the consumer. - set_queue(receiveHandle); + set_queue(queue); Debug::log("Starting producer loop"); // Loop, sending some numbers to the other thread. for (int i = 1; i < 200; i++) { - int ret = blocking_forever<queue_send_sealed>(sendHandle, &i); + int ret = blocking_forever<queue_send_sealed>(queue, &i); // Abort if the queue send errors. Debug::Invariant(ret == 0, "Queue send failed {}", ret); }
diff --git a/examples/10.audit/README.md b/examples/10.audit/README.md new file mode 100644 index 0000000..cd8d379 --- /dev/null +++ b/examples/10.audit/README.md
@@ -0,0 +1,313 @@ +Auditing compartments +===================== + +This example shows a simple use of the auditing tool. +The code in this is very simple: + + - A `caesar` compartment exposes interfaces to encrypt and decrypt data using a Caesar cypher. + - A `producer` compartment uses this to encrypt a value using a key held in a software capability. + - An `entry` compartment receives this encrypted data and forwards it to a `consumer` compartment. + - The `consumer` compartment receives the data and invokes the `caesar` compartment to decrypt it. + +This is a fairly contrived example but it provides a simple structure for exploring [`cheriot-audit`](https://github.com/CHERIoT-Platform/cheriot-audit). +This tool applies [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) policies to CHERIoT firmware images. + +Note: For all of the `cheriot-audit` examples, we'll assume that you're running a command something like this: + +```sh +$ cheriot-audit --board=../../sdk/boards/sail.json --firmware-report=build/cheriot/cheriot/release/caesar_example.json --module=caesar.rego --query '{query}' | jq +``` + +You may need to specify a full path to cheriot-audit (it is in `/cheriot-tools/bin` in the dev container). +The query will be shown before the results. +Piping the output to `jq` is optional, but will give pretty-printed output for valid JSON (remove it for places where you get an error: `undefined` is not rendered by `jq`). + +The `--board` argument is the same as the board JSON file that you identified with the `--board` argument you passed to `xmake` (if you didn't pass a path to that, it will look in `sdk/boards`). +The `--firmware` is the JSON file produced during the final firmware link step. +The `--module` argument is the [`caesar.rego`](caesar.rego) file that contains the policy written for this example. +Finally, the `--query` argument is the Rego query to run. + +Validating our Caesar capabilities +---------------------------------- + +The capabilities for the Caesar cypher (defined in [`caesar_cypher.h`](caesar_cypher.h)) contain three single-byte fields: + + - A permit-encrypt permission. + - A permit-decrypt permission. + - A shift value (Caesar cypher is a simple substitution cypher that rotates the alphabet, and so the 'key' is the amount that each letter is shifted). + +These are all static sealed objects that are held by the compartment that can authorise encryption or decryption. +To encrypt a message, you call the encrypt function exposed by the `caesar` compartment and pass it one of these capabilities. +The compartment will dynamically check that it is the right kind of capability and that it permits sealing, then encrypt with the embedded shift amount. + +This has the property that any compartment that holds an authorising capability can request encryption or decryption, but does not itself know the key. + +To validate the properties of our keys, let's start by defining a rule that identifies whether an import is a Caesar capability: + +```rego +# Check if an import is sealed with the Caesar capability type +is_sealed_caesar_capability(capability) { + capability.kind = "SealedObject" + capability.sealing_type.compartment = "caesar" + capability.sealing_type.key = "CaesarCapabilityType" +} +``` + +This is a unary rule (it takes one argument). +Rego rules are similar to Prolog predicates. +They are not functions that return true or false, they are logical expressions that either hold or don't. +This distinction rarely matters, but it can be confusing because a failure will be reported in Rego as `undefined` instead of `false`. + +This rule holds (is true) if all of the rules listed on the lines between braces hold. +The `=` operator in Rego is *unification*, not assignment. +This means that it will try to find values on the left and right sides that allow the equality to hold. + +In English, this says that the argument is a sealed Caesar capability if (and only if) its kind if `SealedObject`, the compartment that owns the sealing type is `caesar` and the sealing type is the one that this compartment exports as `CaesarCapabilityType`. + +We can see how this works by using it with a Rego *comprehension*. +Try running this query: + +```rego +[ c | c = input.compartments[_].imports[_] ; data.caesar.is_sealed_caesar_capability(c) ] +``` + +This will evaluate to an array of values of `c`, where `c` is every import from any compartment, filtered by the rule that we've just written. +The underscores are distinct anonymous variables. +Because we never constrain these, they can be any value, and so this will find any compartment, and any import in that compartment, and then filter them. + +Note that we refer to our rule with a `data.caesar` prefix. +All Rego modules are imported into the `data` namespace with the module name as the second-level namespace. + +The output should look something like this: + +```json +[ + { + "contents": "00015f00", + "kind": "SealedObject", + "sealing_type": { + "compartment": "caesar", + "key": "CaesarCapabilityType", + "provided_by": "build/cheriot/cheriot/release/caesar.compartment", + "symbol": "__export.sealing_type.caesar.CaesarCapabilityType" + } + }, + { + "contents": "01005f00", + "kind": "SealedObject", + "sealing_type": { + "compartment": "caesar", + "key": "CaesarCapabilityType", + "provided_by": "build/cheriot/cheriot/release/caesar.compartment", + "symbol": "__export.sealing_type.caesar.CaesarCapabilityType" + } + } +] +``` + +This has found the two capabilities that we expected to find looking for. + +Decoding our Caesar capabilities +-------------------------------- + +Seeing something like `"contents": "01005f00"` in the above example isn't that informative. +Is this a valid set of values? +The next step is to write something to decode these. + +We'll start with a helper to convert integers into C booleans: + +```rego +value_as_boolean(value) = output { + value = 1 + output = true +} + +value_as_boolean(value) = output { + value = 0 + output = false +} +``` + +This is a Rego rule that has two definitions. +The first will hold if the value is 1, and will set the result to `true`. +The second will hold if the value is 0, and will set the result to `false`. +If the value is anything other than 0 or 1, this rule will not hold. + +Try this with the following two queries: + +```rego +data.caesar.value_as_boolean(1) +data.caesar.value_as_boolean(2) +``` + +These should give `true` and `undefined`, respectively. +Rego reports unification failure as `undefined` and this propagates upwards. +Any rule that depends on a rule that is undefined will also be undefined. +We can use this to make sure that the boolean values that we want are canonical true and false values, as well as decoding them. + +With these defined, let's move on to the Rego rule that decodes one of these capabilities: + +```rego +decode_user_key_capability(capability) = decoded { + # Fail if this is not sealed with the Caesar capability type + is_sealed_caesar_capability(capability) + some permitEncrypt, permitDecrypt, shift + + # Extract the values. Each of these will fail if the value is not as expected. + # permitEncrypt is a (single-byte) boolean value at offset 0. + permitEncrypt = value_as_boolean(integer_from_hex_string(capability.contents, 0, 1)) + + # permitDecrypt is a (single-byte) boolean value at offset 1. + permitDecrypt = value_as_boolean(integer_from_hex_string(capability.contents, 1, 1)) + + # shift is a single-byte integer value at offset 2. + shift = integer_from_hex_string(capability.contents, 2, 1) + decoded = { + "permitEncrypt": permitEncrypt, + "permitDecrypt": permitDecrypt, + "shift": shift, + } +} +``` + +This starts by depending on the rule that we defined first, which will cause this to fail for anything that isn't a capability of the expected type. +Next, we define three local variables to hold the three fields that we expect. +We'll extract each of these using `integer_from_hex_string`, a built-in function provided by `cheriot-audit`. +This takes a string, an offset, and a length and will decode a little-endian integer from the hex string provided. +We're extracting three one-byte integers at offsets 0, 1, and 2. +We're then converting the first two to booleans. + +Finally, if all of that worked, we're returning a new object that mirrors the C structure that represents our capability. +We can use this with another comprehension to extract and decode all valid capabilities. +This is sufficiently useful that we'll define a new rule for it: + +```rego +all_valid_caesar_capabilities = [{"owner": owner, "capability": decode_user_key_capability(c)} | c = input.compartments[owner].imports[_]; is_sealed_caesar_capability(c)] +``` + +This is a slightly more complex comprehension. +The result is defining a new object with both the owner and the decoded capability. +Try running this: + +```rego +data.caesar.all_valid_caesar_capabilities +``` + +You should see something like this: + +```rego +[ + { + "capability": { + "permitDecrypt": true, + "permitEncrypt": false, + "shift": 95 + }, + "owner": "consumer" + }, + { + "capability": { + "permitDecrypt": false, + "permitEncrypt": true, + "shift": 95 + }, + "owner": "producer" + } +] +``` + +Check these values with the ones declared in the source code (in [`producer.cc`](producer.cc) and [`consumer.cc`](consumer.cc)). + +Defining our requirements +------------------------- + +Now that we have all of the helpers that let us inspect the linked image, let's define a `valid` rule that defines the policy for our linked compartment. +We'll start here by depending on the `valid` rule from the RTOS itself: + +```rego + data.rtos.valid +``` + +This performs some sanity checking on the RTOS core, such as ensuring that only the allocator can read hazard-pointer slots, that allocator capabilities are all valid, and so on. + +Next, we'll make sure that we have the right number of Caesar capabilities and that the number of *valid* Caesar capabilities is the same: + +```rego + # There are two things sealed with the Caesar capability type + count([c | c = input.compartments[owner].imports[_]; is_sealed_caesar_capability(c)]) = 2 + + # Both of them are valid Caesar capabilities + count(all_valid_caesar_capabilities) = 2 +``` + +The first of these will find everything that is sealed as a Caesar capability, the second will find only ones that decode correctly. +We know that only the producer and consumer compartments should hold these capabilities, so we check that the number found is two. + +Having done that, we extract the two capabilities that we expect to exist: + +```rego + some producerCapability, consumerCapability + producerCapability = [c | c = all_valid_caesar_capabilities[_]; c.owner = "producer"][0].capability + consumerCapability = [c | c = all_valid_caesar_capabilities[_]; c.owner = "consumer"][0].capability +``` + +Each of these starts with a comprehension that filters the set of all Caesar capabilities to find the ones with the named owner and then extracts the capability. +Note that, in this case, we can assume that there is a single value here and so hard code array index 0. +If that assumption is incorrect then either our previous assertion that there are two capabilities in total, or a later assertion where we inspect properties of the capabilities, will fail. + +Now that we have these, ensure that the producer is permitted to encrypt and the consumer to decrypt, but not vice versa. + +```rego + producerCapability.permitEncrypt = true + producerCapability.permitDecrypt = false + consumerCapability.permitEncrypt = false + consumerCapability.permitDecrypt = true +``` + +We expect the consumer to be able to decrypt things encrypted by the producer, so let's also make sure that their shift values are the same: + +```rego +producerCapability.shift = consumerCapability.shift +``` + +Finally, for some defence in depth, let's make sure that the producer is the only caller of the encrypt function and the consumer the only caller of the decrypt function: + +```rego + data.compartment.compartment_call_allow_list("caesar", "caesar_encrypt.*", {"producer"}) + data.compartment.compartment_call_allow_list("caesar", "caesar_decrypt.*", {"consumer"}) +``` + +This is not necessary in theory, because these require an authorising capability and so another compartment calling them should fail. +Another compartment trying to call them is definitely a bug though, so it's worth checking. +Similarly, let's make sure that the producer and consumer are called only from the entry compartment + +```rego + data.compartment.compartment_call_allow_list("producer", "produce_message.*", {"entry"}) + data.compartment.compartment_call_allow_list("consumer", "consume_message.*", {"entry"}) +``` + +Putting this all together, we can now run a very simple query: + +```rego +data.caesar.valid +``` + +If this all worked, the result should be simply `true`. + +A policy like this can be included in CI to make sure that everything that you commit meets the policy. +It can drive key release for code signing, so that you never sign a firmware image that doesn't meet your policy. +Along the way to writing the final policy, we built a set of tools for introspection on the firmware image, so you can query properties. + +Note on cryptography +-------------------- + +This example uses a Caesar Cypher. +This was chosen because it is easy to implement, not because it is secure. +Post-quantum encryption algorithms are designed to be robust against hypothetical quantum computers. +Modern classical encryption algorithms are robust against attacks by large classical computers. +The Caesar Cypher is not robust against a person with a pen and paper. + +Under no circumstances should you copy the encryption portion of this code into anything that needs to be robust in the presence of an adversary with ten minutes and a piece of paper. +Breaking Caesar Cyphers is a fun thing for small children to do, not a challenge for a real cryptanalyst. + +All of that said, the same mechanisms used in this example *can* be used with more sensible encryption schemes to ensure key confidentiality.
diff --git a/examples/10.audit/caesar.rego b/examples/10.audit/caesar.rego new file mode 100644 index 0000000..d310288 --- /dev/null +++ b/examples/10.audit/caesar.rego
@@ -0,0 +1,77 @@ +package caesar + +# Check if an import is sealed with the Caesar capability type +is_sealed_caesar_capability(capability) { + capability.kind = "SealedObject" + capability.sealing_type.compartment = "caesar" + capability.sealing_type.key = "CaesarCapabilityType" +} + +# Helpers for converting C integers to booleans. +# These fail if the input is not either 1 or 0 +value_as_boolean(value) = output { + value = 1 + output = true +} + +value_as_boolean(value) = output { + value = 0 + output = false +} + +decode_caesar_capability(capability) = decoded { + # Fail if this is not sealed with the Caesar capability type + is_sealed_caesar_capability(capability) + some permitEncrypt, permitDecrypt, shift + + # Extract the values. Each of these will fail if the value is not as expected. + # permitEncrypt is a (single-byte) boolean value at offset 0. + permitEncrypt = value_as_boolean(integer_from_hex_string(capability.contents, 0, 1)) + + # permitDecrypt is a (single-byte) boolean value at offset 1. + permitDecrypt = value_as_boolean(integer_from_hex_string(capability.contents, 1, 1)) + + # shift is a single-byte integer value at offset 2. + shift = integer_from_hex_string(capability.contents, 2, 1) + decoded = { + "permitEncrypt": permitEncrypt, + "permitDecrypt": permitDecrypt, + "shift": shift, + } +} + +# Helper to extract all valid Caesar capabilities in the firmware image +all_valid_caesar_capabilities = [{"owner": owner, "capability": decode_caesar_capability(c)} | c = input.compartments[owner].imports[_]; is_sealed_caesar_capability(c)] + +# Helper predicate to check that this is valid. +valid { + # Make sure that the RTOS configuration is valid. + data.rtos.valid + + # There are two things sealed with the Caesar capability type + count([c | c = input.compartments[owner].imports[_]; is_sealed_caesar_capability(c)]) = 2 + + # Both of them are valid Caesar capabilities + count(all_valid_caesar_capabilities) = 2 + + # Extract the producer and consumer's capabilities + some producerCapability, consumerCapability + producerCapability = [c | c = all_valid_caesar_capabilities[_]; c.owner = "producer"][0].capability + consumerCapability = [c | c = all_valid_caesar_capabilities[_]; c.owner = "consumer"][0].capability + + # Check permissions + producerCapability.permitEncrypt = true + producerCapability.permitDecrypt = false + consumerCapability.permitEncrypt = false + consumerCapability.permitDecrypt = true + + # Make sure that the shift (encryption keys) are the same. + producerCapability.shift = consumerCapability.shift + + # Make sure that only the producer and consumer compartments call the encrypt and decrypt functions + data.compartment.compartment_call_allow_list("caesar", "caesar_encrypt.*", {"producer"}) + data.compartment.compartment_call_allow_list("caesar", "caesar_decrypt.*", {"consumer"}) + # Make sure that only the entry compartment calls the produce and consume functions + data.compartment.compartment_call_allow_list("producer", "produce_message.*", {"entry"}) + data.compartment.compartment_call_allow_list("consumer", "consume_message.*", {"entry"}) +}
diff --git a/examples/10.audit/caesar_cypher.cc b/examples/10.audit/caesar_cypher.cc new file mode 100644 index 0000000..fd87dac --- /dev/null +++ b/examples/10.audit/caesar_cypher.cc
@@ -0,0 +1,72 @@ +#include "caesar_cypher.h" +#include <compartment-macros.h> +#include <debug.hh> +#include <errno.h> + +using Debug = ConditionalDebug<true, "Caesar">; + +int caesar_encrypt(SObj capability, + const char *input, + char *output, + size_t length) +{ + auto *cypherState = token_unseal<CaesarCapability>( + STATIC_SEALING_TYPE(CaesarCapabilityType), capability); + if (cypherState == nullptr) + { + return -EINVAL; + } + if (!cypherState->permitEncrypt) + { + return -EPERM; + } + for (int i = 0; i < length; i++) + { + uint8_t c = input[i]; + // Replace control characters with 'X' + if ((c < 32) || (c >= 127)) + { + c = 'X'; + } + // Reset the base to 0. + c -= 32; + // Perform the encryption + c += cypherState->shift; + if (c > 94) + { + c -= 94; + } + c += 32; + output[i] = c; + } + return 0; +} + +int caesar_decrypt(SObj capability, + const char *input, + char *output, + size_t length) +{ + auto *cypherState = token_unseal<CaesarCapability>( + STATIC_SEALING_TYPE(CaesarCapabilityType), capability); + if (cypherState == nullptr) + { + return -EINVAL; + } + if (!cypherState->permitDecrypt) + { + return -EPERM; + } + for (int i = 0; i < length; i++) + { + int c = input[i]; + // Perform the encryption + c -= cypherState->shift; + if (c < 32) + { + c += 94; + } + output[i] = c; + } + return 0; +}
diff --git a/examples/10.audit/caesar_cypher.h b/examples/10.audit/caesar_cypher.h new file mode 100644 index 0000000..1692c73 --- /dev/null +++ b/examples/10.audit/caesar_cypher.h
@@ -0,0 +1,29 @@ +#include <token.h> + +struct CaesarCapability +{ + bool permitEncrypt; + bool permitDecrypt; + uint8_t shift; +}; + +#define DECLARE_AND_DEFINE_CAESAR_CAPABILITY( \ + name, permitEncrypt, permitDecrypt, shift) \ + DECLARE_AND_DEFINE_STATIC_SEALED_VALUE(struct CaesarCapability, \ + caesar, \ + CaesarCapabilityType, \ + name, \ + permitEncrypt, \ + permitDecrypt, \ + shift) + +int __cheri_compartment("caesar") + caesar_encrypt(SObj capability, const char *input, char *output, size_t len); + +int __cheri_compartment("caesar") + caesar_decrypt(SObj capability, const char *input, char *output, size_t len); + +ssize_t __cheri_compartment("producer") + produce_message(char *buffer, size_t length); +void __cheri_compartment("consumer") + consume_message(const char *buffer, size_t length);
diff --git a/examples/10.audit/consumer.cc b/examples/10.audit/consumer.cc new file mode 100644 index 0000000..29f8bb8 --- /dev/null +++ b/examples/10.audit/consumer.cc
@@ -0,0 +1,19 @@ +// Copyright Microsoft and CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +#include "caesar_cypher.h" +#include <debug.hh> +#include <errno.h> + +using Debug = ConditionalDebug<true, "Consumer">; + +DECLARE_AND_DEFINE_CAESAR_CAPABILITY(encrypt, false, true, 95); + +void consume_message(const char *buffer, size_t length) +{ + std::string decrypted; + decrypted.resize(length); + caesar_decrypt( + STATIC_SEALED_VALUE(encrypt), buffer, decrypted.data(), length); + Debug::log("Decrypted message: '{}'", decrypted); +}
diff --git a/examples/10.audit/entry.cc b/examples/10.audit/entry.cc new file mode 100644 index 0000000..ae15563 --- /dev/null +++ b/examples/10.audit/entry.cc
@@ -0,0 +1,24 @@ +// Copyright Microsoft and CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +#include "caesar_cypher.h" +#include <debug.hh> + +using Debug = ConditionalDebug<true, "Entry compartment">; + +char buffer[1024]; + +/// Thread entry point. +void __cheri_compartment("entry") entry() +{ + ssize_t length = produce_message(buffer, sizeof(buffer)); + if (length < 0) + { + Debug::log("Failed to get encrypted message"); + return; + } + Debug::log("Received encrypted message: '{}' ({} bytes)", + std::string_view{buffer, static_cast<size_t>(length)}, + length); + consume_message(buffer, length); +}
diff --git a/examples/10.audit/producer.cc b/examples/10.audit/producer.cc new file mode 100644 index 0000000..fbafc45 --- /dev/null +++ b/examples/10.audit/producer.cc
@@ -0,0 +1,27 @@ +// Copyright Microsoft and CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +#include "caesar_cypher.h" +#include <debug.hh> +#include <errno.h> + +using Debug = ConditionalDebug<true, "Producer">; + +DECLARE_AND_DEFINE_CAESAR_CAPABILITY(encrypt, true, false, 95); + +ssize_t produce_message(char *buffer, size_t length) +{ + const std::string_view Plaintext = "Hello, World!"; + if (length < Plaintext.size()) + { + return -ENOMEM; + } + Debug::log("Encrypting message '{}'", Plaintext); + int ret = caesar_encrypt( + STATIC_SEALED_VALUE(encrypt), Plaintext.data(), buffer, Plaintext.size()); + if (ret < 0) + { + return ret; + } + return Plaintext.size(); +}
diff --git a/examples/10.audit/xmake.lua b/examples/10.audit/xmake.lua new file mode 100644 index 0000000..71cb288 --- /dev/null +++ b/examples/10.audit/xmake.lua
@@ -0,0 +1,42 @@ +-- Copyright Microsoft and CHERIoT Contributors. +-- SPDX-License-Identifier: MIT + +set_project("CHERIoT Compartmentalised hello world (more secure)") +sdkdir = "../../sdk" +includes(sdkdir) +set_toolchains("cheriot-clang") + +option("board") + set_default("sail") + +compartment("caesar") + -- This compartment uses C++ thread-safe static initialisation and so + -- depends on the C++ runtime. + add_files("caesar_cypher.cc") + +compartment("entry") + add_files("entry.cc") + +compartment("producer") + add_files("producer.cc") +compartment("consumer") + add_files("consumer.cc") + +-- Firmware image for the example. +firmware("caesar_example") + -- Both compartments require memcpy + add_deps("freestanding", "debug", "string") + add_deps("entry", "caesar") + add_deps("producer", "consumer") + on_load(function(target) + target:values_set("board", "$(board)") + target:values_set("threads", { + { + compartment = "entry", + priority = 1, + entry_point = "entry", + stack_size = 0x400, + trusted_stack_frames = 3 + } + }, {expand = false}) + end)
diff --git a/sdk/boards/sonata-0.2.json b/sdk/boards/sonata-0.2.json index 6faa442..308904a 100644 --- a/sdk/boards/sonata-0.2.json +++ b/sdk/boards/sonata-0.2.json
@@ -71,6 +71,7 @@ "SUNBURST", "SUNBURST_SHADOW_BASE=0x30000000", "SUNBURST_SHADOW_SIZE=0x4000", + "DEFAULT_UART_BAUD_RATE=115200", "ipconfigDRIVER_INCLUDED_RX_IP_CHECKSUM=1", "ipconfigDRIVER_INCLUDED_TX_IP_CHECKSUM=1" ],
diff --git a/sdk/compartment.ldscript b/sdk/compartment.ldscript index ba46ec4..9a602da 100644 --- a/sdk/compartment.ldscript +++ b/sdk/compartment.ldscript
@@ -36,7 +36,7 @@ # If there is a compartment error handler, make sure that it is before # anything that can have linker relaxations so that its displacement # from __compartment_code_start is a constant. - *(.compartment_error_handler_stackless)); + *(.compartment_error_handler_stackless); *(.compartment_error_handler); *(.text .text.*); }
diff --git a/sdk/core/allocator/alloc.h b/sdk/core/allocator/alloc.h index 1e890ea..8ab0c09 100644 --- a/sdk/core/allocator/alloc.h +++ b/sdk/core/allocator/alloc.h
@@ -746,7 +746,9 @@ TChunk *leftmost_child() { if (child[0] != nullptr) + { return child[0]; + } return child[1]; }
diff --git a/sdk/core/allocator/main.cc b/sdk/core/allocator/main.cc index 8568af9..b5336ce 100644 --- a/sdk/core/allocator/main.cc +++ b/sdk/core/allocator/main.cc
@@ -17,6 +17,17 @@ #include <token.h> #include <utils.hh> +/** + * The sealing key for dynamically allocated software-sealed objects. + */ +__attribute__((section(".sealing_key1"))) void *allocatorSealingKey; + +/** + * The root for the software sealing key. + */ +__attribute__((section(".sealing_key2"))) Capability<SKeyStruct> + softwareSealingKey; + using namespace CHERI; Revocation::Revoker revoker; @@ -1021,19 +1032,6 @@ uint32_t nextSealingType = std::numeric_limits<uint32_t>::max(); /** - * Returns the root for the software sealing key. - */ - __always_inline Capability<SKeyStruct> software_sealing_key() - { - SKeyStruct *ret; - __asm("1: " - " auipcc %0, %%cheriot_compartment_hi(__sealingkey2)\n" - " clc %0, %%cheriot_compartment_lo_i(1b)(%0)\n" - : "=C"(ret)); - return {ret}; - } - - /** * Helper that unseals `in` if it is a valid sealed capability sealed with * our hardware sealing key. Returns the unsealed pointer, `nullptr` if it * cannot be helped. @@ -1044,8 +1042,7 @@ // FIXME: At the moment the ISA is still shuffling types around, but // eventually we want to know the type statically and don't need dynamic // instructions. - Capability key{SEALING_CAP()}; - in.unseal(key); + in.unseal(allocatorSealingKey); return in.is_valid() ? in : SealedAllocation{nullptr}; } @@ -1126,7 +1123,7 @@ obj->type = key.address(); auto sealed = obj; - sealed.seal(SEALING_CAP()); + sealed.seal(allocatorSealingKey); obj.address() += ObjHdrSize; // Exclude the header. obj.bounds() = obj.top() - obj.address(); Debug::Assert( @@ -1141,7 +1138,7 @@ // structures and so can have its own lock. static FlagLock tokenLock; LockGuard g{tokenLock}; - auto keyRoot = software_sealing_key(); + auto keyRoot = softwareSealingKey; // For now, strip the user permissions. We might want to use them for // permit-allocate and permit-free. keyRoot.permissions() &=
diff --git a/sdk/core/allocator/token.h b/sdk/core/allocator/token.h index e9ce8f7..42c7343 100644 --- a/sdk/core/allocator/token.h +++ b/sdk/core/allocator/token.h
@@ -92,3 +92,5 @@ EXPORT_ASSEMBLY_OFFSET(TokenSObj, type, 0); EXPORT_ASSEMBLY_OFFSET(TokenSObj, data, 8); +EXPORT_ASSEMBLY_NAME(CheriSealTypeAllocator, 11); +EXPORT_ASSEMBLY_NAME(CheriSealTypeStaticToken, 12);
diff --git a/sdk/core/loader/boot.cc b/sdk/core/loader/boot.cc index a72541f..e41038a 100644 --- a/sdk/core/loader/boot.cc +++ b/sdk/core/loader/boot.cc
@@ -16,6 +16,7 @@ #include "defines.h" #include "types.h" #include <cheri.hh> +#include <compartment.h> #include <platform-uart.hh> #include <priv/riscv.h> #include <riscvreg.h> @@ -49,6 +50,11 @@ // It must also be aligned sufficiently for trusted stacks, so ensure that // we've captured that requirement above. static_assert(alignof(TrustedStack) <= 16); + + static_assert(sizeof(ErrorState) == offsetof(TrustedStack, hazardPointers)); + static_assert(offsetof(ErrorState, pcc) == offsetof(TrustedStack, mepcc)); + static_assert(offsetof(ErrorState, registers) == + offsetof(TrustedStack, cra)); __END_DECLS static_assert( @@ -62,12 +68,12 @@ /** * 0 represents unsealed. */ - Unsealed = 0, + Unsealed = CheriSealTypeUnsealed, /** * Sentry that inherits interrupt status. */ - SentryInheriting, + SentryInheriting = CheriSealTypeSentryInheriting, /// Alternative name: the default sentry type. Sentry = SentryInheriting, @@ -75,22 +81,32 @@ /** * Sentry that disables interrupts on calls. */ - SentryDisabling, + SentryDisabling = CheriSealTypeSentryDisabling, /** * Sentry that enables interrupts on calls. */ - SentryEnabling, + SentryEnabling = CheriSealTypeSentryEnabling, + + /** + * Return sentry that disables interrupts on return + */ + ReturnSentryDisabling = CheriSealTypeReturnSentryDisabling, + + /** + * Return sentry that enables interrupts on return + */ + ReturnSentryEnabling = CheriSealTypeReturnSentryEnabling, /** * Marker for the first sealing type that's valid for data capabilities. */ - FirstDataSealingType = 9, + FirstDataSealingType = CheriSealTypeFirstDataSealingType, /** * The sealing type used for sealed export table entries. */ - SealedImportTableEntries = FirstDataSealingType, + SealedImportTableEntries = CheriSealTypeSealedImportTableEntries, /** * The compartment switcher has a sealing type for the trusted stack. @@ -98,38 +114,74 @@ * This must be the second data sealing type so that we can also permit * the switcher to unseal sentries and export table entries. */ - SealedTrustedStacks, + SealedTrustedStacks = CheriSealTypeSealedTrustedStacks, /** - * The scheduler has a sealing type for waitable objects. + * The allocator has a sealing type for the software sealing mechanism + * with dynamically allocated objects. */ - Scheduler, + Allocator = CheriSealTypeAllocator, /** - * The allocator has a sealing type for the software sealing mechanism. + * The loader reserves a sealing type for the software sealing + * mechanism. The permit-unseal capability for this is destroyed after + * the loader has run, which guarantees that anything sealed with this + * type was present in the original firmware image. The token library + * has the only permit-unseal capability for this type. */ - Allocator, + StaticToken = CheriSealTypeStaticToken, /** * The first sealing key that is reserved for use by the allocator's * software sealing mechanism and used for static sealing types, */ - FirstStaticSoftware = 16, + FirstStaticSoftware = CheriSealTypeFirstStaticSoftware, /** * The first sealing key in the space that the allocator will * dynamically allocate for sealing types. */ - FirstDynamicSoftware = 0x1000000, + FirstDynamicSoftware = CheriSealTypeFirstDynamicSoftware, }; + // The switcher assembly includes the types of import table entries and + // trusted stacks. This enumeration and the assembly must be kept in sync. + // This will fail if the enumeration value changes. + static_assert(int(SealedImportTableEntries) == 9, + "If this fails, update switcher/entry.S to the new value"); + static_assert(int(SealedTrustedStacks) == 10, + "If this fails, update switcher/entry.S to the new value"); + + // The allocator and static sealing types must be contiguous so that the + // token library can hold a permit-unseal capability for both. + static_assert(int(Allocator) + 1 == int(StaticToken), + "Allocator and StaticToken must be consecutive"); + + // The token library includes the types for allocator and statically sealed + // objects. This enumeration and the assembly must be kept in sync. This + // will fail if the enumeration value changes. + static_assert(int(Allocator) == 11, + "If this fails, update token_unseal.S to the new value"); + // We currently have a 3-bit hardware otype, with different sealing spaces // for code and data capabilities, giving the range 0-0xf reserved for // hardware use. Assert that we're not using more than we need (two in the // enum are outside of the hardware space). - static_assert(magic_enum::enum_count<SealingType>() <= 10, + static_assert(magic_enum::enum_count<SealingType>() <= 12, "Too many sealing types reserved for a 3-bit otype field"); +} // namespace + +/* + * Unusually late, include this where we have access to the above enum + * SealingType, but early enough that the constants defined herein are available + * to the rest of the code. + */ +#include "../switcher/misc-assembly.h" + +namespace +{ + constexpr auto StoreLPerm = Root::Permissions<Root::Type::RWStoreL>; /// PCC permissions for the switcher. constexpr auto SwitcherPccPermissions = @@ -306,6 +358,23 @@ return ptr.seal(key); } + template<InterruptStatus Status, typename T> + T *seal_return(Capability<T> ptr) + { + static_assert((Status == InterruptStatus::Enabled) || + (Status == InterruptStatus::Disabled)); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc99-designator" + constexpr SealingType Sentries[] = { + [int(InterruptStatus::Enabled)] = ReturnSentryEnabling, + [int(InterruptStatus::Disabled)] = ReturnSentryDisabling}; +#pragma clang diagnostic pop + size_t otype = size_t{Sentries[int(Status)]}; + void *key = build<void, Root::Type::Seal>(otype, 1); + return ptr.seal(key); + } + /** * Helper to determine whether an object, given by a start address and size, * is completely contained within a specified range. @@ -582,12 +651,6 @@ Root::Type::RWGlobal, PermissionSet{Permission::Load, Permission::Store}>( entry.address); - // Is the software sealing type owned by the scheduler? If so, - // we're going to seal the object with the scheduler's sealing - // type, not the allocator's. This lets the scheduler export - // software-defined capabilities without adding the allocator - // to the TCB for availability. - bool isSchedulerObject = false; // TODO: This currently places a restriction that data memory // can't be in the low 64 KiB of the address space. That may be // too restrictive. If we haven't visited this sealed object @@ -613,11 +676,7 @@ return false; }; bool found = findExport(image.allocator()); - if (!found && findExport(image.scheduler())) - { - found = true; - isSchedulerObject = true; - } + found |= findExport(image.scheduler()); for (auto &compartment : image.compartments()) { if (found) @@ -632,8 +691,8 @@ } Capability sealedObject = build(entry.address, entry.size()); // Seal with the allocator's sealing key - sealedObject.seal(build<void, Root::Type::Seal>( - isSchedulerObject ? Scheduler : Allocator, 1)); + sealedObject.seal( + build<void, Root::Type::Seal>(StaticToken, 1)); Debug::log("Static sealed object: {}", sealedObject); return sealedObject; } @@ -784,6 +843,21 @@ // Space per thread for hazard pointers. static constexpr size_t HazardPointerSpace = HazardPointersPerThread * sizeof(void *); + + /* + * Construct a return sentry with which to populate initial thread + * register files, as if they had been entered by the switcher rather + * than by fiat of initial construction. The switcher will detect the + * trusted stack underflow and will signal the scheduler that the thread + * has exited and should not be brought back on core. + */ + auto threadInitialReturn = + build<void, Root::Type::Execute, SwitcherPccPermissions>( + image.switcher.code); + threadInitialReturn.address() += image.switcher.crossCallReturnEntry; + threadInitialReturn = + seal_return<InterruptStatus::Disabled>(threadInitialReturn); + for (size_t i = 0; const auto &config : image.threads()) { Debug::log("Creating thread {}", i); @@ -819,6 +893,7 @@ false>(config.trustedStack); threadTStack->mepcc = pcc; threadTStack->cgp = cgp; + threadTStack->cra = threadInitialReturn; // Stacks have store-local but not global permission. auto stack = build<void, @@ -1184,11 +1259,10 @@ trustedStackKey.bounds() = 1; switcherKey.address() = SealedImportTableEntries; switcherKey.bounds() = 1; - setSealingKey(imgHdr.scheduler(), Scheduler); setSealingKey(imgHdr.allocator(), Allocator); setSealingKey(imgHdr.token_library(), Allocator, - 1, + 2, // Allocator and StaticToken 0, PermissionSet{Permission::Global, Permission::Unseal}); constexpr size_t DynamicSealingLength =
diff --git a/sdk/core/loader/types.h b/sdk/core/loader/types.h index ec488c9..7a29847 100644 --- a/sdk/core/loader/types.h +++ b/sdk/core/loader/types.h
@@ -363,6 +363,12 @@ AddressRange code; /** + * The PCC-relative location of the cross-compartment call return + * path, used to build the initial return addresses for threads. + */ + uint16_t crossCallReturnEntry; + + /** * The PCC-relative location of the sealing key, which the * compartment switcher will use to unseal import table entries. */ @@ -447,7 +453,7 @@ * The distance from the start of the code region to the end of the * import table. */ - uint16_t importTableSize; + AddressRange importTable; /** * The export table for the scheduler. @@ -465,11 +471,7 @@ */ [[nodiscard]] AddressRange import_table() const { - // Skip the sealing keys - const size_t SealingKeysSize = 2 * sizeof(void *); - return { - code.start() + SealingKeysSize, - static_cast<uint16_t>(importTableSize - SealingKeysSize)}; + return importTable; } /** @@ -612,7 +614,7 @@ // This is a random 32-bit number and should be changed whenever // the compartment header layout changes to provide some sanity // checking. - return magic == 0x6cef3879; + return magic == 0xca2b63de; } /**
diff --git a/sdk/core/scheduler/common.h b/sdk/core/scheduler/common.h index 565152d..ea9ccfd 100644 --- a/sdk/core/scheduler/common.h +++ b/sdk/core/scheduler/common.h
@@ -8,6 +8,7 @@ #include <cheri.hh> #include <debug.hh> #include <stdlib.h> +#include <token.h> #include <type_traits> namespace @@ -45,57 +46,14 @@ StackUsageCheck<StackMode, expected, __PRETTY_FUNCTION__> stackCheck /** - * Base class for types that are exported from the scheduler with a common - * sealing type. Includes an inline type marker. + * Base class for sealed objects that are exported from the scheduler. + * + * Subclasses must implement a static `sealing_type` method that returns + * the sealing key. */ + template<bool IsDynamic> struct Handle { - protected: - /** - * The real type of this subclass. - * - * This must be 32 bits and must be the first word of the class, so that - * we are layout-compatible with the static sealing capabilities. We - * use low-value numbers for things that we dynamically allocate to - * check types. Anything that is statically allocated will have this - * field initialised by the loader. - * - * Note that allocator-allocated and static types have a word of - * padding here. This is necessary to ensure that the base of the sub - * object is capability-aligned. In cases created with subclassing, - * this is not required - the compiler will insert padding if it needs - * to, but we can also put small fields in this space. For anything - * statically allocated, we will need to handle this separately. - */ - enum class Type : uint32_t - { - Invalid = 0, - - /** - * Multiwaiter type. - */ - MultiWaiter, - - /** - * Used only as the type marker, not for comparisons. This - * indicates that the type uses a dynamically allocated type value - * that is provided in the `dynamic_type_marker()` static function. - */ - Dynamic, - } type; - - /** - * Constructor, takes the type of the subclass. - */ - Handle(Type type) : type(type) {} - ~Handle() - { - __clang_ignored_warning_push("-Watomic-alignment") __atomic_store_n( - reinterpret_cast<uint8_t *>(&type), 0, __ATOMIC_RELAXED); - __clang_ignored_warning_pop() - } - - public: /** * Unseal `unsafePointer` as a pointer to an object of the specified * type. Returns nullptr if `unsafePointer` is not a valid sealed @@ -106,6 +64,7 @@ { return static_cast<Handle *>(unsafePointer)->unseal_as<T>(); } + /** * Unseal this object as the specified type. Returns nullptr if this * is not a valid sealed object of the correct type. @@ -116,25 +75,18 @@ static_assert(std::is_base_of_v<Handle, T>, "Cannot down-cast something that is not a subclass " "of Handle"); - static_assert(offsetof(T, type) == 0, - "Type field must be at the start of the object"); - auto unsealed = compart_unseal(this); - if constexpr (T::TypeMarker == Type::Dynamic) + void *result; + if constexpr (IsDynamic) { - if (unsealed.is_valid() && uint32_t(unsealed->type) == - T::dynamic_type_marker().address()) - { - return unsealed.cast<T>(); - } + result = token_obj_unseal_dynamic(T::sealing_type(), + reinterpret_cast<SObj>(this)); } else { - if (unsealed.is_valid() && unsealed->type == T::TypeMarker) - { - return unsealed.cast<T>(); - } + result = token_obj_unseal_static(T::sealing_type(), + reinterpret_cast<SObj>(this)); } - return nullptr; + return static_cast<T *>(result); } }; @@ -191,151 +143,4 @@ void exception_entry_asm(void); __END_DECLS - /** - * Wrapper around `std::unique_ptr` for objects allocated with a specific - * heap capability. The scheduler is not authorised to allocate memory - * except on behalf of callers. - */ - template<typename T> - class HeapObject - { - /** - * Deleter for use with `std::unique_ptr`, for memory allocated with an - * explicit capability. - */ - class Deleter - { - /** - * The capability that should authorise access to the heap. - */ - struct SObjStruct *heapCapability; - - public: - /** - * Constructor, captures the capability to use for deallocation. - */ - __always_inline Deleter(struct SObjStruct *heapCapability) - : heapCapability(heapCapability) - { - } - - /** - * Apply function, calls the destructor and cleans up the underlying - * memory. - */ - __always_inline void operator()(T *object) - { - object->~T(); - heap_free(heapCapability, object); - } - }; - - protected: - /** - * The underlying unique pointer. - */ - std::unique_ptr<T, Deleter> pointer = {nullptr, nullptr}; - - public: - /// Default constructor, creates a null object. - HeapObject() = default; - /** - * Constructor for use with externally heap-allocated objects and - * placement new. Takes ownership of `allocatedObject`. The heap - * capability will be used to free the object on destruction. - */ - HeapObject(struct SObjStruct *heapCapability, T *allocatedObject) - : pointer(allocatedObject, heapCapability) - { - if (!__builtin_cheri_tag_get(allocatedObject)) - { - pointer = nullptr; - } - } - - /** - * Attempt to allocate memory for, and construct, an instance of `T` - * with the specified arguments. If memory allocation fails, this will - * construct an object wrapping a null pointer and not call the - * constructor. The bool-conversion operator can be used to check for - * success. - */ - template<typename... Args> - HeapObject(Timeout *timeout, - struct SObjStruct *heapCapability, - Args... args) - : pointer(static_cast<T *>( - heap_allocate(timeout, heapCapability, sizeof(T))), - {heapCapability}) - { - if (__builtin_cheri_tag_get(pointer.raw())) - { - new (pointer.get()) T(std::forward<Args>(args)...); - } - else - { - pointer = nullptr; - } - } - - /** - * Convert to bool. Returns true if the unique pointer owns a non-null - * pointer. - */ - operator bool() - { - return !!pointer; - } - - /** - * Returns the wrapped pointer without transferring ownership. - */ - T *get() - { - return pointer.get(); - } - - /** - * Returns the wrapped pointer, transferring ownership to the caller. - */ - T *release() - { - return pointer.release(); - } - }; - - /** - * Wrapper for a buffer of a dynamic length, allocated on the heap owned by - * another compartment. - */ - struct HeapBuffer : public HeapObject<char> - { - HeapBuffer() = default; - - /** - * Construct an array of `count` elemets, each of which is `size` bytes - * long, using `heapCapability` to authorise allocation. - */ - HeapBuffer(Timeout *timeout, - struct SObjStruct *heapCapability, - size_t size, - size_t count) - : HeapObject( - heapCapability, - static_cast<char *>( - heap_allocate_array(timeout, heapCapability, size, count))) - { - } - - /** - * Update the address of the wrapped pointer. - */ - void set_address(ptraddr_t address) - { - CHERI::Capability old{pointer.release()}; - old.address() = address; - pointer.reset(old); - } - }; - } // namespace
diff --git a/sdk/core/scheduler/main.cc b/sdk/core/scheduler/main.cc index 18c0d7b..678c784 100644 --- a/sdk/core/scheduler/main.cc +++ b/sdk/core/scheduler/main.cc
@@ -247,7 +247,9 @@ simulation_exit(1); for (;;) + { wfi(); + } } [[cheri::interrupt_state(disabled)]] TrustedStack * @@ -256,6 +258,15 @@ size_t mepc, size_t mtval) { + if constexpr (DebugScheduler) + { + /* Ensure that we got here from an IRQ-s deferred context */ + Capability returnAddress{__builtin_return_address(0)}; + Debug::Assert( + returnAddress.type() == CheriSealTypeReturnSentryDisabling, + "Scheduler exception_entry called from IRQ-enabled context"); + } + // The cycle count value the last time the scheduler returned. bool schedNeeded; if constexpr (Accounting) @@ -348,7 +359,7 @@ template<typename T> int typed_op(void *sealed, auto &&fn) { - auto *unsealed = Handle::unseal<T>(sealed); + auto *unsealed = T::template unseal<T>(sealed); // If we can't unseal the sealed capability and have it be of the // correct type then return an error. if (!unsealed) @@ -396,29 +407,6 @@ }); } - /** - * Return path from `*_create` functions. Performs a return check with - * interrupts disabled and stores the object managed by `object` via `ret` - * (consuming ownership) if `ret` has valid permissions. Returns the value - * that the caller should return to the public API. - * - * This function runs with interrupts disabled so that the majority of - * `*_create` can have them enabled. - */ - template<typename T> - [[cheri::interrupt_state(disabled)]] int write_result(void **ret, - HeapObject<T> &object) - { - if (!check_pointer<PermissionSet{Permission::Store, - Permission::LoadStoreCapability}>(ret)) - { - return -EINVAL; - } - - *ret = compart_seal(object.release()); - return 0; - } - } // namespace sched using namespace sched; @@ -594,7 +582,12 @@ return error; } - return write_result(reinterpret_cast<void **>(ret), mw); + // This can trap, but only if the caller has provided a bad pointer. + // In this case, the caller can leak memory, but only memory allocated + // against its own quota. + *reinterpret_cast<void **>(ret) = mw; + + return 0; } __cheriot_minimum_stack(0x70) int multiwaiter_delete( @@ -668,30 +661,19 @@ namespace { /** - * + * An interrupt capability. */ - struct InterruptCapability : Handle + struct InterruptCapability : Handle</*IsDynamic=*/false> { /** - * Type marker used by `Handle`, tells it to use the dynamic path. + * Sealing type used by `Handle`. */ - static constexpr auto TypeMarker = Handle::Type::Dynamic; - - /** - * Dynamic type marker used by `Handle`. - */ - static Capability<void> dynamic_type_marker() + static SKey sealing_type() { return STATIC_SEALING_TYPE(InterruptKey); } /** - * Padding for compatibility with the token layout. This can go away - * at some point. - */ - uint32_t padding; - - /** * The public structure state. */ InterruptCapabilityState state; @@ -702,8 +684,9 @@ 0x30) const uint32_t *interrupt_futex_get(struct SObjStruct *sealed) { STACK_CHECK(0x30); - auto *interruptCapability = Handle::unseal<InterruptCapability>(sealed); - uint32_t *result = nullptr; + auto *interruptCapability = + InterruptCapability::unseal<InterruptCapability>(sealed); + uint32_t *result = nullptr; if (interruptCapability && interruptCapability->state.mayWait) { InterruptController::master() @@ -722,7 +705,8 @@ 0x20) int interrupt_complete(struct SObjStruct *sealed) { STACK_CHECK(0x20); - auto *interruptCapability = Handle::unseal<InterruptCapability>(sealed); + auto *interruptCapability = + InterruptCapability::unseal<InterruptCapability>(sealed); if (interruptCapability && interruptCapability->state.mayComplete) { InterruptController::master().interrupt_complete(
diff --git a/sdk/core/scheduler/multiwait.h b/sdk/core/scheduler/multiwait.h index 23710dc..e1fdb5f 100644 --- a/sdk/core/scheduler/multiwait.h +++ b/sdk/core/scheduler/multiwait.h
@@ -101,13 +101,16 @@ /** * Multiwaiter object. This contains space for all of the triggers. */ - class MultiWaiterInternal : public Handle + class MultiWaiterInternal : public Handle</*IsDynamic*/ true> { public: /** - * Type marker used for `Handle::unseal_as`. + * Sealing type used by `Handle`. */ - static constexpr auto TypeMarker = Handle::Type::MultiWaiter; + static SKey sealing_type() + { + return STATIC_SEALING_TYPE(MultiWaiterKey); + } private: /** @@ -176,12 +179,13 @@ * Factory method. Creates a multiwaiter of the specified size. On * failure, sets `error` to the errno constant corresponding to the * failure reason and return `nullptr`. + * + * The result is a *sealed* multiwaiter handle. */ - static HeapObject<MultiWaiterInternal> - create(Timeout *timeout, - struct SObjStruct *heapCapability, - size_t length, - int &error) + static SObj create(Timeout *timeout, + struct SObjStruct *heapCapability, + size_t length, + int &error) { static_assert(sizeof(MultiWaiterInternal) <= 2 * sizeof(void *), "Header for event queue is too large"); @@ -190,17 +194,21 @@ error = -EINVAL; return {}; } - void *q = heap_allocate(timeout, - heapCapability, - sizeof(MultiWaiterInternal) + - (length * sizeof(EventWaiter))); - if (!__builtin_cheri_tag_get(q)) + void *memory = nullptr; + SObj sealed = token_sealed_unsealed_alloc( + timeout, + heapCapability, + sealing_type(), + sizeof(MultiWaiterInternal) + (length * sizeof(EventWaiter)), + &memory); + if (!memory) { error = -ENOMEM; - return {}; + return nullptr; } + new (memory) MultiWaiterInternal(length); error = 0; - return {heapCapability, new (q) MultiWaiterInternal(length)}; + return sealed; } /** @@ -369,9 +377,7 @@ /** * Private constructor, called only from the factory method (`create`). */ - MultiWaiterInternal(size_t length) : Handle(TypeMarker), Length(length) - { - } + MultiWaiterInternal(size_t length) : Length(length) {} /** * Priority-sorted wait queue for threads that are blocked on a
diff --git a/sdk/core/switcher/entry.S b/sdk/core/switcher/entry.S index 8ef9cca..387a2d1 100644 --- a/sdk/core/switcher/entry.S +++ b/sdk/core/switcher/entry.S
@@ -3,6 +3,7 @@ #include "export-table-assembly.h" #include "trusted-stack-assembly.h" +#include "misc-assembly.h" #include <errno.h> .include "assembly-helpers.s" @@ -27,6 +28,25 @@ #define SPILL_SLOT_pcc 24 #define SPILL_SLOT_SIZE 32 +/* + * The switcher uniformly speaks of registers using their RISC-V ELF psABI names + * and not their raw index, as, broadly speaking, we use registers in a similar + * way to C functions. However, it's probably convenient to have a mapping + * readily accessible, so here 'tis: + * + * # x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 + * psABI zero ra sp gp tp t0 t1 t2 s0 s1 a0 a1 a2 a3 a4 a5 + * + * When we use the psABI name without a 'c' prefix, we are sometimes meaning to + * refer to the address component of the capability. + * + * Despite the use of psABI names and conformance at the interface (argument + * registers used for arguments, return address register used for its canonical + * purpose, &c), one should not read too much of the psABI calling convention + * into the code here. Within the switcher, the machine is a raw register + * machine and C is a distant, high-level language. + */ + switcher_code_start: # Global for the sealing key. Stored in the switcher's code section. @@ -164,11 +184,19 @@ .p2align 2 .type __Z26compartment_switcher_entryz,@function __Z26compartment_switcher_entryz: - cincoffset csp, csp, -SPILL_SLOT_SIZE - csc cs0, SPILL_SLOT_cs0(csp) - csc cs1, SPILL_SLOT_cs1(csp) - csc cgp, SPILL_SLOT_cgp(csp) - csc cra, SPILL_SLOT_pcc(csp) + /* + * Spill caller-save registers carefully. If we find ourselves unable to do + * so, we'll return an error to the caller (via the exception path; see + * .Lhandle_error_in_switcher). The error handling path assumes that the + * first spill is to the lowest address and guaranteed to trap if any would. + */ + cincoffset ct2, csp, -SPILL_SLOT_SIZE +.Lswitcher_entry_first_spill: + csc cs0, SPILL_SLOT_cs0(ct2) + csc cs1, SPILL_SLOT_cs1(ct2) + csc cgp, SPILL_SLOT_cgp(ct2) + csc cra, SPILL_SLOT_pcc(ct2) + cmove csp, ct2 // before we access any privileged state, we can verify the // compartment's csp is valid. If not, force unwind. // Note that this check is purely to protect the callee, not the switcher @@ -207,7 +235,6 @@ // table entry on the trusted stack, a fault here will cause a forced // unwind until we set the correct one. csh s1, TrustedStack_offset_frameoffset(ct2) -#ifndef CONFIG_NO_SWITCHER_SAFETY // Chop off the stack. cgetaddr s0, csp cgetbase s1, csp @@ -219,25 +246,18 @@ // Read the stack high water mark (which is 16-byte aligned) csrr gp, CSR_MSHWM // Skip zeroing if high water mark >= stack pointer - bge t2, sp, after_zero + bge gp, sp, .Lafter_zero // Use stack high water mark as base address for zeroing. If this faults // then it will trigger a force unwind. This can happen only if the caller // is doing something bad. csetaddr ct2, csp, gp #endif zero_stack t2, s0, gp -after_zero: - // Reserve space for unwind state and so on. - cincoffset csp, csp, -STACK_ENTRY_RESERVED_SPACE -#ifdef CONFIG_MSHWM - // store new stack top as stack high water mark - csrw CSR_MSHWM, sp -#endif -#endif // CONFIG_NO_SWITCHER_SAFETY -.Lout: +.Lafter_zero: + // Fetch the sealing key LoadCapPCC cs0, compartment_switcher_sealing_key - li gp, 9 + li gp, SEAL_TYPE_SealedImportTableEntries csetaddr cs0, cs0, gp // The target capability is in ct1. Unseal, check tag and load the entry point offset. cunseal ct1, ct1, cs0 @@ -258,10 +278,25 @@ // At this point, we have already truncated the stack and so the length of // the stack is the length that the callee can use. cgetlen t2, csp - // Include the space we reserved for the unwind state. - addi t2, t2, -STACK_ENTRY_RESERVED_SPACE + /* + * Include the space we reserved for the unwind state. + * + * tp holds the number of required stack bytes, a value between 0 and 0x7F8 + * (the result of an unsigned byte load left shifted by 3). Given this + * extremely limited range, adding STACK_ENTRY_RESERVED_SPACE will not cause + * overflow (while instead subtracting it from the available length, in t2, + * might underflow). + */ + addi tp, tp, STACK_ENTRY_RESERVED_SPACE bgtu tp, t2, .Lstack_too_small + // Reserve space for unwind state and so on. + cincoffset csp, csp, -STACK_ENTRY_RESERVED_SPACE +#ifdef CONFIG_MSHWM + // store new stack top as stack high water mark + csrw CSR_MSHWM, sp +#endif + // Get the flags field into tp clbu tp, ExportEntry_offset_flags(ct1) cgetbase s1, ct1 @@ -298,19 +333,75 @@ csrsi mstatus, 0x8 .Lskip_interrupt_disable: // Registers passed to the callee are: - // c1 (ra), c2 (csp), and c3 (cgp) are passed unconditionally. + // cra (c1), csp (c2), and cgp (c3) are passed unconditionally. // ca0-ca5 (c10-c15) and ct0 (c5) are either passed as arguments or cleared // above. This should add up to 10 registers, with the remaining 5 being // cleared now: zeroRegisters tp, t1, t2, s0, s1 cjalr cra -.Lskip_compartment_call: + .globl switcher_skip_compartment_call +switcher_skip_compartment_call: // If we are doing a forced unwind of the trusted stack then we do almost // exactly the same as a normal unwind. We will jump here from the - // exception path. - cjal .Lpop_trusted_stack_frame - cmove cra, ca2 + // exception path (.Lforce_unwind) + + /* + * Pop a frame from the trusted stack, leaving all registers in the state + * expected by the caller of a cross-compartment call. The callee is + * responsible for zeroing argument and temporary registers. + * + * The below should not fault before returning back to the caller. If a + * fault occurs there must be a serious bug elsewhere. + */ + + cspecialr ctp, mtdc + clear_hazard_slots ctp, ct2 + // make sure there is a frame left in the trusted stack + clhu t2, TrustedStack_offset_frameoffset(ctp) + li tp, TrustedStack_offset_frames + // Move to the previous trusted stack frame. + addi t2, t2, -TrustedStackFrame_size + // If this is the first trusted stack frame, then the csp that we would be + // loading is the csp on entry, which does not have a spilled area. In + // this case, we would fault when loading, so would exit the thread, but we + // should instead gracefully exit the thread. + bgeu tp, t2, .Lcommon_defer_irqs_and_thread_exit + cspecialr ctp, mtdc + cincoffset ct1, ctp, t2 + // Restore the stack pointer. All other spilled values are spilled there. + clc csp, TrustedStackFrame_offset_csp(ct1) + // Update the current frame offset. + csh t2, TrustedStack_offset_frameoffset(ctp) + // Do the loads *after* moving the trusted stack pointer. In theory, the + // checks in `check_compartment_stack_integrity` make it impossible for + // this to fault, but if we do fault here then we'd end up in an infinite + // loop trying repeatedly to pop the same trusted stack frame. This would + // be bad. Instead, we move the trusted stack pointer *first* and so, if + // the accesses to the untrusted stack fault, we will detect a fault in the + // switcher, enter the force-unwind path, and pop the frame for the + // compartment that gave us a malicious csp. + clc cs0, SPILL_SLOT_cs0(csp) + clc cs1, SPILL_SLOT_cs1(csp) + clc cra, SPILL_SLOT_pcc(csp) + clc cgp, SPILL_SLOT_cgp(csp) + cincoffset csp, csp, SPILL_SLOT_SIZE +#ifdef CONFIG_MSHWM + // read the stack high water mark, which is 16-byte aligned + // we will use this as base address for stack clearing + // note that it cannot be greater than stack top as we + // we set it to stack top when we pushed to trusted stack frame + csrr tp, CSR_MSHWM +#else + cgetbase tp, csp +#endif + cgetaddr t1, csp + csetaddr ct2, csp, tp + zero_stack t2, t1, tp +#ifdef CONFIG_MSHWM + csrw CSR_MSHWM, sp +#endif + // Zero all registers apart from RA, GP, SP and return args. // cra, csp and cgp needed for the compartment // cs0 saved and restored on trusted stack @@ -318,6 +409,7 @@ // ca0, used for first return value // ca1, used for second return value zeroAllRegistersExcept ra, sp, gp, s0, s1, a0, a1 +.Ljust_return: cret // If the stack is too small, we don't do the call, but to avoid leaking @@ -327,7 +419,7 @@ .Lstack_too_small: li a0, -ENOTENOUGHSTACK li a1, 0 - j .Lskip_compartment_call + j switcher_skip_compartment_call .size compartment_switcher_entry, . - compartment_switcher_entry // the entry point of all exceptions and interrupts @@ -367,8 +459,8 @@ // csp now points to the save reg frame that we can use. // The guest csp (c2) is now in mtdc. Will be spilled later, but we - // spill all other registers now. - spillRegisters c1, cgp, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15 + // spill all the other 14 registers now. + spillRegisters cra, cgp, ctp, ct0, ct1, ct2, cs0, cs1, ca0, ca1, ca2, ca3, ca4, ca5 // If a thread has exited then it will set a fake value in the mcause so // that the scheduler knows not to try to resume it. @@ -398,7 +490,7 @@ // If we hit one of the exception conditions that we should let // compartments handle then deliver it to the compartment. // CHERI exception code. - li a0, 0x1c + li a0, MCAUSE_CHERI beq a0, t1, .Lhandle_error // Misaligned instruction, instruction access, illegal instruction, // breakpoint, misaligned load, load fault, misaligned store, and store @@ -443,7 +535,7 @@ // Switch onto the new thread's trusted stack LoadCapPCC ct0, compartment_switcher_sealing_key - li gp, 10 + li gp, SEAL_TYPE_SealedTrustedStacks csetaddr ct0, ct0, gp cunseal csp, ca0, ct0 clw t0, TrustedStack_offset_mcause(csp) @@ -457,11 +549,11 @@ // mret, so reentrancy is no longer a concern. cspecialw mtdc, csp - // If mcause is 25, then we will jump into the error handler: another - // thread has signalled that this thread should be interrupted. 25 is a - // reserved exception number that we repurpose to indicate explicit - // interruption. - li t1, 25 + // If mcause is MCAUSE_THREAD_INTERRUPT, then we will jump into the error + // handler: another thread has signalled that this thread should be + // interrupted. MCAUSE_THREAD_INTERRUPT is a reserved exception number that + // we repurpose to indicate explicit interruption. + li t1, MCAUSE_THREAD_INTERRUPT beq t0, t1, .Lhandle_injected_error // Environment call from M-mode is exception code 11. @@ -476,36 +568,27 @@ // ct2 to be the pcc to jump to. All other registers are in unspecified states // and will be overwritten when we install the context. .Linstall_context: - clw x1, TrustedStack_offset_mstatus(csp) - csrw mstatus, x1 + clw ra, TrustedStack_offset_mstatus(csp) + csrw mstatus, ra #ifdef CONFIG_MSHWM - clw x1, TrustedStack_offset_mshwm(csp) - csrw CSR_MSHWM, x1 - clw x1, TrustedStack_offset_mshwmb(csp) - csrw CSR_MSHWMB, x1 + clw ra, TrustedStack_offset_mshwm(csp) + csrw CSR_MSHWM, ra + clw ra, TrustedStack_offset_mshwmb(csp) + csrw CSR_MSHWMB, ra #endif cspecialw mepcc, ct2 - // c2 is csp, which will be loaded last and will overwrite the trusted - // stack pointer with the thread's stack pointer. - reloadRegisters c1, cgp, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, csp + // csp (c2) will be loaded last and will overwrite the trusted stack pointer + // with the thread's stack pointer. + reloadRegisters cra, cgp, ctp, ct0, ct1, ct2, cs0, cs1, ca0, ca1, ca2, ca3, ca4, ca5, csp mret // We are starting a forced unwind. This is reached either when we are unable // to run an error handler, or when we do run an error handler and it instructs // us to return. This treats all register values as undefined on entry. .Lforce_unwind: - // Pop the trusted stack frame. - cjal .Lpop_trusted_stack_frame - cmove cra, ca2 - // Zero all registers apart from RA, GP, SP and return args. - // cra, cs0, cs1, and cgp were restored from the compartment's stack - // csp restored from the trusted stack. - // ca0, used for first return value - // ca1, used for second return value - zeroAllRegistersExcept ra, sp, gp, s0, s1, a0, a1 li a0, -ECOMPARTMENTFAIL li a1, 0 - cret + j switcher_skip_compartment_call // If we have run out of trusted stack, then just restore the caller's state @@ -554,15 +637,15 @@ auipcc ct0, 0 clc ct1, TrustedStack_offset_mepcc(csp) cgetbase t0, ct0 - cgetbase t1, ct1 - beq t0, t1, .Lforce_unwind + cgetbase tp, ct1 + beq t0, tp, .Lhandle_error_in_switcher // Load the interrupted thread's stack pointer into ct0 clc ct0, TrustedStack_offset_csp(csp) // See if we can find a handler: clhu tp, TrustedStack_offset_frameoffset(csp) li t1, TrustedStack_offset_frames - beq tp, t1, .Lreset_mepcc_and_install_context + beq tp, t1, .Lset_mcause_and_exit_thread addi tp, tp, -TrustedStackFrame_size // ctp points to the current available trusted stack frame. @@ -618,22 +701,22 @@ // Get the previous trusted stack frame // Load the caller's csp - clc ca0, TrustedStackFrame_offset_csp(ctp) + clc ct0, TrustedStackFrame_offset_csp(ctp) // If this is the top stack frame, then the csp field is the value on // entry. If it's any other frame then we need to go to the previous one cincoffset cs1, csp, TrustedStack_offset_frames - beq s1, t1, .Lrecovered_stack + beq s1, tp, .Lrecovered_stack // The address of the stack pointer will point to the bottom of the // caller's save area, so we set the bounds to be the base up to the // current address. - cgetaddr a1, ca0 - cgetbase a2, ca0 + cgetaddr a1, ct0 + cgetbase a2, ct0 sub a1, a1, a2 - csetaddr ca0, ca0, a2 + csetaddr ct0, ct0, a2 // The code that installs the context expects csp to be in ct0 - csetboundsexact ct0, ca0, a1 + csetboundsexact ct0, ct0, a1 .Lrecovered_stack: li a0, 1 @@ -682,9 +765,9 @@ ccleartag cs1, cs1 csc cs1, 0(ct0) // Source for context copy. - cincoffset ca2, csp, TrustedStack_offset_c1 + cincoffset ca2, csp, TrustedStack_offset_cra // Destination for context copy - cincoffset ca3, ct0, TrustedStack_offset_c1 + cincoffset ca3, ct0, TrustedStack_offset_cra copyContext ca3, ca2, cs1, a4 // Set up the arguments for the call @@ -694,6 +777,9 @@ cmove csp, ca0 .Linvoke_error_handler: + // Enable interrupts before invoking the handler + csrsi mstatus, 0x8 + // Clear all registers except: // cra is set by cjalr. csp and cgp are needed for the called compartment. // ca0, used for the register state @@ -703,6 +789,15 @@ // Call the handler. cjalr cra + /* + * Now that we're back, defer interrupts again before we do anything that + * manipulates the TrustedStack. + * + * TODO: Eventually we'd like to move this down onto the paths where it + * actually matters and let most of this code run with IRQs enabled. + */ + csrci mstatus, 0x8 + // Move the return value to a register that will be cleared in a forced // unwind and zero the return registers. move s0, a0 @@ -747,9 +842,9 @@ csetaddr ct2, ct0, ra // Now copy everything else from the stack into the saved context // Source - cincoffset ca2, csp, TrustedStack_offset_c1 + cincoffset ca2, csp, TrustedStack_offset_cra // Destination - cincoffset ca3, ct1, TrustedStack_offset_c1 + cincoffset ca3, ct1, TrustedStack_offset_cra copyContext ca3, ca2, cs1, a4 // Increment the handler invocation count. We have now returned and // finished touching any data from the error handler that might cause a @@ -766,84 +861,55 @@ .Lhandle_injected_error: #ifdef CONFIG_MSHWM - clw x1, TrustedStack_offset_mshwm(csp) - csrw CSR_MSHWM, x1 - clw x1, TrustedStack_offset_mshwmb(csp) - csrw CSR_MSHWMB, x1 + clw ra, TrustedStack_offset_mshwm(csp) + csrw CSR_MSHWM, ra + clw ra, TrustedStack_offset_mshwmb(csp) + csrw CSR_MSHWMB, ra #endif j .Lhandle_error +.Lcommon_defer_irqs_and_thread_exit: + csrci mstatus, 0x8 + // Fall-through, now that IRQs are off // Value 24 is reserved for custom use. .Lset_mcause_and_exit_thread: - csrw mcause, 24 + csrw mcause, MCAUSE_THREAD_EXIT + // The thread exit code expects the trusted stack pointer to be in csp and + // the stack pointer to be in mtdc. After thread exit, we don't need the + // stack pointer so just put zero there. + zeroOne sp + cspecialrw csp, mtdc, csp j .Lthread_exit - // The continue-resume path expects the location that we will mret to to be - // in ct2. If we're just resuming, then resume from the stashed link - // register value. -.Lreset_mepcc_and_install_context: - clc ct2, TrustedStack_offset_mepcc(csp) + /* + * Some switcher instructions' traps are handled specially, by looking at + * the offset of mepcc. Otherwise, we're off to a force unwind. + */ +.Lhandle_error_in_switcher: + auipcc ctp, %cheriot_compartment_hi(.Lswitcher_entry_first_spill) + cincoffset ctp, ctp, %cheriot_compartment_lo_i(.Lhandle_error_in_switcher) + bne t1, tp, .Lforce_unwind + li a0, -ENOTENOUGHSTACK + li a1, 0 + + /* + * Cause the interrupted thread to resume as if a return had just executed. + * We do this by vectoring to a `cjalr ra` (`cret`) instruction through + * `mepcc`; whee! Overwrites the stored context a0 and a1 with the current + * values of those registers, effectively passing them through + * .Linstall_context. + */ +.Linstall_return_context: + auipcc ct2, %cheriot_compartment_hi(.Ljust_return) + cincoffset ct2, ct2, %cheriot_compartment_lo_i(.Linstall_return_context) + csc ca0, TrustedStack_offset_ca0(csp) + csc ca1, TrustedStack_offset_ca1(csp) j .Linstall_context + .size exception_entry_asm, . - exception_entry_asm -/** - * Pops a frame from the trusted stack. Leaves all registers in the state - * expected by the caller of a cross-compartment call, except for the return - * address which is left in ca2. The callee is responsible for zeroing - * argument and temporary registers. - */ -.Lpop_trusted_stack_frame: - // The below should not fault before returning back to the caller. If a fault occurs there must - // be a serious bug elsewhere. - cspecialr ctp, mtdc - clear_hazard_slots ctp, ct2 - // make sure there is a frame left in the trusted stack - clhu t2, TrustedStack_offset_frameoffset(ctp) - li tp, TrustedStack_offset_frames - bgeu tp, t2, .Lset_mcause_and_exit_thread - cspecialr ctp, mtdc - addi t2, t2, -TrustedStackFrame_size - cincoffset ct1, ctp, t2 - // Restore the stack pointer. All other spilled values are spilled there. - clc csp, TrustedStackFrame_offset_csp(ct1) - // Update the current frame offset. - csh t2, TrustedStack_offset_frameoffset(ctp) - // Do the loads *after* moving the trusted stack pointer. In theory, the - // checks in `check_compartment_stack_integrity` make it impossible for - // this to fault, but if we do fault here then we'd end up in an infinite - // loop trying repeatedly to pop the same trusted stack frame. This would - // be bad. Instead, we move the trusted stack pointer *first* and so, if - // the accesses to the untrusted stack fault, we will detect a fault in the - // switcher, enter the force-unwind path, and pop the frame for the - // compartment that gave us a malicious csp. - clc cs0, SPILL_SLOT_cs0(csp) - clc cs1, SPILL_SLOT_cs1(csp) - clc ca2, SPILL_SLOT_pcc(csp) - clc cgp, SPILL_SLOT_cgp(csp) - cincoffset csp, csp, SPILL_SLOT_SIZE -#ifndef CONFIG_NO_SWITCHER_SAFETY -#ifdef CONFIG_MSHWM - // read the stack high water mark, which is 16-byte aligned - // we will use this as base address for stack clearing - // note that it cannot be greater than stack top as we - // we set it to stack top when we pushed to trusted stack frame - csrr tp, CSR_MSHWM -#else - cgetbase tp, csp -#endif - cgetaddr t1, csp - csetaddr ct2, csp, tp - zero_stack t2, t1, tp -#ifdef CONFIG_MSHWM - csrw CSR_MSHWM, sp -#endif -#endif // CONFIG_NO_SWITCHER_SAFETY - cret - - - /******************************************************************************* * Switcher-exported library functions. * @@ -914,7 +980,7 @@ // Load the unsealing key into a register that we will clobber two // instructions later. LoadCapPCC ca1, compartment_switcher_sealing_key - li a2, 10 + li a2, SEAL_TYPE_SealedTrustedStacks csetaddr ca1, ca1, a2 // The target capability is in ct1. Unseal, check tag and load the entry point offset. cunseal ca1, ca0, ca1 @@ -960,7 +1026,7 @@ // Mark the thread as interrupted. // Store a magic value in mcause - li a2, 25 + li a2, MCAUSE_THREAD_INTERRUPT csw a2, TrustedStack_offset_mcause(ca1) // Return success li a0, 1 @@ -973,7 +1039,7 @@ .type __Z23switcher_current_threadv,@function __Z23switcher_current_threadv: LoadCapPCC ca0, compartment_switcher_sealing_key - li a1, 10 + li a1, SEAL_TYPE_SealedTrustedStacks csetaddr ca0, ca0, a1 cspecialr ca1, mtdc cseal ca0, ca1, ca0
diff --git a/sdk/core/switcher/misc-assembly.h b/sdk/core/switcher/misc-assembly.h new file mode 100644 index 0000000..f93293c --- /dev/null +++ b/sdk/core/switcher/misc-assembly.h
@@ -0,0 +1,43 @@ +// Copyright CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +#pragma once +#include <assembly-helpers.h> + +/* + * Constant to represent the raw permissions of the compartment CSP. We use + * this in the switcher, to verify the permissions of the CSP that comes from + * the compartment are exactly what we expect. + */ +EXPORT_ASSEMBLY_EXPRESSION(COMPARTMENT_STACK_PERMISSIONS, + (CHERI::PermissionSet{ + CHERI::Permission::Load, + CHERI::Permission::Store, + CHERI::Permission::LoadStoreCapability, + CHERI::Permission::LoadMutable, + CHERI::Permission::StoreLocal, + CHERI::Permission::LoadGlobal} + .as_raw()), + 0x7e) + +/** + * Space reserved at the top of a stack on entry to the compartment. + * + * This *must* be a multiple of 16, which is the stack alignment. + */ +#define STACK_ENTRY_RESERVED_SPACE 16 + +#ifdef __cplusplus +using namespace priv; +#endif + +EXPORT_ASSEMBLY_NAME(MCAUSE_THREAD_EXIT, 24) +EXPORT_ASSEMBLY_NAME(MCAUSE_THREAD_INTERRUPT, 25) +EXPORT_ASSEMBLY_NAME(MCAUSE_CHERI, 28) + +EXPORT_ASSEMBLY_EXPRESSION(SEAL_TYPE_SealedImportTableEntries, + SealingType::SealedImportTableEntries, + 9) +EXPORT_ASSEMBLY_EXPRESSION(SEAL_TYPE_SealedTrustedStacks, + SealingType::SealedTrustedStacks, + 10)
diff --git a/sdk/core/switcher/trusted-stack-assembly.h b/sdk/core/switcher/trusted-stack-assembly.h index 02fc248..7969662 100644 --- a/sdk/core/switcher/trusted-stack-assembly.h +++ b/sdk/core/switcher/trusted-stack-assembly.h
@@ -4,21 +4,21 @@ #pragma once #include <assembly-helpers.h> EXPORT_ASSEMBLY_OFFSET(TrustedStack, mepcc, 0 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c1, 1 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, cra, 1 * 8) EXPORT_ASSEMBLY_OFFSET(TrustedStack, csp, 2 * 8) EXPORT_ASSEMBLY_OFFSET(TrustedStack, cgp, 3 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c4, 4 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c5, 5 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c6, 6 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c7, 7 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c8, 8 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c9, 9 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c10, 10 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c11, 11 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c12, 12 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c13, 13 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c14, 14 * 8) -EXPORT_ASSEMBLY_OFFSET(TrustedStack, c15, 15 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, ctp, 4 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, ct0, 5 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, ct1, 6 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, ct2, 7 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, cs0, 8 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, cs1, 9 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca0, 10 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca1, 11 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca2, 12 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca3, 13 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca4, 14 * 8) +EXPORT_ASSEMBLY_OFFSET(TrustedStack, ca5, 15 * 8) EXPORT_ASSEMBLY_OFFSET(TrustedStack, hazardPointers, 16 * 8) EXPORT_ASSEMBLY_OFFSET(TrustedStack, mstatus, 17 * 8) EXPORT_ASSEMBLY_OFFSET(TrustedStack, mcause, (17 * 8) + 4) @@ -57,18 +57,3 @@ #define TSTACKOFFSET_FIRSTFRAME \ (TrustedStack_offset_frameoffset + TSTACK_HEADER_SZ) - -/* Constant to represent the raw permissions of the compartment CSP. - * We use this in the switcher, to verify the CSP comes from the - * compartment is exactly what we expect. - * This represents the following permissions: - * Load, Store, LoadStoreCapability, LoadMutable StoreLocal and LoadGlobal - */ -#define COMPARTMENT_STACK_PERMISSIONS 0x7e - -/** - * Space reserved at the top of a stack on entry to the compartment. - * - * This *must* be a multiple of 16, which is the stack alignment. - */ -#define STACK_ENTRY_RESERVED_SPACE 16
diff --git a/sdk/core/switcher/tstack.h b/sdk/core/switcher/tstack.h index 45af06e..dc8338b 100644 --- a/sdk/core/switcher/tstack.h +++ b/sdk/core/switcher/tstack.h
@@ -32,21 +32,21 @@ struct TrustedStackGeneric { void *mepcc; - void *c1; - void *csp; - void *cgp; - void *c4; - void *c5; - void *c6; - void *c7; - void *c8; - void *c9; - void *c10; - void *c11; - void *c12; - void *c13; - void *c14; - void *c15; + void *cra; // c1 + void *csp; // c2 + void *cgp; // c3 + void *ctp; // c4 + void *ct0; // c5 + void *ct1; // c6 + void *ct2; // c7 + void *cs0; // c8 + void *cs1; // c9 + void *ca0; // c10 + void *ca1; // c11 + void *ca2; // c12 + void *ca3; // c13 + void *ca4; // c14 + void *ca5; // c15 void *hazardPointers; size_t mstatus; size_t mcause; @@ -77,13 +77,3 @@ using TrustedStack = TrustedStackGeneric<0>; #include "trusted-stack-assembly.h" - -static_assert( - CheckSize<COMPARTMENT_STACK_PERMISSIONS, - CHERI::PermissionSet{CHERI::Permission::Load, - CHERI::Permission::Store, - CHERI::Permission::LoadStoreCapability, - CHERI::Permission::LoadMutable, - CHERI::Permission::StoreLocal, - CHERI::Permission::LoadGlobal} - .as_raw()>::value);
diff --git a/sdk/core/token_library/token_unseal.S b/sdk/core/token_library/token_unseal.S index 8391902..1dccc04 100644 --- a/sdk/core/token_library/token_unseal.S +++ b/sdk/core/token_library/token_unseal.S
@@ -2,24 +2,27 @@ #include <cheri-builtins.h> #include "../allocator/token.h" -/* - * An in-assembler implementation of + .hidden __sealingkey + .type __sealingkey,@object + .section .sealing_key1,"aw",@progbits + .globl __sealingkey + .p2align 3 +__sealingkey: + .chericap 0 + .size __sealingkey, 8 + + +.section .text,"ax",@progbits + +.p2align 1 + +/** + * The core of unsealing: * - * [[cheri::interrupt_state(disabled)]] void *__cheri_libcall - * token_obj_unseal(struct SKeyStruct *, struct SObjStruct *); - * - * The name has been manually mangled as per the C++ rules. + * void *token_unseal_internal(struct SKeyStruct *, struct SObjStruct *, int); */ - .section .text._Z16token_obj_unsealP10SKeyStructP10SObjStruct,"axG", \ - @progbits,_Z16token_obj_unsealP10SKeyStructP10SObjStruct,comdat - - .hidden _Z16token_obj_unsealP10SKeyStructP10SObjStruct - .globl _Z16token_obj_unsealP10SKeyStructP10SObjStruct - .p2align 1 - .type _Z16token_obj_unsealP10SKeyStructP10SObjStruct,@function - -_Z16token_obj_unsealP10SKeyStructP10SObjStruct: +.Ltoken_unseal_internal: /* * Register allocation: * @@ -28,9 +31,11 @@ * * - ca1 holds the user's sealed object pointer * - * - t0/ct0 holds a copy of the user key + * - a2 contains the expected sealing type. * - * - t1/ct1 is used within each local computation and never holds secrets + * - t0 holds a copy of the user key's address field (authorized type) + * + * - t1 is used within each local computation and never holds secrets */ /* Verify key tag */ @@ -48,8 +53,8 @@ andi t1, t1, CHERI_PERM_UNSEAL beqz t1, .Lexit_failure - /* Copy key capability to scratch register */ - cmove ct0, ca0 + /* Copy key type to scratch register */ + cgetaddr t0, ca0 /* * Load unsealing root capability, to be clobbered by return value @@ -59,6 +64,7 @@ .Lload_sealing_key: auipcc ca0, %cheriot_compartment_hi(__sealingkey) clc ca0, %cheriot_compartment_lo_i(.Lload_sealing_key)(ca0) + csetaddr ca0, ca0, a2 /* Unseal, clobbering authority */ cunseal ca0, ca1, ca0 @@ -73,17 +79,12 @@ */ clw t1, TokenSObj_offset_type(ca0) - /* - * Verify that the loaded value matches the address of the key (via as-integer - * access to capability register ct0). - */ + /* Verify that the loaded value matches the address of the key. */ bne t0, t1, .Lexit_failure /* Subset bounds to ->data */ // Get the top into t1 - cgetlen t1, ca0 - cgetbase t0, ca0 - add t1, t1, t0 + cgettop t1, ca0 // Move the address to the start of the data cincoffset ca0, ca0, TokenSObj_offset_data // Subtract the address of the (to-be-returned-unsealed) data from the top to @@ -101,8 +102,60 @@ cmove ca0, cnull cret +/** + * An in-assembler implementation of + * + * [[cheri::interrupt_state(disabled)]] void *__cheri_libcall + * token_obj_unseal(struct SKeyStruct *, struct SObjStruct *); + * + * The name has been manually mangled as per the C++ rules. + */ + .hidden _Z16token_obj_unsealP10SKeyStructP10SObjStruct + .globl _Z16token_obj_unsealP10SKeyStructP10SObjStruct +_Z16token_obj_unsealP10SKeyStructP10SObjStruct: + cgettype a2, ca1 + j .Ltoken_unseal_internal + +/** + * An in-assembler implementation of + * + * [[cheri::interrupt_state(disabled)]] void *__cheri_libcall + * token_obj_unseal_static(struct SKeyStruct *, struct SObjStruct *); + * + * The name has been manually mangled as per the C++ rules. + */ + .hidden _Z23token_obj_unseal_staticP10SKeyStructP10SObjStruct + .globl _Z23token_obj_unseal_staticP10SKeyStructP10SObjStruct +_Z23token_obj_unseal_staticP10SKeyStructP10SObjStruct: + li a2, CheriSealTypeStaticToken + j .Ltoken_unseal_internal + +/** + * An in-assembler implementation of + * + * [[cheri::interrupt_state(disabled)]] void *__cheri_libcall + * token_obj_unseal_dynamic(struct SKeyStruct *, struct SObjStruct *); + * + * The name has been manually mangled as per the C++ rules. + */ + .hidden _Z16token_obj_unsealP10SKeyStructP10SObjStruct + .globl _Z16token_obj_unsealP10SKeyStructP10SObjStruct +_Z24token_obj_unseal_dynamicP10SKeyStructP10SObjStruct: + li a2, CheriSealTypeAllocator + j .Ltoken_unseal_internal + /* TODO: Eventually this goes away, when the assembler can generate it for us */ CHERIOT_EXPORT_LIBCALL \ _Z16token_obj_unsealP10SKeyStructP10SObjStruct, \ 0 /* No stack usage */, \ 0b00010010 /* IRQs deferred, zero two registers */ + +CHERIOT_EXPORT_LIBCALL \ + _Z23token_obj_unseal_staticP10SKeyStructP10SObjStruct, \ + 0 /* No stack usage */, \ + 0b00010010 /* IRQs deferred, zero two registers */ + +CHERIOT_EXPORT_LIBCALL \ + _Z24token_obj_unseal_dynamicP10SKeyStructP10SObjStruct, \ + 0 /* No stack usage */, \ + 0b00010010 /* IRQs deferred, zero two registers */
diff --git a/sdk/firmware.ldscript.in b/sdk/firmware.ldscript.in index 9fc9a29..a9a3956 100644 --- a/sdk/firmware.ldscript.in +++ b/sdk/firmware.ldscript.in
@@ -46,6 +46,8 @@ scheduler_code : CAPALIGN { .scheduler_start = .; + *.scheduler.compartment(.compartment_sealing_keys); + .scheduler_import_start = .; *.scheduler.compartment(.compartment_import_table); .scheduler_import_end = .; *.scheduler.compartment(.text .text.* .rodata .rodata.* .data.rel.ro); @@ -55,6 +57,8 @@ allocator_code : CAPALIGN { .allocator_start = .; + */cheriot.allocator.compartment(.compartment_sealing_keys); + .allocator_import_start = .; */cheriot.allocator.compartment(.compartment_import_table); .allocator_import_end = .; allocator.compartment(.text .text.* .rodata .rodata.* .data.rel.ro); @@ -66,6 +70,8 @@ token_library_code : CAPALIGN { .token_library_start = .; + */cheriot.token_library.library(.compartment_sealing_keys); + .token_library_import_start = .; */cheriot.token_library.library(.compartment_import_table); .token_library_import_end = .; token_library.library(.text .text.* .rodata .rodata.* .data.rel.ro); @@ -156,6 +162,8 @@ LONG(.compartment_switcher_start); # Compartment switcher end SHORT(.compartment_switcher_end - .compartment_switcher_start); + # Cross-compartment call return path + SHORT(switcher_skip_compartment_call - .compartment_switcher_start); # Compartment switcher sealing key SHORT(compartment_switcher_sealing_key - .compartment_switcher_start); # Switcher's copy of the scheduler's PCC. @@ -178,8 +186,10 @@ LONG(.scheduler_globals); # Scheduler globals end size SHORT(SIZEOF(.scheduler_globals)); + # Start of the scheduler's import table + LONG(.scheduler_import_start); # Size of the scheduler import table - SHORT(.scheduler_import_end - .scheduler_start); + SHORT(.scheduler_import_end - .scheduler_import_start); # Address of scheduler export table LONG(.scheduler_export_table); # Size of the scheduler export table @@ -197,8 +207,10 @@ LONG(.allocator_globals); # Allocator globals end SHORT(SIZEOF(.allocator_globals)); + # Start of the allocator's import table + LONG(.allocator_import_start); # Size of the allocator import table - SHORT(.allocator_import_end - .allocator_start); + SHORT(.allocator_import_end - .allocator_import_start); # Address of allocator export table LONG(.allocator_export_table); # Size of the allocator export table @@ -215,8 +227,10 @@ # No data segment LONG(0); SHORT(0); + # Start of the token_library's import table + LONG(.token_library_import_start); # Size of the token server import table - SHORT(.token_library_import_end - .token_library_start); + SHORT(.token_library_import_end - .token_library_import_start); # Address of the token server export table LONG(.token_library_export_table); # Size of the token server export table @@ -232,7 +246,7 @@ # loader versions. # New versions of this can be generated with: # head /dev/random | shasum | cut -c 0-8 - LONG(0x6cef3879); + LONG(0xca2b63de); # Number of library headers. SHORT(@library_count@); # Number of compartment headers.
diff --git a/sdk/include/FreeRTOS-Compat/queue.h b/sdk/include/FreeRTOS-Compat/queue.h index 088d588..9b8f072 100644 --- a/sdk/include/FreeRTOS-Compat/queue.h +++ b/sdk/include/FreeRTOS-Compat/queue.h
@@ -8,17 +8,7 @@ /** * Queue handle. This is used to reference queues in the API functions. */ -typedef struct -{ - /** - * The real handle, holds pointers to the relevant elements of the queue. - */ - struct QueueHandle handle; - /** - * The pointer used to free the queue. - */ - void *freePointer; -} * QueueHandle_t; +typedef struct MessageQueue *QueueHandle_t; /** * Receive a message on a queue. The message is received into `buffer`, which @@ -34,7 +24,7 @@ xQueueReceive(QueueHandle_t queueHandle, void *buffer, TickType_t waitTicks) { struct Timeout timeout = {0, waitTicks}; - int rv = queue_receive(&timeout, &queueHandle->handle, buffer); + int rv = queue_receive(&timeout, queueHandle, buffer); if (rv == 0) return pdPASS; @@ -54,7 +44,7 @@ TickType_t waitTicks) { struct Timeout timeout = {0, waitTicks}; - int rv = queue_send(&timeout, &queueHandle->handle, buffer); + int rv = queue_send(&timeout, queueHandle, buffer); if (rv == 0) return pdPASS; @@ -86,24 +76,13 @@ static inline QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize) { - QueueHandle_t ret; + QueueHandle_t ret = NULL; struct Timeout timeout = {0, UnlimitedTimeout}; - ret = (QueueHandle_t)malloc(sizeof(*ret)); - if (!ret) - { - return NULL; - } int rc = queue_create(&timeout, MALLOC_CAPABILITY, - &ret->handle, - &ret->freePointer, + &ret, uxItemSize, uxQueueLength); - if (rc) - { - free(ret); - return NULL; - } return ret; } #endif @@ -113,8 +92,7 @@ */ static inline void vQueueDelete(QueueHandle_t xQueue) { - free(xQueue->freePointer); - free(xQueue); + queue_destroy(MALLOC_CAPABILITY, xQueue); } /** @@ -126,7 +104,7 @@ static inline UBaseType_t uxQueueMessagesWaiting(const QueueHandle_t xQueue) { size_t ret; - int rv = queue_items_remaining(&xQueue->handle, &ret); + int rv = queue_items_remaining(xQueue, &ret); assert(rv == 0);
diff --git a/sdk/include/assembly-helpers.h b/sdk/include/assembly-helpers.h index cdc92d7..73b0a68 100644 --- a/sdk/include/assembly-helpers.h +++ b/sdk/include/assembly-helpers.h
@@ -22,6 +22,25 @@ }; /** + * Export a macro into assembly named `name` with value `value`. In C++, this + * macro will report an error if the provided value does not equal the constexpr + * evaluation of `expression`. + */ +# define EXPORT_ASSEMBLY_NAME(name, val) \ + static_assert(CheckSize<name, val>::value, \ + "Value provided for assembly is incorrect"); + +/** + * Export a macro into assembly named `name` with value `value`. In C++, this + * macro will report an error if the provided value does not equal the constexpr + * evaluation of `expression`. + */ +# define EXPORT_ASSEMBLY_EXPRESSION(name, expression, val) \ + static constexpr size_t name = expression; \ + static_assert(CheckSize<name, val>::value, \ + "Value provided for assembly is incorrect"); + +/** * Export a macro into assembly of the form `{structure}_offset_{field}`. The * value of this macro will be `value`. In C++, this macro will report an error * if the provided value does not match the compiler's understanding of the @@ -61,13 +80,19 @@ static_assert(CheckSize<sizeof(structure), val>::value, \ "Size provided for assembly is incorrect"); #elif defined(__ASSEMBLER__) +# define EXPORT_ASSEMBLY_NAME(name, value) \ + .set name, value +# define EXPORT_ASSEMBLY_EXPRESSION(name, expression, value) \ + .set name, value # define EXPORT_ASSEMBLY_OFFSET_NAMED(structure, field, value, name) \ - .set name value + .set name, value # define EXPORT_ASSEMBLY_OFFSET(structure, field, value) \ .set structure##_offset_##field, value # define EXPORT_ASSEMBLY_SIZE(structure, value) .set structure##_size, value #else -# define EXPORT_ASSEMBLY_OFFSET(structure, field, name, value) +# define EXPORT_ASSEMBLY_NAME(name, value) +# define EXPORT_ASSEMBLY_EXPRESSION(name, expression, value) +# define EXPORT_ASSEMBLY_OFFSET(structure, field, name) # define EXPORT_ASSEMBLY_SIZE(structure, name, value) # define EXPORT_ASSEMBLY_OFFSET_NAMED(structure, field, value, name) #endif
diff --git a/sdk/include/cdefs.h b/sdk/include/cdefs.h index 2242e7e..0a91697 100644 --- a/sdk/include/cdefs.h +++ b/sdk/include/cdefs.h
@@ -61,7 +61,6 @@ #define __section(x) __attribute__((section(x))) #define __alloc_size(x) __attribute__((alloc_size(x))) #define __alloc_align(x) __attribute__((alloc_align(x))) -#define __cheri_callback __attribute__((cheri_ccallback)) #if __has_attribute(cheriot_minimum_stack) # define __cheriot_minimum_stack(x) __attribute__((cheriot_minimum_stack(x))) #else @@ -69,15 +68,34 @@ "cheriot_minimum_stack attribute not supported, please update your compiler" # define __cheriot_minimum_stack(x) #endif + // When running clang-tidy, we use the same compile flags for everything and so // will get errors about things being defined in the wrong compartment, so // define away the compartment name and pretend everything is local for now. -#ifdef CLANG_TIDY +#if defined(CLANG_TIDY) || defined(__CHERIOT_BAREMETAL__) # define __cheri_compartment(x) #else # define __cheri_compartment(x) __attribute__((cheri_compartment(x))) #endif -#define __cheri_libcall __attribute__((cheri_libcall)) + +// Define the CHERIoT calling-convention attributes macros to nothing if we're +// targeting bare metal and to the correct attributes if we're targeting the +// RTOS. +#ifdef __CHERIOT_BAREMETAL__ +# define __cheri_libcall +# define __cheri_callback +#else +# define __cheri_libcall __attribute__((cheri_libcall)) +# define __cheri_callback __attribute__((cheri_ccallback)) + +/** + * Define the symbol for the libcall that the compiler will expand the `strlen` + * builtin to. This builtin is used internally in libc++ (and possibly in + * other places) to avoid the namespace pollution from including `string.h` but + * is either constant folded in the front end or expanded to a libcall. + */ +unsigned __builtin_strlen(const char *str) __asm__("_Z6strlenPKc"); +#endif #define offsetof(a, b) __builtin_offsetof(a, b) @@ -107,14 +125,6 @@ # define __clang_ignored_warning_pop() #endif -/** - * Define the symbol for the libcall that the compiler will expand the `strlen` - * builtin to. This builtin is used internally in libc++ (and possibly in - * other places) to avoid the namespace pollution from including `string.h` but - * is either constant folded in the front end or expanded to a libcall. - */ -unsigned __builtin_strlen(const char *str) __asm__("_Z6strlenPKc"); - #if !defined(CLANG_TIDY) && !__has_builtin(__builtin_cheri_top_get) # error Your compiler is too old for this version of CHERIoT RTOS, please upgrade to a newer version #endif
diff --git a/sdk/include/cheri.h b/sdk/include/cheri.h index f7cc6bc..b10d059 100644 --- a/sdk/include/cheri.h +++ b/sdk/include/cheri.h
@@ -9,6 +9,359 @@ struct Timeout; /** + * The complete set of architectural permissions. + */ +enum CHERIPermission +{ + /** + * Capability refers to global memory (this capability may be stored + * anywhere). + */ + CheriPermissionGlobal = 0, + /** + * Global capabilities can be loaded through this capability. Without + * this permission, any capability loaded via this capability will + * have `Global` and `LoadGlobal` removed. + */ + CheriPermissionLoadGlobal = 1, + /** + * Capability may be used to store. Any store via a capability without + * this permission will trap. + */ + CheriPermissionStore = 2, + /** + * Capabilities with store permission may be loaded through this + * capability. Without this, any loaded capability will have + * `LoadMutable` and `Store` removed. + */ + CheriPermissionLoadMutable = 3, + /** + * This capability may be used to store capabilities that do not have + * `Global` permission. + */ + CheriPermissionStoreLocal = 4, + /** + * This capability can be used to load. + */ + CheriPermissionLoad = 5, + /** + * Any load and store permissions on this capability convey the right to + * load or store capabilities in addition to data. + */ + CheriPermissionLoadStoreCapability = 6, + /** + * If installed as the program counter capability, running code may + * access privileged system registers. + */ + CheriPermissionAccessSystemRegisters = 7, + /** + * This capability may be used as a jump target and used to execute + * instructions. + */ + CheriPermissionExecute = 8, + /** + * This capability may be used to unseal other capabilities. The + * 'address' range is in the sealing type namespace and not in the + * memory namespace. + */ + CheriPermissionUnseal = 9, + /** + * This capability may be used to seal other capabilities. The + * 'address' range is in the sealing type namespace and not in the + * memory namespace. + */ + CheriPermissionSeal = 10, + /** + * Software defined permission bit, no architectural meaning. + */ + CheriPermissionUser0 = 11 +}; + +/** + * The codes used in the cause field of the mtval CSR when the processor + * takes a CHERI exception. + */ +enum CHERICauseCode +{ + /** + * No exception. This value is passed to the error handler after a + * forced unwind in a called compartment. + */ + CheriCauseCodeNone = 0, + /** + * Attempted to use a capability outside its bounds. + */ + CheriCauseCodeBoundsViolation = 1, + /** + * Attempted to use an untagged capability to authorize something. + */ + CheriCauseCodeTagViolation = 2, + /** + * Attempted to use a sealed capability to authorize something. + */ + CheriCauseCodeSealViolation = 3, + /** + * Attempted to jump to a capability without `Permission::Execute`. + */ + CheriCauseCodePermitExecuteViolation = 0x11, + /** + * Attempted to load via a capability without `Permission::Load`. + */ + CheriCauseCodePermitLoadViolation = 0x12, + /** + * Attempted to store via a capability without `Permission::Store`. + */ + CheriCauseCodePermitStoreViolation = 0x13, + /** + * Attempted to store a tagged capability via a capability without + * `Permission::LoadStoreCapability`. + */ + CheriCauseCodePermitStoreCapabilityViolation = 0x15, + /** + * Attempted to store a tagged capability without `Permission::Global` + * via capability without `Permission::StoreLocal`. + */ + CheriCauseCodePermitStoreLocalCapabilityViolation = 0x16, + /** + * Attempted to access a restricted CSR or SCR with PCC without + * `Permission::AccessSystemRegisters`. + */ + CheriCauseCodePermitAccessSystemRegistersViolation = 0x18, + /** + * Used to represent a value that has no valid meaning in hardware. + */ + CheriCauseCodeInvalid = -1 +}; + +/** + * Register numbers as reported in cap idx field of `mtval` CSR when + * a CHERI exception is taken. Values less than 32 refer to general + * purpose registers and others to SCRs (of these, only PCC can actually + * cause an exception). + */ +enum CHERIRegisterNumber +{ + /** + * The zero register, which always contains the `NULL` capability. + */ + CheriRegisterNumberCzr = 0x0, + /** + * `$c1` / `$cra` used by the ABI as the return address. + * Not preserved across calls. + */ + CheriRegisterNumberCra = 0x1, + /** + * `$c2` / `$csp` used by the ABI as the stack pointer. + * Preserved across calls. + */ + CheriRegisterNumberCsp = 0x2, + /** + * `$c3` / `$cgp` used by the ABI as the global pointer. + * Not allocatable by the compiler, set by the switcher on compartment + * entry. + */ + CheriRegisterNumberCgp = 0x3, + /** + * `$c4` / `$ctp` used by the ABI as the thread pointer. + * Currently unused by the compiler. + * Not preserved across compartment calls. + */ + CheriRegisterNumberCtp = 0x4, + /** + * `$c5` / `$ct0` used by the ABI as temporary register. + * Not preserved across calls. + */ + CheriRegisterNumberCT0 = 0x5, + /** + * `$c6` / `$ct1` used by the ABI as temporary register. + * Not preserved across calls. + */ + CheriRegisterNumberCT1 = 0x6, + /** + * `$c7` / `$ct2` used by the ABI as temporary register. + * Not preserved across calls. + */ + CheriRegisterNumberCT2 = 0x7, + /** + * `$c8` / `$cs0` used by the ABI as a callee-saved register. + * Preserved across calls. + */ + CheriRegisterNumberCS0 = 0x8, + /** + * `$c9` / `$cs1` used by the ABI as a callee-saved register. + * Preserved across calls. + */ + CheriRegisterNumberCS1 = 0x9, + /** + * `$c10` / `$ca0` used by the ABI as an argument register. + * Not preserved across calls. + */ + CheriRegisterNumberCA0 = 0xa, + /** + * `$c11` / `$ca1` used by the ABI as an argument register. + * Not preserved across calls. + */ + CheriRegisterNumberCA1 = 0xb, + /** + * `$c12` / `$ca2` used by the ABI as an argument register. + * Not preserved across calls. + */ + CheriRegisterNumberCA2 = 0xc, + /** + * `$c13` / `$ca3` used by the ABI as an argument register. + * Not preserved across calls. + */ + CheriRegisterNumberCA3 = 0xd, + /** + * `$c14` / `$ca4` used by the ABI as an argument register. + * Not preserved across calls. + */ + CheriRegisterNumberCA4 = 0xe, + /** + * `$c15` / `$ca5` used by the ABI as an argument register. + * Not preserved across calls. + */ + CheriRegisterNumberCA5 = 0xf, + /** + * The Program Counter Capability. + * + * Special capability register used to authorize instruction fetch. The + * address is that of the faulting instruction. Also used for accessing + * read-only globals. + */ + CheriRegisterNumberPcc = 0x20, + /** + * Machine-mode Trap Code Capability. + * + * Special capability register that + * is installed in PCC when the CPU takes a trap. The address has the + * same semantics as the RISC-V `mtvec` CSR. Only accessible when PCC + * has the AccessSystemRegisters permission. + */ + CheriRegisterNumberMtcc = 0x3c, + /** + * Machine-mode Tusted Data Capability. + * + * Special capability register that contains the memory root capability + * on boot. Only accessible when PCC has the AccessSystemRegisters + * permission. Use by the RTOS to store a capability to the trusted + * stack. + */ + CheriRegisterNumberMtdc = 0x3d, + /** + * Machine-mode Scratch Capability. Special capabiltiy register that + * contains the sealing root capability on boot. Only accessible when + * PCC has the AccessSystemRegisters permission. + */ + CheriRegisterNumberMScratchC = 0x3e, + /** + * Machine-mode Exception Program Counter Capability. Special capability + * register that contains the PCC of the faulting instruction on trap. + * The address has the same semantics as the RISC-V `mepc` CSR. Only + * accessible when PCC has the AccessSystemRegisters permission. + */ + CheriRegisterNumberMepcc = 0x3f, + /** + * Indicates a value that is not used by the hardware to refer to a + * register. + */ + CheriRegisterNumberInvalid = -1 +}; + +/** + * Sealing types. + */ +enum CHERISealingType +{ + /** + * 0 represents unsealed. + */ + CheriSealTypeUnsealed = 0, + + /** + * Sentry that inherits interrupt status. + */ + CheriSealTypeSentryInheriting, + + /** + * Sentry that disables interrupts on calls. + */ + CheriSealTypeSentryDisabling, + + /** + * Sentry that enables interrupts on calls. + */ + CheriSealTypeSentryEnabling, + + /** + * Return sentry that disables interrupts on return + */ + CheriSealTypeReturnSentryDisabling, + + /** + * Return sentry that enables interrupts on return + */ + CheriSealTypeReturnSentryEnabling, + + /** + * Marker for the first sealing type that's valid for data capabilities. + */ + CheriSealTypeFirstDataSealingType = 9, + + /** + * The sealing type used for sealed export table entries. + * + * This is RTOS- and not CHERIoT-specific. + */ + CheriSealTypeSealedImportTableEntries = CheriSealTypeFirstDataSealingType, + + /** + * The compartment switcher has a sealing type for the trusted stack. + * + * This must be the second data sealing type so that we can also permit + * the switcher to unseal sentries and export table entries. + * + * This is RTOS- and not CHERIoT-specific. + */ + CheriSealTypeSealedTrustedStacks, + + /** + * The allocator has a sealing type for the software sealing mechanism + * with dynamically allocated objects. + * + * This is RTOS- and not CHERIoT-specific. + */ + CheriSealTypeAllocator, + + /** + * The loader reserves a sealing type for the software sealing + * mechanism. The permit-unseal capability for this is destroyed after + * the loader has run, which guarantees that anything sealed with this + * type was present in the original firmware image. The token library + * has the only permit-unseal capability for this type. + * + * This is RTOS- and not CHERIoT-specific. + */ + CheriSealTypeStaticToken, + + /** + * The first sealing key that is reserved for use by the allocator's + * software sealing mechanism and used for static sealing types, + * + * Architecturally, this is the smallest non-interpreted sealing type. + */ + CheriSealTypeFirstStaticSoftware = 16, + + /** + * The first sealing key in the space that the allocator will + * dynamically allocate for sealing types. + * + * This is RTOS- and not CHERIoT-specific. + */ + CheriSealTypeFirstDynamicSoftware = 0x1000000 +}; + +/** * Checks that `ptr` is valid, unsealed, has at least `rawPermissions`, and has * at least `space` bytes after the current offset. *
diff --git a/sdk/include/cheri.hh b/sdk/include/cheri.hh index 8e18db3..4663e17 100644 --- a/sdk/include/cheri.hh +++ b/sdk/include/cheri.hh
@@ -22,64 +22,64 @@ * Capability refers to global memory (this capability may be stored * anywhere). */ - Global = 0, + Global = CheriPermissionGlobal, /** * Global capabilities can be loaded through this capability. Without * this permission, any capability loaded via this capability will * have `Global` and `LoadGlobal` removed. */ - LoadGlobal = 1, + LoadGlobal = CheriPermissionLoadGlobal, /** * Capability may be used to store. Any store via a capability without * this permission will trap. */ - Store = 2, + Store = CheriPermissionStore, /** * Capabilities with store permission may be loaded through this * capability. Without this, any loaded capability will have * `LoadMutable` and `Store` removed. */ - LoadMutable = 3, + LoadMutable = CheriPermissionLoadMutable, /** * This capability may be used to store capabilities that do not have * `Global` permission. */ - StoreLocal = 4, + StoreLocal = CheriPermissionStoreLocal, /** * This capability can be used to load. */ - Load = 5, + Load = CheriPermissionLoad, /** * Any load and store permissions on this capability convey the right to * load or store capabilities in addition to data. */ - LoadStoreCapability = 6, + LoadStoreCapability = CheriPermissionLoadStoreCapability, /** * If installed as the program counter capability, running code may * access privileged system registers. */ - AccessSystemRegisters = 7, + AccessSystemRegisters = CheriPermissionAccessSystemRegisters, /** * This capability may be used as a jump target and used to execute * instructions. */ - Execute = 8, + Execute = CheriPermissionExecute, /** * This capability may be used to unseal other capabilities. The * 'address' range is in the sealing type namespace and not in the * memory namespace. */ - Unseal = 9, + Unseal = CheriPermissionUnseal, /** * This capability may be used to seal other capabilities. The * 'address' range is in the sealing type namespace and not in the * memory namespace. */ - Seal = 10, + Seal = CheriPermissionSeal, /** * Software defined permission bit, no architectural meaning. */ - User0 = 11 + User0 = CheriPermissionUser0 }; /** @@ -361,6 +361,30 @@ }; /** + * Rounds `len` up to a CHERI representable length for the current + * architecture. + */ + __always_inline inline size_t representable_length(size_t length) + { + return __builtin_cheri_round_representable_length(length); + } + + /** + * Returns the alignment mask required for a given length. + */ + __always_inline inline size_t representable_alignment_mask(size_t length) + { + return __builtin_cheri_representable_alignment_mask(length); + } + + /// Can the range [base, base + size) be precisely covered by a capability? + inline bool is_precise_range(ptraddr_t base, size_t size) + { + return (base & ~representable_alignment_mask(size)) == 0 && + representable_length(size) == size; + } + + /** * Helper class for accessing capability properties on pointers. */ template<typename T> @@ -390,12 +414,12 @@ */ class PropertyProxyBase { + protected: /** * The capability that this proxy refers to. */ Capability ∩ - protected: /** * Replaces the underlying capability */ @@ -521,6 +545,68 @@ set(__builtin_cheri_bounds_set(ptr(), bounds)); return *this; } + + private: + BoundsProxy &set_inexact_at_most_slow(size_t bounds) + { + ptraddr_t newBaseAddress = this->cap.address(); + + // The number of bits in CHERIoT's capability encoding's + // mantissa. This is part of the capability encoding and + // so, ideally, wouldn't be hard coded here. + static constexpr size_t MantissaBits = 9; + + // The maximum possible representable length given the new + // base is a full mantissa width of 1s followed by 0s with + // its least significant 1 aligned to the least significant + // 1 in the base address. + size_t maximumLength = ((1 << MantissaBits) - 1) + << __builtin_ctz(newBaseAddress); + + // Ensure that the requested length is representable by + // making sure that it fits within a mantissa width, + // rounding down by dropping any lower bits. This might be + // excessive by up to one bit position, because the + // representable alignment mask is designed to work with the + // rounding-up inexact bounds setting instruction. As a result, + // we might not return the largest possible representable + // length, but we won't return a wildly too small one, either. + size_t alignedLength = + bounds & representable_alignment_mask(bounds); + + // Select the smaller of those two lengths. + bounds = std::min<size_t>(alignedLength, maximumLength); + *this = bounds; + return *this; + } + + public: + /** + * Set the bounds to `length` if `length` is representable with the + * current alignment of `buffer`. If not, then select a smaller + * `length` that is representable. Unlike set_inexact(), the + * resulting base will always be the current address; that is, there + * will be no padding below the current address. + * + * The caller must call .length() on the resulting capability to + * determine the imposed bounds. + * + * See is_precise_range(). + */ + __always_inline BoundsProxy &set_inexact_at_most(size_t bounds) + { + // Just try to set the requested bounds, first. If that works, + // there's no need for bit-twiddling at all. + Capability p = ptr(); + p.bounds() = bounds; + if (p.is_valid()) + { + set(static_cast<T *>(p)); + return *this; + } + + return set_inexact_at_most_slow(bounds); + } }; /** @@ -1020,30 +1106,6 @@ }; /** - * Rounds `len` up to a CHERI representable length for the current - * architecture. - */ - __always_inline inline size_t representable_length(size_t length) - { - return __builtin_cheri_round_representable_length(length); - } - - /** - * Returns the alignment mask required for a given length. - */ - __always_inline inline size_t representable_alignment_mask(size_t length) - { - return __builtin_cheri_representable_alignment_mask(length); - } - - /// Can the range [base, base + size) be precisely covered by a capability? - inline bool is_precise_range(ptraddr_t base, size_t size) - { - return (base & ~representable_alignment_mask(size)) == 0 && - representable_length(size) == size; - } - - /** * Concept that matches pointers. */ template<typename T> @@ -1169,50 +1231,53 @@ * No exception. This value is passed to the error handler after a * forced unwind in a called compartment. */ - None = 0, + None = CheriCauseCodeNone, /** * Attempted to use a capability outside its bounds. */ - BoundsViolation = 1, + BoundsViolation = CheriCauseCodeBoundsViolation, /** * Attempted to use an untagged capability to authorize something. */ - TagViolation = 2, + TagViolation = CheriCauseCodeTagViolation, /** * Attempted to use a sealed capability to authorize something. */ - SealViolation = 3, + SealViolation = CheriCauseCodeSealViolation, /** * Attempted to jump to a capability without `Permission::Execute`. */ - PermitExecuteViolation = 0x11, + PermitExecuteViolation = CheriCauseCodePermitExecuteViolation, /** * Attempted to load via a capability without `Permission::Load`. */ - PermitLoadViolation = 0x12, + PermitLoadViolation = CheriCauseCodePermitLoadViolation, /** * Attempted to store via a capability without `Permission::Store`. */ - PermitStoreViolation = 0x13, + PermitStoreViolation = CheriCauseCodePermitStoreViolation, /** * Attempted to store a tagged capability via a capability without * `Permission::LoadStoreCapability`. */ - PermitStoreCapabilityViolation = 0x15, + PermitStoreCapabilityViolation = + CheriCauseCodePermitStoreCapabilityViolation, /** * Attempted to store a tagged capability without `Permission::Global` * via capability without `Permission::StoreLocal`. */ - PermitStoreLocalCapabilityViolation = 0x16, + PermitStoreLocalCapabilityViolation = + CheriCauseCodePermitStoreLocalCapabilityViolation, /** * Attempted to access a restricted CSR or SCR with PCC without * `Permission::AccessSystemRegisters`. */ - PermitAccessSystemRegistersViolation = 0x18, + PermitAccessSystemRegistersViolation = + CheriCauseCodePermitAccessSystemRegistersViolation, /** * Used to represent a value that has no valid meaning in hardware. */ - Invalid = -1 + Invalid = CheriCauseCodeInvalid, }; /** @@ -1226,84 +1291,84 @@ /** * The zero register, which always contains the `NULL` capability. */ - CZR = 0x0, + CZR = CheriRegisterNumberCzr, /** * `$c1` / `$cra` used by the ABI as the return address. * Not preserved across calls. */ - CRA = 0x1, + CRA = CheriRegisterNumberCra, /** * `$c2` / `$csp` used by the ABI as the stack pointer. * Preserved across calls. */ - CSP = 0x2, + CSP = CheriRegisterNumberCsp, /** * `$c3` / `$cgp` used by the ABI as the global pointer. * Not allocatable by the compiler, set by the switcher on compartment * entry. */ - CGP = 0x3, + CGP = CheriRegisterNumberCgp, /** * `$c4` / `$ctp` used by the ABI as the thread pointer. * Currently unused by the compiler. * Not preserved across compartment calls. */ - CTP = 0x4, + CTP = CheriRegisterNumberCtp, /** * `$c5` / `$ct0` used by the ABI as temporary register. * Not preserved across calls. */ - CT0 = 0x5, + CT0 = CheriRegisterNumberCT0, /** * `$c6` / `$ct1` used by the ABI as temporary register. * Not preserved across calls. */ - CT1 = 0x6, + CT1 = CheriRegisterNumberCT1, /** * `$c7` / `$ct2` used by the ABI as temporary register. * Not preserved across calls. */ - CT2 = 0x7, + CT2 = CheriRegisterNumberCT2, /** * `$c8` / `$cs0` used by the ABI as a callee-saved register. * Preserved across calls. */ - CS0 = 0x8, + CS0 = CheriRegisterNumberCS0, /** * `$c9` / `$cs1` used by the ABI as a callee-saved register. * Preserved across calls. */ - CS1 = 0x9, + CS1 = CheriRegisterNumberCS1, /** * `$c10` / `$ca0` used by the ABI as an argument register. * Not preserved across calls. */ - CA0 = 0xa, + CA0 = CheriRegisterNumberCA0, /** * `$c11` / `$ca1` used by the ABI as an argument register. * Not preserved across calls. */ - CA1 = 0xb, + CA1 = CheriRegisterNumberCA1, /** * `$c12` / `$ca2` used by the ABI as an argument register. * Not preserved across calls. */ - CA2 = 0xc, + CA2 = CheriRegisterNumberCA2, /** * `$c13` / `$ca3` used by the ABI as an argument register. * Not preserved across calls. */ - CA3 = 0xd, + CA3 = CheriRegisterNumberCA3, /** * `$c14` / `$ca4` used by the ABI as an argument register. * Not preserved across calls. */ - CA4 = 0xe, + CA4 = CheriRegisterNumberCA4, /** * `$c15` / `$ca5` used by the ABI as an argument register. * Not preserved across calls. */ - CA5 = 0xf, + CA5 = CheriRegisterNumberCA5, /** * The Program Counter Capability. * @@ -1311,7 +1376,7 @@ * address is that of the faulting instruction. Also used for accessing * read-only globals. */ - PCC = 0x20, + PCC = CheriRegisterNumberPcc, /** * Machine-mode Trap Code Capability. * @@ -1320,7 +1385,7 @@ * same semantics as the RISC-V `mtvec` CSR. Only accessible when PCC * has the AccessSystemRegisters permission. */ - MTCC = 0x3c, + MTCC = CheriRegisterNumberMtcc, /** * Machine-mode Tusted Data Capability. * @@ -1329,25 +1394,25 @@ * permission. Use by the RTOS to store a capability to the trusted * stack. */ - MTDC = 0x3d, + MTDC = CheriRegisterNumberMtdc, /** * Machine-mode Scratch Capability. Special capabiltiy register that * contains the sealing root capability on boot. Only accessible when * PCC has the AccessSystemRegisters permission. */ - MScratchC = 0x3e, + MScratchC = CheriRegisterNumberMScratchC, /** * Machine-mode Exception Program Counter Capability. Special capability * register that contains the PCC of the faulting instruction on trap. * The address has the same semantics as the RISC-V `mepc` CSR. Only * accessible when PCC has the AccessSystemRegisters permission. */ - MEPCC = 0x3f, + MEPCC = CheriRegisterNumberMepcc, /** * Indicates a value that is not used by the hardware to refer to a * register. */ - Invalid = -1 + Invalid = CheriRegisterNumberInvalid, }; /**
diff --git a/sdk/include/compartment-macros.h b/sdk/include/compartment-macros.h index ef0a024..ed728f1 100644 --- a/sdk/include/compartment-macros.h +++ b/sdk/include/compartment-macros.h
@@ -142,16 +142,6 @@ */ #define DEVICE_EXISTS(x) defined(DEVICE_EXISTS_##x) -#define SEALING_CAP() \ - ({ \ - void *ret; \ - __asm("1: " \ - " auipcc %0, %%cheriot_compartment_hi(__sealingkey)\n" \ - " clc %0, %%cheriot_compartment_lo_i(1b)(%0)\n" \ - : "=C"(ret)); \ - ret; \ - }) - /** * Helper macro, used by `STATIC_SEALING_TYPE`. Do not use this directly, it * exists to avoid error-prone copying and pasting of the mangled name for a
diff --git a/sdk/include/compartment.h b/sdk/include/compartment.h index 6113c83..00079dc 100644 --- a/sdk/include/compartment.h +++ b/sdk/include/compartment.h
@@ -4,52 +4,18 @@ #pragma once #include <cdefs.h> #include <compartment-macros.h> +#include <stddef.h> #include <stdint.h> #ifdef __cplusplus # include <cheri.hh> -template<typename T> -static inline CHERI::Capability<T> compart_seal(T *in) -{ - void *key = SEALING_CAP(); - - return CHERI::Capability{in}.seal(key); -} - -template<typename T> -static inline CHERI::Capability<T> compart_unseal(T *in) -{ - void *key = SEALING_CAP(); - - return CHERI::Capability{in}.unseal(key); -} - -template<typename T> -static inline auto compart_unseal(void *in) -{ - return compart_unseal(static_cast<T *>(in)); -} -#else -# include <cheri-builtins.h> -static inline void *compart_seal(void *in) -{ - void *key = SEALING_CAP(); - - return cseal(in, key); -} - -static inline void *compart_unseal(void *in) -{ - void *key = SEALING_CAP(); - - return cunseal(in, key); -} #endif /** * State for error handlers to use. * - * Note: This structure should have the same layout as the register-save area. + * Note: This structure should have the same layout as the register-save area + * (that is, the initial sequence of a TrustedStack, up through ca5, inclusive). */ struct ErrorState {
diff --git a/sdk/include/fail-simulator-on-error.h b/sdk/include/fail-simulator-on-error.h index d26dceb..cad8bd7 100644 --- a/sdk/include/fail-simulator-on-error.h +++ b/sdk/include/fail-simulator-on-error.h
@@ -42,56 +42,36 @@ { if (mcause == priv::MCAUSE_CHERI) { + // An unexpected error -- log it and end the simulation with error. + // Note: handle CZR differently as `get_register_value` will return a + // nullptr which we cannot dereference. + auto [exceptionCode, registerNumber] = CHERI::extract_cheri_mtval(mtval); - // The thread entry point is called with a NULL return address so the - // cret at the end of the entry point function will trap if it is - // reached. We don't want to treat this as an error but thankfully we - // detect it quite specifically by checking for all of: - // 1) CHERI cause is tag violation - // 2) faulting register is CRA - // 3) value of CRA is NULL - // 4) we've reached the top of the thread's stack - CHERI::Capability stackCapability{ - frame->get_register_value<CHERI::RegisterNumber::CSP>()}; - CHERI::Capability returnCapability{ - frame->get_register_value<CHERI::RegisterNumber::CRA>()}; - // The top of the stack is 16 bytes above the stack pointer on entry, - // to provide space for unwind lists and so on. - if (registerNumber == CHERI::RegisterNumber::CRA && - returnCapability.address() == 0 && - exceptionCode == CHERI::CauseCode::TagViolation && - (stackCapability.top() - 16) == stackCapability.address()) - { - // looks like thread exit -- just log it then ForceUnwind - DebugErrorHandler::log( - "Thread exit CSP={}, PCC={}", stackCapability, frame->pcc); - } - else - { - // An unexpected error -- log it and end the simulation - // with error. Note: handle CZR differently as - // `get_register_value` will return a nullptr which we - // cannot dereference. - DebugErrorHandler::log( - "{} error at {} (return address: {}), with capability register " - "{}: {}", - exceptionCode, - frame->pcc, - frame->get_register_value<CHERI::RegisterNumber::CRA>(), - registerNumber, - registerNumber == CHERI::RegisterNumber::CZR - ? nullptr - : *frame->get_register_value(registerNumber)); - simulation_exit(1); - } + + DebugErrorHandler::log( + "{} error at {} (return address: {}), with capability register " + "{}: {}", + exceptionCode, + frame->pcc, + frame->get_register_value<CHERI::RegisterNumber::CRA>(), + registerNumber, + registerNumber == CHERI::RegisterNumber::CZR + ? nullptr + : *frame->get_register_value(registerNumber)); } else { // other error (e.g. __builtin_trap causes ReservedInstruciton) // log and end simulation with error. DebugErrorHandler::log("Unhandled error {} at {}", mcause, frame->pcc); - simulation_exit(1); } + + simulation_exit(1); + /* + * simulation_exit may fail (say, we're not on a simulator or there isn't + * enough stack space to invoke the function. In that case, just fall back + * to forcibly unwinding. + */ return ErrorRecoveryBehaviour::ForceUnwind; }
diff --git a/sdk/include/platform/sunburst/platform-uart.hh b/sdk/include/platform/sunburst/platform-uart.hh index a9acf85..09f217d 100644 --- a/sdk/include/platform/sunburst/platform-uart.hh +++ b/sdk/include/platform/sunburst/platform-uart.hh
@@ -4,6 +4,10 @@ #include_next <platform-uart.hh> #pragma pop_macro("CHERIOT_PLATFORM_CUSTOM_UART") +#ifndef DEFAULT_UART_BAUD_RATE +# define DEFAULT_UART_BAUD_RATE 921'600 +#endif + /** * OpenTitan UART * @@ -128,6 +132,23 @@ ControlTransmitEnable = 1 << 0, }; + /// Status Register Fields + enum : uint32_t + { + /// Receive FIFO is empty. + StatusReceiveEmpty = 1 << 5, + /// Receive logic is idle. + StatusReceiveIdle = 1 << 4, + /// Transmit FIFO is empty and all bits have been transmitted. + StatusTransmitIdle = 1 << 3, + /// Transmit FIFO is empty; transmission may still be occurring. + StatusTransmitEmpty = 1 << 2, + /// Receive FIFO is full. + StatusReceiveFull = 1 << 1, + /// Transmit FIFO is full. + StatusTransmitFull = 1 << 0, + }; + /// The encoding for different transmit watermark levels. enum class TransmitWatermark { @@ -181,8 +202,8 @@ /// Clears the contents of the receive and transmit FIFOs. void fifos_clear() volatile { - fifoCtrl = (fifoCtrl & ~0b11) | FifoControlTransmitReset | - FifoControlReceiveReset; + fifoCtrl = + fifoCtrl | FifoControlTransmitReset | FifoControlReceiveReset; } /** @@ -204,7 +225,7 @@ */ void receive_watermark(ReceiveWatermark level) volatile { - fifoCtrl = static_cast<uint32_t>(level) << 5 | (fifoCtrl & 0b11100011); + fifoCtrl = static_cast<uint32_t>(level) << 2 | (fifoCtrl & 0b11100011); } /// Enable the given interrupt. @@ -219,7 +240,7 @@ interruptEnable = interruptEnable & ~interrupt; } - void init(unsigned baudRate = 115'200) volatile + void init(unsigned baudRate = DEFAULT_UART_BAUD_RATE) volatile { // Nco = 2^20 * baud rate / cpu frequency const uint32_t Nco = @@ -240,12 +261,12 @@ bool can_write() volatile { - return transmit_fifo_level() < 32; + return !(status & StatusTransmitFull); } bool can_read() volatile { - return receive_fifo_level() > 0; + return !(status & StatusReceiveEmpty); } /**
diff --git a/sdk/include/platform/sunburst/platform-usbdev.hh b/sdk/include/platform/sunburst/platform-usbdev.hh new file mode 100644 index 0000000..1a279bc --- /dev/null +++ b/sdk/include/platform/sunburst/platform-usbdev.hh
@@ -0,0 +1,681 @@ +// SPDX-FileCopyrightText: CHERIoT contributors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +#include <cdefs.h> +#include <optional> +#include <stdint.h> +#include <utils.hh> + +/** + * A driver for OpenTitan USB Device, which is used in the Sonata system. + * + * This peripheral's source and documentation can be found at: + * https://github.com/lowRISC/opentitan/tree/ab878b5d3578939a04db72d4ed966a56a869b2ed/hw/ip/usbdev + * + * With rendered register documentation served at: + * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html + * + * An incredibly brief overview of how the USB device and it's buffers: + * Data packet ingress and egress goes via a pool of 64 byte buffers living + * in a 2kB SRAM packet buffer, which is accessable as a large MMIO region. + * The software manages these buffers using buffer IDs as references. + * IDs are pushed or popped into different FIFOs by either the software or + * device depending on whether they contain a packet to be sent, are available + * for a packet to be received into them, or contain a packet that has been + * received. + * + * See https://opentitan.org/book/hw/ip/usbdev/doc/programmers_guide.html for + * more information. + */ +class OpenTitanUsbdev : private utils::NoCopyNoMove +{ + public: + /// Supported sizes for the USB Device. + static constexpr uint8_t MaxPacketLength = 64u; + static constexpr uint8_t BufferCount = 32u; + static constexpr uint8_t MaxEndpoints = 12u; + + /** + * The offset from the start of the USB Device MMIO region at which + * packet buffer memory begins. + */ + static constexpr uint32_t BufferStartAddress = 0x800u; + + /// Device Registers + uint32_t interruptState; + uint32_t interruptEnable; + uint32_t interruptTest; + uint32_t alertTest; + uint32_t usbControl; + uint32_t endpointOutEnable; + uint32_t endpointInEnable; + uint32_t usbStatus; + uint32_t availableOutBuffer; + uint32_t availableSetupBuffer; + uint32_t receiveBuffer; + /// Register to enable receive SETUP transactions + uint32_t receiveEnableSetup; + /// Register to enable receive OUT transactions + uint32_t receiveEnableOut; + /// Register to set NAK (Not/Negated Acknowledge) after OUT transactions + uint32_t setNotAcknowledgeOut; + /// Register showing ACK receival to indicate a successful IN send + uint32_t inSent; + /// Registers for controlling the stalling of OUT and IN endpoints + uint32_t outStall; + uint32_t inStall; + /** + * IN transaction configuration registers. There is one register per + * endpoint for the USB device. + */ + uint32_t configIn[MaxEndpoints]; + /** + * Registers for configuring which endpoints should be treated as + * isochronous endpoints. This means that if the corresponding bit is set, + * then that no handshake packet will be sent for an OUT/IN transaction on + * that endpoint. + */ + uint32_t outIsochronous; + uint32_t inIsochronous; + /// Registers for configuring if endpoints data toggle on transactions + uint32_t outDataToggle; + uint32_t inDataToggle; + + private: + /** + * Registers to sense/drive the USB PHY pins. That is, these registers can + * be used to respectively read out the state of the USB device inputs and + * outputs, or to control the inputs and outputs from software. These + * registers are kept private as they are intended to be used for debugging + * purposes or during chip testing, and not in actual software. + */ + [[maybe_unused]] uint32_t phyPinsSense; + [[maybe_unused]] uint32_t phyPinsDrive; + + public: + /// Config register for the USB PHY pins. + uint32_t phyConfig; + + /// Interrupt definitions for OpenTitan's USB Device. + enum class UsbdevInterrupt : uint32_t + { + /// Interrupt asserted whilst the receive FIFO (buffer) is not empty. + PacketReceived = 1u << 0, + /** + * Interrupt asserted when a packet was sent as part of an IN + * transaction, but not cleared from the `inSent` register. + */ + PacketSent = 1u << 1, + /** + * Interrupt raised when VBUS (power supply) is lost, i.e. the link to + * the USB host controller has been disconnected. + */ + Disconnected = 1u << 2, + /** + * Interrupt raised when the link is active, but a Start of Frame (SOF) + * packet has not been received within a given timeout threshold, which + * is set to 4.096 milliseconds. + */ + HostLost = 1u << 3, + /** + * Interrupt raised when a Bus Reset condition is indicated on the link + * by the link being held in an SE0 state (Single Ended Zero, both lines + * being pulled low) for longer than 3 microseconds. + */ + LinkReset = 1u << 4, + /** + * Interrupt raised when the link has entered the suspend state, due to + * being idle for more than 3 milliseconds. + */ + LinkSuspend = 1u << 5, + /// Interrupt raised on link transition from suspended to non-idle. + LinkResume = 1u << 6, + /// Interrupt asserted whilst the Available OUT buffer is empty. + AvailableOutEmpty = 1u << 7, + /// Interrupt asserted whilst the Receive buffer is full. + ReceiveFull = 1u << 8, + /** + * Interrupt raised when the Available OUT buffer or the Available SETUP + * buffer overflows. + */ + AvailableBufferOverflow = 1u << 9, + /// Interrupt raised when an error occurs during an IN transaction. + LinkInError = 1u << 10, + /** + * Interrupt raised when a CRC (cyclic redundancy check) error occurs on + * a received packet; i.e. there was an error in transmission. + */ + RedundancyCheckError = 1u << 11, + /// Interrupt raised when an invalid Packet Identifier is received. + PacketIdentifierError = 1u << 12, + /// Interrupt raised when a bit stuffing violation is detected. + BitstuffingError = 1u << 13, + /** + * Interrupt raised when the USB frame number is updated with a valid + * SOF (Start of Frame) packet. + */ + FrameUpdated = 1u << 14, + /// Interrupt raised when VBUS (power supply) is detected. + Powered = 1u << 15, + /// Interrupt raised when an error occurs during an OUT transaction. + LinkOutError = 1u << 16, + /// Interrupt asserted whilst the Available SETUP buffer is empty. + AvailableSetupEmpty = 1u << 17, + }; + + /** + * Definitions of fields (and their locations) for the USB Control register + * (offset 0x10). + * + * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#usbctrl + */ + enum class UsbControlField : uint32_t + { + Enable = 1u << 0, + ResumeLinkActive = 1u << 1, + DeviceAddress = 0x7Fu << 16, + }; + + /** + * Definitions of fields (and their locations) for the USB Status register + * (offset 0x1c). + * + * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#usbstat + */ + enum class UsbStatusField : uint32_t + { + Frame = 0x7FFu << 0, + HostLost = 1u << 11, + LinkState = 0x7u << 12, + Sense = 1u << 15, + AvailableOutDepth = 0xFu << 16, + AvailableSetupDepth = 0x7u << 20, + AvailableOutFull = 1u << 23, + ReceiveDepth = 0xFu << 24, + AvailableSetupFull = 1u << 30, + ReceiveEmpty = 1u << 31, + }; + + /** + * Definitions of fields (and their locations) for the Receive FIFO + * buffer register (offset 0x28). + * + * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#rxfifo + */ + enum class ReceiveBufferField : uint32_t + { + BufferId = 0x1Fu << 0, + Size = 0x7Fu << 8, + Setup = 1u << 19, + EndpointId = 0xFu << 20, + }; + + /** + * Definitions of fields (and their locations) for a Config In register + * (where there is one such register for each endpoint). These are + * the registers with offsets 0x44 up to (and not including) 0x74. + * + * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#configin + */ + enum class ConfigInField : uint32_t + { + BufferId = 0x1Fu << 0, + Size = 0x7Fu << 8, + Sending = 1u << 29, + Pending = 1u << 30, + Ready = 1u << 31, + }; + + /** + * Definitions of fields (and their locations) for the PHY Config + * Register (offset 0x8c). + * + * https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#phy_config + */ + enum class PhyConfigField : uint32_t + { + UseDifferentialReceiver = 1u << 0, + // Other PHY Configuration fields are omitted. + }; + + /** + * Ensure that the Available OUT and Available SETUP buffers are kept + * supplied with buffers for packet reception. + * + * @param bufferBitmap A bitmap of the buffers that are not currently + * committed (where 1 corresponds to not in use). + * @returns The updated bitmap after supplying buffers. + */ + [[nodiscard]] uint64_t supply_buffers(uint64_t bufferBitmap) volatile + { + constexpr uint32_t SetupFullBit = + uint32_t(UsbStatusField::AvailableSetupFull); + constexpr uint32_t OutFullBit = + uint32_t(UsbStatusField::AvailableOutFull); + + for (uint8_t index = 0; index < BufferCount; index++) + { + const uint32_t Buffer = (1u << index); + if (!(bufferBitmap & Buffer)) + { + continue; // Skip buffers that are not available + } + + // If a buffer is available, and either Available SETUP or OUT are + // not yet full, then commit that buffer and mark it as in use. + if (usbStatus & SetupFullBit) + { + if (usbStatus & OutFullBit) + { + break; // Both are full - stop trying to supply buffers. + } + availableOutBuffer = index; + } + else + { + availableSetupBuffer = index; + } + bufferBitmap &= ~Buffer; + } + return bufferBitmap; + } + + /** + * Enable a specified interrupt / interrupts. + */ + void interrupt_enable(UsbdevInterrupt interrupt) volatile + { + interruptEnable = interruptEnable | uint32_t(interrupt); + } + + /** + * Disable a specified interrupt / interrupts. + */ + void interrupt_disable(UsbdevInterrupt interrupt) volatile + { + interruptEnable = interruptEnable & ~uint32_t(interrupt); + } + + /** + * Initialise the USB device, ensuring that packet buffers are available for + * reception and that the PHY has been appropriately configured. Note that + * at this stage, endpoints have not been configured and the device has not + * been connected to the USB. + * + * @param bufferBitmap An out-parameter, to initialise a bitmap of the + * buffers that are not currently commited (1 corresponds to not in use). + * + * @returns 0 if initialisation is sucessful, and non-zero otherwise. + */ + [[nodiscard]] int init(uint64_t &bufferBitmap) volatile + { + bufferBitmap = supply_buffers((uint64_t(1u) << BufferCount) - 1u); + phyConfig = uint32_t(PhyConfigField::UseDifferentialReceiver); + return 0; + } + + /** + * Set up the configuration of an OUT endpoint for the USB device. + * + * @param endpointId The ID of the OUT endpoint to configure. + * @param enabled Whether the OUT endpoint should be enabled or not. + * @param setup Whether SETUP transactions should be enabled for the + * endpoint. + * @param isochronous Whether the endpoint should operate isochronously or + * non-isochronously. + * + * @returns 0 if configuration is successful, and non-zero otherwise. + */ + [[nodiscard]] int out_endpoint_configure(uint8_t endpointId, + bool enabled, + bool setup, + bool isochronous) volatile + { + if (endpointId >= MaxEndpoints) + { + return -1; + } + const uint32_t Mask = 1u << endpointId; + endpointOutEnable = (endpointOutEnable & ~Mask) | (enabled ? Mask : 0u); + outIsochronous = (outIsochronous & ~Mask) | (isochronous ? Mask : 0u); + receiveEnableSetup = (receiveEnableSetup & ~Mask) | (setup ? Mask : 0u); + receiveEnableOut = (receiveEnableOut & ~Mask) | (enabled ? Mask : 0u); + return 0; + } + + /** + * Set up the configuration of an IN endpoint for the USB device. + * + * @param endpointId The ID of the IN endpoint to configure + * @param enabled Whether the IN endpoint should be enabled or not. + * @param isochronous Whether the endpoint should operate isochronously or + * non-isochronously. + * + * @returns 0 if configuration is successful, and non-zero otherwise. + */ + [[nodiscard]] int in_endpoint_configure(uint8_t endpointId, + bool enabled, + bool isochronous) volatile + { + if (endpointId >= MaxEndpoints) + { + return -1; + } + const uint32_t Mask = 1u << endpointId; + endpointInEnable = (endpointInEnable & ~Mask) | (enabled ? Mask : 0u); + inIsochronous = (inIsochronous & ~Mask) | (isochronous ? Mask : 0u); + return 0; + } + + /** + * Set the STALL state of a specified endpoint pair (both IN and OUT). + * + * @param endpointId The ID of the endpoint pair to modify. + * @param stalling Whether the endpoints are stalling or not. + * + * @returns 0 if successful, and non-zero otherwise. + */ + [[nodiscard]] int endpoint_stalling_set(uint8_t endpointId, + bool stalling) volatile + { + if (endpointId >= MaxEndpoints) + { + return -1; + } + const uint32_t Mask = 1u << endpointId; + outStall = (outStall & ~Mask) | (stalling ? Mask : 0u); + inStall = (inStall & ~Mask) | (stalling ? Mask : 0u); + return 0; + } + + /** + * Connect the device to the USB, indicating its presence to the USB host + * controller. Endpoints must already have been configured at this point + * because traffic may be received imminently. + * + * @returns 0 if successful, and non-zero otherwise. + * @returns -1 if endpoint 0 isn't enabled, + * suggesting the endpoints haven't been configured. + */ + [[nodiscard]] int connect() volatile + { + if (!(endpointInEnable & endpointOutEnable & 0b1)) + { + return -1; + } + usbControl = usbControl | uint32_t(UsbControlField::Enable); + return 0; + } + + /** + * Disconnect the device from the USB. + * + * @returns 0 if successful, and non-zero otherwise. + */ + void disconnect() volatile + { + usbControl = usbControl & ~uint32_t(UsbControlField::Enable); + } + + /** + * Check whether the USB device is connected (i.e. pullup enabled). + * + * @returns True to indicate it is connected, and false otherwise. + */ + [[nodiscard]] bool connected() volatile + { + return (usbControl & uint32_t(UsbControlField::Enable)); + } + + /** + * Set the device address on the USB; this address will have been supplied + * by the USB host controller in the standard `SET_ADDRESS` Control + * Transfer. + * + * @param address The device address to set on the USB. + * + * @returns 0 if successful, and non-zero otherwise. + */ + [[nodiscard]] int device_address_set(uint8_t address) volatile + { + if (address >= 0x80) + { + return -1; // Device addresses are only 7 bits long. + } + constexpr uint32_t Mask = uint32_t(UsbControlField::DeviceAddress); + usbControl = (usbControl & ~Mask) | (address << 16); + return 0; + } + + /** + * Check and retrieve the endpoint and buffer numbers of a + * recently-collected IN data packet. The caller is responsible for reusing + * or releasing the buffer. + * + * @param endpointId An out-parameter, to which the ID of the endpoint for + * a recently-collected IN data packet will be written. + * @param bufferId An out-parameter, to which the ID of the buffer for a + * recently-collected IN data packet will be written. + * + * @returns 0 if successful, and non-zero otherwise. + */ + [[nodiscard]] int retrieve_collected_packet(uint8_t &endpointId, + uint8_t &bufferId) volatile + { + constexpr uint32_t BufferIdMask = uint32_t(ConfigInField::BufferId); + uint32_t sent = inSent; + + // Clear the first encountered packet sent indication. + for (endpointId = 0; endpointId < MaxEndpoints; endpointId++) + { + const uint32_t EndpointBit = 1u << endpointId; + if (sent & EndpointBit) + { + // Clear the `in_sent` bit for this specific endpoint, and + // indicate which buffer has been released. + inSent = EndpointBit; + bufferId = (configIn[endpointId] & BufferIdMask); + return 0; + } + } + + // If no packet sent indications were found, then fail. + return -1; + } + + /** + * Present a packet on the specified IN endpoint for collection by the USB + * host controller. + * + * @param bufferId The buffer to use to store the packet. + * @param endpointId The IN endpoint used to send the packet. + * @param data The packet to be transmitted. + * @param size The size of the packet. + */ + void packet_send(uint8_t bufferId, + uint8_t endpointId, + const uint32_t *data, + uint8_t size) volatile + { + // Transmission of zero length packets is common over USB + if (size > 0) + { + usbdev_transfer(buffer(bufferId), data, size, true); + } + + constexpr uint32_t ReadyBit = uint32_t(ConfigInField::Ready); + configIn[endpointId] = bufferId | (size << 8); + configIn[endpointId] = configIn[endpointId] | ReadyBit; + } + + /// The information associated with a received packet + struct ReceiveBufferInfo + { + uint32_t info; + /// The endpoint ID the received packet was received on + constexpr uint8_t endpoint_id() + { + return (info & uint32_t(ReceiveBufferField::EndpointId)) >> 20; + } + /// The size of the received packet + constexpr uint16_t size() + { + return (info & uint32_t(ReceiveBufferField::Size)) >> 8; + } + /// Whether the received packet was a setup packet + constexpr bool is_setup() + { + return (info & uint32_t(ReceiveBufferField::Setup)) != 0; + } + /// The buffer ID used to store the received packet + constexpr uint8_t buffer_id() + { + return (info & uint32_t(ReceiveBufferField::BufferId)) >> 0; + } + }; + + /** + * If a packet has been received, removes the packet's buffer from the + * receive FIFO giving it's information and ownership to the user. + * + * `packet_data_get` can be used to retrieve the packet's data. + * + * @returns Information about the received packet, if a packet had been + * received. + */ + [[nodiscard]] std::optional<ReceiveBufferInfo> packet_take() volatile + { + if (!(usbStatus & uint32_t(UsbStatusField::ReceiveDepth))) + { + return {}; // No packets received + } + return ReceiveBufferInfo{receiveBuffer}; + } + + /** + * Retrieves the data from a buffer containing a received packet. + * + * @param destination A destination buffer to read the packet's data into. + */ + void packet_data_get(ReceiveBufferInfo bufferInfo, + uint32_t *destination) volatile + { + const auto [id, size] = + std::pair{bufferInfo.buffer_id(), bufferInfo.size()}; + // Reception of Zero Length Packets occurs in the Status Stage of IN + // Control Transfers. + if (size > 0) + { + usbdev_transfer(destination, buffer(id), size, false); + } + } + + private: + /** + * Return a pointer to the given offset within the USB device register + * space; this is used to access the packet buffer memory. + * + * @param bufferId The buffer number to access the packet buffer memory for + * + * @returns A pointer to the buffer's memory. + */ + uint32_t *buffer(uint8_t bufferId) volatile + { + const uint32_t Offset = BufferStartAddress + bufferId * MaxPacketLength; + const uintptr_t Address = reinterpret_cast<uintptr_t>(this) + Offset; + return const_cast<uint32_t *>(reinterpret_cast<uint32_t *>(Address)); + } + + /** + * Perform a transfer to or from packet buffer memory. This function is + * hand-optimised to perform a faster, unrolled, word-based data transfer + * for efficiency. + * + * @param destination A pointer to transfer the source data to. + * @param source A pointer to the data to be transferred. + * @param size The size of the data pointed to by `source`. + * @param toDevice True if the transfer is to the device (e.g. when sending + * a packet), and False if not (e.g. when receiving a packet). + */ + static void usbdev_transfer(uint32_t *destination, + const uint32_t *source, + uint8_t size, + bool toDevice) + { + // Unroll word transfer. Each word transfer is 4 bytes, so we must round + // to the closest multiple of (4 * words) when unrolling. + constexpr uint8_t UnrollFactor = 4u; + constexpr uint32_t UnrollMask = (UnrollFactor * 4u) - 1; + + // Round down to the previous multiple for unrolling + const uint32_t UnrollSize = (size & ~UnrollMask); + const uint32_t *sourceEnd = reinterpret_cast<uint32_t *>( + reinterpret_cast<uintptr_t>(source) + UnrollSize); + + // This is manulally unrolled for two reasons: + // 1. We can't do partial writes to the USB packet buffer, + // which memcpy will attempt and causes a BUS fault. + // 2. In the sonata system at the time of writing, + // the core clock is 40MHz compared to the USB device's 48MHz. + // This approach was found to be significantly faster than when + // left to compiler to optimisation. + // + // Ensure the unrolling here matches `UnrollFactor`. + while (source < sourceEnd) + { + destination[0] = source[0]; + destination[1] = source[1]; + destination[2] = source[2]; + destination[3] = source[3]; + destination += UnrollFactor; + source += UnrollFactor; + } + + // Copy the remaining whole words. + for (size &= UnrollMask; size >= UnrollFactor; size -= UnrollFactor) + { + *destination++ = *source++; + } + if (size == 0) + { + return; + } + + // Copy trailing tail bytes, as USBDEV only supports 32-bit accesses. + if (toDevice) + { + // Collect final bytes into a word. + const volatile uint8_t *trailingBytes = + reinterpret_cast<const volatile uint8_t *>(source); + uint32_t partialWord = trailingBytes[0]; + if (size > 1) + { + partialWord |= trailingBytes[1] << 8; + } + if (size > 2) + { + partialWord |= trailingBytes[2] << 16; + } + // Write the final word to the device. + *destination = partialWord; + } + else + { + volatile uint8_t *destinationBytes = + reinterpret_cast<volatile uint8_t *>(destination); + // Collect the final word from the device. + const uint32_t TrailingBytes = *source; + // Unpack it into final bytes. + destinationBytes[0] = static_cast<uint8_t>(TrailingBytes); + if (size > 1) + { + destinationBytes[1] = static_cast<uint8_t>(TrailingBytes >> 8); + } + if (size > 2) + { + destinationBytes[2] = static_cast<uint8_t>(TrailingBytes >> 16); + } + } + } +};
diff --git a/sdk/include/priv/riscv.h b/sdk/include/priv/riscv.h index d5c616e..d8cc51c 100644 --- a/sdk/include/priv/riscv.h +++ b/sdk/include/priv/riscv.h
@@ -75,6 +75,7 @@ constexpr size_t MCAUSE_LOAD_PAGE_FAULT = 13; constexpr size_t MCAUSE_STORE_PAGE_FAULT = 15; constexpr size_t MCAUSE_THREAD_EXIT = 24; + constexpr size_t MCAUSE_THREAD_INTERRUPT = 25; constexpr size_t MCAUSE_CHERI = 28; constexpr size_t MSTATUS_UIE = (1 << 0);
diff --git a/sdk/include/queue.h b/sdk/include/queue.h index dc16e72..68743da 100644 --- a/sdk/include/queue.h +++ b/sdk/include/queue.h
@@ -31,50 +31,71 @@ #include <timeout.h> /** - * A handle to a queue endpoint. + * Structure representing a queue. This structure represents the queue + * metadata, the buffer is stored at the end. * - * Dropping permissions can make this a receive-only or a send-only handle. + * A queue is a ring buffer of fixed-sized elements with a producer and consumer + * counter. */ -struct QueueHandle +struct MessageQueue { /** - * The size of one element in this queue. + * The size of one element in this queue. This should not be modified after + * construction. */ size_t elementSize; /** - * The size of the queue. + * The size of the queue. This should not be modified after construction. */ size_t queueSize; /** - * The buffer used for storing queue elements. - */ - void *buffer; - /** * The producer counter. */ - _Atomic(uint32_t) *producer; + _Atomic(uint32_t) producer; /** * The consumer counter. */ - _Atomic(uint32_t) *consumer; + _Atomic(uint32_t) consumer; +#ifdef __cplusplus + MessageQueue(size_t elementSize, size_t queueSize) + : elementSize(elementSize), queueSize(queueSize) + { + } +#endif }; +_Static_assert(sizeof(struct MessageQueue) % sizeof(void *) == 0, + "MessageQueue structure must end correctly aligned for storing " + "capabilities."); + __BEGIN_DECLS /** + * Returns the allocation size needed for a queue with the specified number and + * size of elements. This can be used to statically allocate queues. + * + * Returns the allocation size on success, or `-EINVAL` if the arguments would + * cause an overflow. + */ +ssize_t __cheri_libcall queue_allocation_size(size_t elementSize, + size_t elementCount); + +/** * Allocates space for a queue using `heapCapability` and stores a handle to it - * via `outQueue`. The underlying allocation (which is necessary to free the - * queue) is returned via `outAllocation`. + * via `outQueue`. * * The queue is has space for `elementCount` entries. Each entry is a fixed * size, `elementSize` bytes. + * + * Returns 0 on success, `-ENOMEM` on allocation failure, and `-EINVAL` if the + * arguments are invalid (for example, if the requested number of elements + * multiplied by the element size would overflow). */ -int __cheri_libcall queue_create(Timeout *timeout, - struct SObjStruct *heapCapability, - struct QueueHandle *outQueue, - void **outAllocation, - size_t elementSize, - size_t elementCount); +int __cheri_libcall queue_create(Timeout *timeout, + struct SObjStruct *heapCapability, + struct MessageQueue **outQueue, + size_t elementSize, + size_t elementCount); /** * Destroys a queue. This wakes up all threads waiting to produce or consume, @@ -87,29 +108,8 @@ * Returns 0 on success. On failure, returns `-EPERM` if the queue handle is * restricted (see comment above). */ -int __cheri_libcall queue_destroy(struct SObjStruct *heapCapability, - struct QueueHandle *handle); - -/** - * Convert a queue handle returned from `queue_create` into one that can be - * used *only* for receiving. - * - * Note: This is primarily defence in depth. A malicious holder of this queue - * handle can still set the consumer counter to invalid values. - */ -struct QueueHandle __cheri_libcall -queue_make_receive_handle(struct QueueHandle handle); - -/** - * Convert a queue handle returned from `queue_create` into one that can be - * used *only* for sending. - * - * Note: This is primarily defence in depth. A malicious holder of this queue - * handle can still set the producer counter to invalid values and overwrite - * arbitrary queue locations. - */ -struct QueueHandle __cheri_libcall -queue_make_send_handle(struct QueueHandle handle); +int __cheri_libcall queue_destroy(struct SObjStruct *heapCapability, + struct MessageQueue *handle); /** * Send a message to the queue specified by `handle`. This expects to be able @@ -122,9 +122,9 @@ * This expected to be called with a valid queue handle. It does not validate * that this is correct. It uses `safe_memcpy` and so will check the buffer. */ -int __cheri_libcall queue_send(Timeout *timeout, - struct QueueHandle *handle, - const void *src); +int __cheri_libcall queue_send(Timeout *timeout, + struct MessageQueue *handle, + const void *src); /** * Receive a message over a queue specified by `handle`. This expects to be @@ -135,9 +135,9 @@ * Returns 0 on success, `-ETIMEOUT` if the timeout was exhausted, `-EINVAL` on * invalid arguments. */ -int __cheri_libcall queue_receive(Timeout *timeout, - struct QueueHandle *handle, - void *dst); +int __cheri_libcall queue_receive(Timeout *timeout, + struct MessageQueue *handle, + void *dst); /** * Returns the number of items in the queue specified by `handle` via `items`. @@ -150,27 +150,28 @@ * may change in between the return of this function and the caller acting on * the result. */ -int __cheri_libcall queue_items_remaining(struct QueueHandle *handle, - size_t *items); +int __cheri_libcall queue_items_remaining(struct MessageQueue *handle, + size_t *items); /** * Allocate a new message queue that is managed by the message queue - * compartment. This is returned as two sealed pointers to send and receive - * ends of the queue. + * compartment. The resulting queue handle (returned in `outQueue`) is a + * sealed capability to a queue that can be used for both sending and + * receiving. */ int __cheri_compartment("message_queue") queue_create_sealed(Timeout *timeout, struct SObjStruct *heapCapability, - struct SObjStruct **outQueueSend, - struct SObjStruct **outQueReceive, + struct SObjStruct **outQueue, size_t elementSize, size_t elementCount); /** - * Destroy a queue using a sealed queue endpoint handle. The queue is not - * actually freed until *both* endpoints are destroyed, which means that you - * can safely call this from the sending end without the receiving end losing - * access to messages held in the queue. + * Destroy a queue handle. If this is called on a restricted endpoint + * (returned from `queue_receive_handle_create_sealed` or + * `queue_send_handle_create_sealed`), this frees only the handle. If called + * with the queue handle returned from `queue_create_sealed`, this will destroy + * the queue. */ int __cheri_compartment("message_queue") queue_destroy_sealed(Timeout *timeout, @@ -215,7 +216,7 @@ */ void __cheri_libcall multiwaiter_queue_receive_init(struct EventWaiterSource *source, - struct QueueHandle *handle); + struct MessageQueue *handle); /** * Initialise an event waiter source so that it will wait for the queue to be @@ -224,7 +225,7 @@ */ void __cheri_libcall multiwaiter_queue_send_init(struct EventWaiterSource *source, - struct QueueHandle *handle); + struct MessageQueue *handle); /** * Initialise an event waiter source as in `multiwaiter_queue_receive_init`, @@ -250,4 +251,32 @@ multiwaiter_queue_send_init_sealed(struct EventWaiterSource *source, struct SObjStruct *handle); +/** + * Convert a queue handle returned from `queue_create_sealed` into one that can + * be used *only* for receiving. + * + * Returns 0 on success and writes the resulting restricted handle via + * `outHandle`. Returns `-ENOMEM` on allocation failure or `-EINVAL` if the + * handle is not valid. + */ +int __cheri_compartment("message_queue") + queue_receive_handle_create_sealed(struct Timeout *timeout, + struct SObjStruct *heapCapability, + struct SObjStruct *handle, + struct SObjStruct **outHandle); + +/** + * Convert a queue handle returned from `queue_create_sealed` into one that can + * be used *only* for sending. + * + * Returns 0 on success and writes the resulting restricted handle via + * `outHandle`. Returns `-ENOMEM` on allocation failure or `-EINVAL` if the + * handle is not valid. + */ +int __cheri_compartment("message_queue") + queue_send_handle_create_sealed(struct Timeout *timeout, + struct SObjStruct *heapCapability, + struct SObjStruct *handle, + struct SObjStruct **outHandle); + __END_DECLS
diff --git a/sdk/include/setjmp-assembly.h b/sdk/include/setjmp-assembly.h new file mode 100644 index 0000000..5c2bc30 --- /dev/null +++ b/sdk/include/setjmp-assembly.h
@@ -0,0 +1,11 @@ +// Copyright CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +#pragma once + +#include <assembly-helpers.h> + +EXPORT_ASSEMBLY_OFFSET(__jmp_buf, __cs0, 0) +EXPORT_ASSEMBLY_OFFSET(__jmp_buf, __cs1, 8) +EXPORT_ASSEMBLY_OFFSET(__jmp_buf, __csp, 16) +EXPORT_ASSEMBLY_OFFSET(__jmp_buf, __cra, 24)
diff --git a/sdk/include/setjmp.h b/sdk/include/setjmp.h index d8d3cdf..9710ccd 100644 --- a/sdk/include/setjmp.h +++ b/sdk/include/setjmp.h
@@ -30,11 +30,14 @@ */ typedef struct __jmp_buf jmp_buf[1]; +#include <setjmp-assembly.h> + +__BEGIN_DECLS /** * C `setjmp` function. Returns (up to) twice. First returns 0, returns a * value passed to `longjmp` on the second return. */ -__attribute__((returns_twice)) extern "C" int setjmp(jmp_buf env); +__attribute__((returns_twice)) int setjmp(jmp_buf env); __asm__(".section .text.setjmp,\"awG\",@progbits,setjmp,comdat\n" ".globl setjmp\n" ".p2align 2\n" @@ -50,7 +53,7 @@ /** * C `longjmp` function. Does not return, jumps back to the `setjmp` call. */ -extern "C" void longjmp(jmp_buf env, int val); +__attribute__((__noreturn__)) void longjmp(jmp_buf env, int val); __asm__(".section .text.longjmp,\"awG\",@progbits,longjmp,comdat\n" ".globl longjmp\n" ".p2align 2\n" @@ -62,3 +65,4 @@ " clc cra, 24(ca0)\n" " mv a0, a1\n" " cjr cra\n"); +__END_DECLS
diff --git a/sdk/include/stdbool.h b/sdk/include/stdbool.h index c4013ec..debc62b 100644 --- a/sdk/include/stdbool.h +++ b/sdk/include/stdbool.h
@@ -5,9 +5,11 @@ #define _STDBOOL_H_ #ifndef __cplusplus +#if __STDC_VERSION__ < 202000 typedef _Bool bool; # define true 1 # define false 0 +#endif #endif // __cplusplus #endif // _STDBOOL_H_
diff --git a/sdk/include/stdlib.h b/sdk/include/stdlib.h index 6955d23..adc2143 100644 --- a/sdk/include/stdlib.h +++ b/sdk/include/stdlib.h
@@ -329,6 +329,6 @@ static inline void yield(void) { - __asm volatile("ecall"); + __asm volatile("ecall" ::: "memory"); } __END_DECLS
diff --git a/sdk/include/token.h b/sdk/include/token.h index 3c7ba20..c25c04b 100644 --- a/sdk/include/token.h +++ b/sdk/include/token.h
@@ -69,17 +69,51 @@ size_t); /** - * Unseal the obj given the key. + * Unseal the object given the key. * - * The key must have the permit-unseal permission. + * The key may be either a static or dynamic key (i.e. one created with the + * `STATIC_SEALING_TYPE` macro or with `token_key_new`) and the object may be + * either allocated dynamically (via the token APIs) or statically (via the + * `DEFINE_STATIC_SEALED_VALUE` macro). * - * @return unsealed obj if key and obj are valid and they match. nullptr - * otherwise + * Returns the unsealed object if the key and object are valid and of the + * correct type, null otherwise. + * + * This function is equivalent to calling both `token_obj_unseal_static` and + * `token_obj_unseal_dynamic` and returning the result of the first one that + * succeeds, or null if both fail. */ [[cheri::interrupt_state(disabled)]] void * __cheri_libcall token_obj_unseal(SKey, SObj); /** + * Unseal the object given the key. + * + * The key must be a static sealing key (i.e. one created with the + * `STATIC_SEALING_TYPE` macro) and the object must be a statically sealed + * object (i.e. one created with the `DEFINE_STATIC_SEALED_VALUE` macro). + * + * Returns the unsealed object if the key and object are valid and of the + * correct type, null otherwise. + */ +[[cheri::interrupt_state(disabled)]] void * + __cheri_libcall token_obj_unseal_static(SKey, SObj); + +/** + * Unseal the object given the key. + * + * The key may be either a static or dynamic key (i.e. one created with the + * `STATIC_SEALING_TYPE` macro or with `token_key_new`) and the object must be + * allocated dynamically with `token_sealed_alloc` or + * `token_sealed_unsealed_alloc`. + * + * Returns the unsealed object if the key and object are valid and of the + * correct type, null otherwise. + */ +[[cheri::interrupt_state(disabled)]] void * + __cheri_libcall token_obj_unseal_dynamic(SKey, SObj); + +/** * Destroy the obj given its key, freeing memory. * * The key must have the permit-unseal permission.
diff --git a/sdk/include/unwind-assembly.h b/sdk/include/unwind-assembly.h new file mode 100644 index 0000000..377d97f --- /dev/null +++ b/sdk/include/unwind-assembly.h
@@ -0,0 +1,12 @@ +// Copyright CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +#pragma once + +#include <assembly-helpers.h> +#include <setjmp-assembly.h> + +#define INVOCATION_LOCAL_UNWIND_LIST_OFFSET 8 + +EXPORT_ASSEMBLY_OFFSET(CleanupList, next, 0) +EXPORT_ASSEMBLY_OFFSET(CleanupList, env, 8)
diff --git a/sdk/include/unwind.h b/sdk/include/unwind.h index 7a1c898..ce6a25a 100644 --- a/sdk/include/unwind.h +++ b/sdk/include/unwind.h
@@ -9,11 +9,13 @@ struct CleanupList { /// Next pointer. - CleanupList *next; + struct CleanupList *next; /// Jump buffer to return to. - __jmp_buf env; + struct __jmp_buf env; }; +#include <unwind-assembly.h> + /** * Head of the cleanup list. * @@ -25,7 +27,8 @@ { void *csp = __builtin_cheri_stack_get(); ptraddr_t top = __builtin_cheri_top_get(csp); - csp = __builtin_cheri_address_set(csp, top - 8); + csp = __builtin_cheri_address_set( + csp, top - INVOCATION_LOCAL_UNWIND_LIST_OFFSET); return (struct CleanupList **)csp; } @@ -34,9 +37,9 @@ */ __always_inline static inline void cleanup_unwind(void) { - CleanupList **__head = cleanup_list_head(); - CleanupList *__top = *__head; - *__head = __top->next; + struct CleanupList **__head = cleanup_list_head(); + struct CleanupList *__top = *__head; + *__head = __top->next; switcher_handler_invocation_count_reset(); longjmp(&__top->env, 1); } @@ -53,10 +56,10 @@ */ #define CHERIOT_DURING \ { \ - CleanupList cleanupListEntry; \ - auto **__head = cleanup_list_head(); \ - cleanupListEntry.next = *__head; \ - *__head = &cleanupListEntry; \ + struct CleanupList cleanupListEntry; \ + struct CleanupList **__head = cleanup_list_head(); \ + cleanupListEntry.next = *__head; \ + *__head = &cleanupListEntry; \ if (setjmp(&cleanupListEntry.env) == 0) \ { /// See CHERIOT_DURING.
diff --git a/sdk/lib/freestanding/memcpy.c b/sdk/lib/freestanding/memcpy.c index 56ed012..5b7c791 100644 --- a/sdk/lib/freestanding/memcpy.c +++ b/sdk/lib/freestanding/memcpy.c
@@ -49,7 +49,9 @@ typedef void *word; #define wsize sizeof(word) -_Static_assert(wsize != 0 && (wsize & (wsize - 1)) == 0); +_Static_assert(wsize != 0); +_Static_assert((wsize & (wsize - 1)) == 0); + #define wmask (wsize - 1) void *__cheri_libcall memcpy(void *dst0, const void *src0, size_t length)
diff --git a/sdk/lib/queue/README.md b/sdk/lib/queue/README.md new file mode 100644 index 0000000..b5837a3 --- /dev/null +++ b/sdk/lib/queue/README.md
@@ -0,0 +1,12 @@ +Message queues +============== + +Message queues, as described in [`queue.h`](../../include/queue.h). + +This directory provides two targets. + + - The message queue library (`message_queue_library`) provides APIs for message queues that can be shared between two threads in the same compartment. + - The message queue compartment (`message_queue`) wraps these in APIs that can be used from different compartments. + +The library uses the `setjmp`-based error handler (see: [`unwind.h`](../../include/unwind.h)) to recover from invalid bounds or permissions. +If you are using the library and want to be robust in the presence of CHERI exceptions, you should either add `unwind_error_handler` as a dependency of your compartment or provide an error handler that calls `cleanup_unwind`.
diff --git a/sdk/lib/queue/queue.cc b/sdk/lib/queue/queue.cc index 879f8c6..716ad77 100644 --- a/sdk/lib/queue/queue.cc +++ b/sdk/lib/queue/queue.cc
@@ -7,11 +7,12 @@ #include <stdint.h> #include <timeout.h> #include <type_traits> +#include <unwind.h> using namespace CHERI; using cheriot::atomic; -using Debug = ConditionalDebug<false, "Queue library">; +using Debug = ConditionalDebug<false, "MessageQueue library">; #ifdef __cplusplus using MessageQueueCounter = atomic<uint32_t>; @@ -208,15 +209,15 @@ /** * Returns a pointer to the element in the queue indicated by `counter`. */ - Capability<void> buffer_at_counter(struct QueueHandle &handle, - uint32_t counter) + Capability<void> buffer_at_counter(struct MessageQueue &handle, + uint32_t counter) { // Handle wrap for the second run around the counter. size_t index = counter >= handle.queueSize ? counter - handle.queueSize : counter; auto offset = index * handle.elementSize; - Capability<void> pointer{handle.buffer}; - pointer.address() += offset; + Capability<void> pointer{&handle}; + pointer.address() += sizeof(MessageQueue) + offset; return pointer; } @@ -332,102 +333,30 @@ old, (old & HighBitFlagLock::reserved_bits()) | value)); } - /// Permissions for read-only access to a counter. - static constexpr PermissionSet ReadOnly{Permission::Global, - Permission::Load}; - /// Permissions for read-only access to a buffer. - static constexpr PermissionSet ReadOnlyCapability{ - Permission::Global, - Permission::Load, - Permission::LoadStoreCapability, - Permission::LoadGlobal, - Permission::LoadGlobal}; - /// Permissions for write-only access to a buffer. - static constexpr PermissionSet WriteOnlyCapability{ - Permission::Global, - Permission::Store, - Permission::LoadStoreCapability}; - - /** - * Helper to drop our short-lived claims. - */ - void drop_claims() - { - Timeout t{0}; - heap_claim_fast(&t, nullptr, nullptr); - } - - void bound_queue_buffer(struct QueueHandle handle) - { - Capability buffer = handle.buffer; - Capability producer = handle.producer; - // Restrict the bounds using the address of the producer, which - // comes immediately after the queue buffer. This should be - // strictly equivalent to calculating the size of the queue - // from the number of elements and the size of elements (we - // assert that below). - buffer.bounds() = producer.address() - buffer.address(); - Debug::Assert( - [&]() -> bool { - size_t bufferSize; - bool overflow = __builtin_mul_overflow( - handle.queueSize, handle.elementSize, &bufferSize); - bufferSize = CHERI::representable_length(bufferSize); - return (!overflow) && (buffer.bounds() == bufferSize); - }, - "Mismatch between the size of the queue as reported by `queueSize` " - "and `elementSize` and its real size."); - } - } // namespace -struct QueueHandle queue_make_receive_handle(struct QueueHandle handle) -{ - Capability buffer = handle.buffer; - Capability producer = handle.producer; - bound_queue_buffer(handle); - buffer.permissions() &= ReadOnlyCapability; - producer.permissions() &= ReadOnly; - handle.buffer = buffer; - handle.producer = producer; - return handle; -} - -struct QueueHandle queue_make_send_handle(struct QueueHandle handle) -{ - Capability buffer = handle.buffer; - Capability consumer = handle.consumer; - bound_queue_buffer(handle); - buffer.permissions() &= WriteOnlyCapability; - consumer.permissions() &= ReadOnly; - handle.buffer = buffer; - handle.consumer = consumer; - return handle; -} - -int queue_destroy(struct SObjStruct *heapCapability, struct QueueHandle *handle) +int queue_destroy(struct SObjStruct *heapCapability, + struct MessageQueue *handle) { int ret = 0; // Only upgrade the locks for destruction if we know that we will be // able to free the queue at the end. This will fail if passed a // restricted buffer, which will happen if `queue_destroy` is called on // a restricted queue. - if (ret = heap_can_free(heapCapability, handle->buffer); ret != 0) + if (ret = heap_can_free(heapCapability, handle); ret != 0) { return ret; } - auto *producer = handle->producer; - HighBitFlagLock producerLock{*producer}; + HighBitFlagLock producerLock{handle->producer}; producerLock.upgrade_for_destruction(); - auto *consumer = handle->consumer; - HighBitFlagLock consumerLock{*consumer}; + HighBitFlagLock consumerLock{handle->consumer}; consumerLock.upgrade_for_destruction(); // This should not fail because of the `heap_can_free` check, unless we // run out of stack. - if (ret = heap_free(heapCapability, handle->buffer); ret != 0) + if (ret = heap_free(heapCapability, handle); ret != 0) { return ret; } @@ -435,27 +364,16 @@ return ret; } -int queue_create(Timeout *timeout, - struct SObjStruct *heapCapability, - struct QueueHandle *outQueue, - void **outAllocation, - size_t elementSize, - size_t elementCount) +ssize_t queue_allocation_size(size_t elementSize, size_t elementCount) { size_t bufferSize; size_t allocSize; bool overflow = __builtin_mul_overflow(elementCount, elementSize, &bufferSize); - // We must be able to accurately represent the buffer, so round it up to a - // representable length. - bufferSize = CHERI::representable_length(bufferSize); static constexpr size_t CounterSize = sizeof(uint32_t); - // Round up the size to be correctly aligned for the counters at the end - // (if necessary) and add two counters worth of space. - overflow |= __builtin_add_overflow( - bufferSize, - (2 * CounterSize) + (CounterSize - (bufferSize & (CounterSize - 1))), - &allocSize); + // We also need space for the header + overflow |= + __builtin_add_overflow(sizeof(MessageQueue), bufferSize, &allocSize); if (overflow) { return -EINVAL; @@ -471,6 +389,21 @@ return -EINVAL; } + return allocSize; +} + +int queue_create(Timeout *timeout, + struct SObjStruct *heapCapability, + struct MessageQueue **outQueue, + size_t elementSize, + size_t elementCount) +{ + ssize_t allocSize = queue_allocation_size(elementSize, elementCount); + if (allocSize < 0) + { + return allocSize; + } + // Allocate the space for the queue. Capability buffer{heap_allocate(timeout, heapCapability, allocSize)}; if (!buffer.is_valid()) @@ -478,86 +411,79 @@ return -ENOMEM; } - Capability<std::atomic<uint32_t>> producer{ - buffer.cast<std::atomic<uint32_t>>()}; - Capability<std::atomic<uint32_t>> consumer{ - buffer.cast<std::atomic<uint32_t>>()}; - // Make the producer and consumer point after the buffer - producer.address() += bufferSize; - consumer.address() += bufferSize + CounterSize; - // Set their bounds to 4 bytes. - producer.bounds() = CounterSize; - consumer.bounds() = CounterSize; - // The pointer used to free the allocation - *outAllocation = buffer; - Debug::log("Created queue with buffer: {}", buffer); - // The handle - *outQueue = {elementSize, elementCount, buffer, producer, consumer}; - + *outQueue = new (buffer.get()) MessageQueue(elementSize, elementCount); return 0; } -int queue_send(Timeout *timeout, struct QueueHandle *handle, const void *src) +int queue_send(Timeout *timeout, struct MessageQueue *handle, const void *src) { Debug::log("Send called on: {}", handle); - auto *producer = handle->producer; - auto *consumer = handle->consumer; + auto *producer = &handle->producer; + auto *consumer = &handle->consumer; bool shouldWake = false; { Debug::log("Lock word: {}", producer->load()); HighBitFlagLock l{*producer}; if (LockGuard g{l, timeout}) { - uint32_t producerCounter = counter_load(producer); - uint32_t consumerValue = consumer->load(); - uint32_t consumerCounter = - consumerValue & ~(HighBitFlagLock::reserved_bits()); - Debug::log("Producer counter: {}, consumer counter: {}, Size: {}", - producerCounter, - consumerCounter, - handle->queueSize); - while (is_full(handle->queueSize, producerCounter, consumerCounter)) + volatile int ret = 0; + // In an error-handling context, try to add the element to the + // queue. If the permissions on src are invalid, or either `handle` + // or `src` is freed concurrently, we will hit the path that returns + // -EPERM. The counter update happens last, so any failure will + // simply leave the queue in the old state. + on_error( + [&] { + uint32_t producerCounter = counter_load(producer); + uint32_t consumerValue = consumer->load(); + uint32_t consumerCounter = + consumerValue & ~(HighBitFlagLock::reserved_bits()); + Debug::log( + "Producer counter: {}, consumer counter: {}, Size: {}", + producerCounter, + consumerCounter, + handle->queueSize); + while (is_full( + handle->queueSize, producerCounter, consumerCounter)) + { + // Wait on the value to change. If we hit this path while + // the consumer lock is held, then the high bits will be + // set. Make sure that we yield. + if (consumer->wait(timeout, consumerValue) == -ETIMEDOUT) + { + Debug::log("Timed out on futex"); + ret = -ETIMEDOUT; + return; + } + consumerValue = consumer->load(); + consumerCounter = + consumerValue & ~(HighBitFlagLock::reserved_bits()); + } + auto entry = buffer_at_counter(*handle, producerCounter); + Debug::log("Send copying {} bytes from {} to {}", + handle->elementSize, + src, + entry); + memcpy(entry, src, handle->elementSize); + counter_store( + &handle->producer, + increment_and_wrap(handle->queueSize, producerCounter)); + // Check if the queue was empty before we updated the producer + // counter. By the time that we reach this point, anything on + // the consumer side will be on the path to a futex_wait with + // the old version of the producer counter and so will bounce + // out again. + shouldWake = + is_empty(producerCounter, counter_load(consumer)); + }, + [&]() { + ret = -EPERM; + Debug::log("Error in send"); + }); + if (ret != 0) { - // Wait on the value to change. If we hit this path while the - // consumer lock is held, then the high bits will be set. Make - // sure that we yield. - if (consumer->wait(timeout, consumerValue) == -ETIMEDOUT) - { - Debug::log("Timed out on futex"); - return -ETIMEDOUT; - } - consumerValue = consumer->load(); - consumerCounter = - consumerValue & ~(HighBitFlagLock::reserved_bits()); + return ret; } - auto entry = buffer_at_counter(*handle, producerCounter); - if (int claim = heap_claim_fast(timeout, handle->buffer, src); - claim != 0) - { - Debug::log("Claim failed: {}", claim); - return claim; - } - if (!check_pointer<PermissionSet{Permission::Load}, false>( - src, handle->elementSize)) - { - drop_claims(); - Debug::log("Load / bounds check failed: {}"); - return -EPERM; - } - Debug::log("Send copying {} bytes from {} to {}", - handle->elementSize, - src, - entry); - memcpy(entry, src, handle->elementSize); - drop_claims(); - counter_store( - handle->producer, - increment_and_wrap(handle->queueSize, producerCounter)); - // Check if the queue was empty before we updated the producer - // counter. By the time that we reach this point, anything on the - // consumer side will be on the path to a futex_wait with the old - // version of the producer counter and so will bounce out again. - shouldWake = is_empty(producerCounter, counter_load(consumer)); } else { @@ -567,71 +493,77 @@ } if (shouldWake) { - handle->producer->notify_all(); + handle->producer.notify_all(); } return 0; } -int queue_receive(Timeout *timeout, struct QueueHandle *handle, void *dst) +int queue_receive(Timeout *timeout, struct MessageQueue *handle, void *dst) { Debug::log("Receive called on: {}", handle); - auto *producer = handle->producer; - auto *consumer = handle->consumer; + auto *producer = &handle->producer; + auto *consumer = &handle->consumer; bool shouldWake = false; { HighBitFlagLock l{*consumer}; if (LockGuard g{l, timeout}) { - uint32_t producerValue = producer->load(); - uint32_t producerCounter = - producerValue & ~(HighBitFlagLock::reserved_bits()); - uint32_t consumerCounter = counter_load(consumer); - Debug::log("Producer counter: {}, consumer counter: {}, Size: {}", - producerCounter, - consumerCounter, - handle->queueSize); - while (is_empty(producerCounter, consumerCounter)) + volatile int ret = 0; + // In an error-handling context, try to add the element to the + // queue. If the permissions on `dst` are invalid, or either + // `handle` or `dst` is freed concurrently, we will hit the path + // that returns `-EPERM`. The counter update happens last, so any + // failure will simply leave the queue in the old state. + on_error( + [&] { + uint32_t producerValue = producer->load(); + uint32_t producerCounter = + producerValue & ~(HighBitFlagLock::reserved_bits()); + uint32_t consumerCounter = counter_load(consumer); + Debug::log( + "Producer counter: {}, consumer counter: {}, Size: {}", + producerCounter, + consumerCounter, + handle->queueSize); + while (is_empty(producerCounter, consumerCounter)) + { + // Wait on the value to change. If we hit this path while + // the producer lock is held, then the high bits will be + // set. Make sure that we yield. + if (producer->wait(timeout, producerValue) == -ETIMEDOUT) + { + ret = -ETIMEDOUT; + return; + } + producerValue = producer->load(); + producerCounter = + producerValue & ~(HighBitFlagLock::reserved_bits()); + } + auto entry = buffer_at_counter(*handle, consumerCounter); + Debug::log("Receive copying {} bytes from {} to {}", + handle->elementSize, + entry, + dst); + memcpy(dst, entry, handle->elementSize); + counter_store( + consumer, + increment_and_wrap(handle->queueSize, consumerCounter)); + // Check if the queue was full before we updated the consumer + // counter. By the time that we reach this point, anything on + // the producer side will be on the path to a futex_wait with + // the old version of the consumer counter and so will bounce + // out again. + shouldWake = is_full( + handle->queueSize, counter_load(producer), consumerCounter); + }, + [&]() { + ret = -EPERM; + Debug::log("Error in receive"); + }); + if (ret != 0) { - // Wait on the value to change. If we hit this path while the - // producer lock is held, then the high bits will be set. Make - // sure that we yield. - if (producer->wait(timeout, producerValue) == -ETIMEDOUT) - { - return -ETIMEDOUT; - } - producerValue = producer->load(); - producerCounter = - producerValue & ~(HighBitFlagLock::reserved_bits()); + return ret; } - auto entry = buffer_at_counter(*handle, consumerCounter); - if (int claim = heap_claim_fast(timeout, handle->buffer, dst); - claim != 0) - { - return claim; - } - if (!check_pointer<PermissionSet{Permission::Store}, false>( - dst, handle->elementSize)) - { - drop_claims(); - Debug::log("Check pointer failed with {} for {} byte write", - dst, - handle->elementSize); - return -EPERM; - } - Debug::log("Receive copying {} bytes from {} to {}", - handle->elementSize, - entry, - dst); - memcpy(dst, entry, handle->elementSize); - drop_claims(); - counter_store( - consumer, increment_and_wrap(handle->queueSize, consumerCounter)); - // Check if the queue was full before we updated the consumer - // counter. By the time that we reach this point, anything on the - // producer side will be on the path to a futex_wait with the old - // version of the consumer counter and so will bounce out again. - shouldWake = is_full( - handle->queueSize, counter_load(producer), consumerCounter); } else { @@ -639,17 +571,19 @@ return -ETIMEDOUT; } } + // If the queue is concurrently freed, this can trap, but we won't leak any + // locks. if (shouldWake) { - handle->consumer->notify_all(); + handle->consumer.notify_all(); } return 0; } -int queue_items_remaining(struct QueueHandle *handle, size_t *items) +int queue_items_remaining(struct MessageQueue *handle, size_t *items) { - auto producerCounter = counter_load(handle->producer); - auto consumerCounter = counter_load(handle->consumer); + auto producerCounter = counter_load(&handle->producer); + auto consumerCounter = counter_load(&handle->consumer); *items = items_remaining(handle->queueSize, producerCounter, consumerCounter); Debug::log("Producer counter: {}, consumer counter: {}, items: {}", @@ -660,20 +594,20 @@ } void multiwaiter_queue_send_init(struct EventWaiterSource *source, - struct QueueHandle *handle) + struct MessageQueue *handle) { - uint32_t producer = counter_load(handle->producer); - uint32_t consumer = counter_load(handle->consumer); - source->eventSource = handle->consumer; + uint32_t producer = counter_load(&handle->producer); + uint32_t consumer = counter_load(&handle->consumer); + source->eventSource = &handle->consumer; source->value = is_full(handle->queueSize, producer, consumer) ? consumer : -1; } void multiwaiter_queue_receive_init(struct EventWaiterSource *source, - struct QueueHandle *handle) + struct MessageQueue *handle) { - uint32_t producer = counter_load(handle->producer); - uint32_t consumer = counter_load(handle->consumer); - source->eventSource = handle->producer; + uint32_t producer = counter_load(&handle->producer); + uint32_t consumer = counter_load(&handle->consumer); + source->eventSource = &handle->producer; source->value = is_empty(producer, consumer) ? producer : -1; }
diff --git a/sdk/lib/queue/queue_compartment.cc b/sdk/lib/queue/queue_compartment.cc index 96f4845..9aec00a 100644 --- a/sdk/lib/queue/queue_compartment.cc +++ b/sdk/lib/queue/queue_compartment.cc
@@ -8,10 +8,14 @@ using namespace CHERI; -using Debug = ConditionalDebug<false, "Queue compartment">; +using Debug = ConditionalDebug<false, "MessageQueue compartment">; namespace { + __always_inline SKey handle_key() + { + return STATIC_SEALING_TYPE(MessageQueueHandle); + } __always_inline SKey receive_key() { return STATIC_SEALING_TYPE(ReceiveHandle); @@ -21,88 +25,62 @@ return STATIC_SEALING_TYPE(SendHandle); } - struct QueueEndpoint + /** + * Wrapper used for restricted endpoints. This is used to provide + * capabilities that allow only sending or receiving. Instances of this + * will be sealed with either `send_key()` or `receive_key()` to define + * their types. + */ + struct RestrictedEndpoint { - QueueHandle handle; - void *allocation; - // Lock that protects against double free. - FlagLockPriorityInherited lock; + MessageQueue *handle; }; + /** + * Unseal something that is either a queue handle or a restricted endpoint + * with the specified key. + */ + MessageQueue *unseal(SKey key, SObj handle) + { + MessageQueue *queue = nullptr; + if (auto *unsealed = + token_unseal(key, Sealed<RestrictedEndpoint>{handle})) + { + queue = unsealed->handle; + } + else if (auto *unsealed = + token_unseal(handle_key(), Sealed<MessageQueue>{handle})) + { + queue = unsealed; + } + return queue; + } + } // namespace int queue_create_sealed(Timeout *timeout, struct SObjStruct *heapCapability, - struct SObjStruct **outQueueSend, - struct SObjStruct **outQueueReceive, + struct SObjStruct **outQueue, size_t elementSize, size_t elementCount) { - if (!check_timeout_pointer(timeout)) + ssize_t allocSize = queue_allocation_size(elementSize, elementCount); + if (allocSize < 0) { - return -EPERM; + return -EINVAL; } - // Allocate the queue endpoints - auto [send, sendSealed] = - token_allocate<QueueEndpoint>(timeout, heapCapability, send_key()); - if (!send) + void *unsealed = nullptr; + // Allocate the space for the queue. + auto sealed = token_sealed_unsealed_alloc( + timeout, heapCapability, handle_key(), allocSize, &unsealed); + if (!unsealed) { - return timeout->may_block() ? -ENOMEM : -ETIMEDOUT; - } - auto [receive, receiveSealed] = - token_allocate<QueueEndpoint>(timeout, heapCapability, receive_key()); - if (!receive) - { - token_obj_destroy(heapCapability, send_key(), sendSealed); - return timeout->may_block() ? -ENOMEM : -ETIMEDOUT; + return -ENOMEM; } - // Bidirectional queue handle - QueueHandle handle; - // The pointer to the queue that is used when freeing - void *freeBuffer; - // Allocate the queue object - int ret = queue_create( - timeout, heapCapability, &handle, &freeBuffer, elementSize, elementCount); - if (ret != 0) - { - token_obj_destroy(heapCapability, send_key(), sendSealed); - token_obj_destroy(heapCapability, receive_key(), receiveSealed); - return ret; - } - - send->handle = queue_make_send_handle(handle); - send->allocation = freeBuffer; - receive->handle = queue_make_receive_handle(handle); - receive->allocation = freeBuffer; - // Add a second claim on the buffer so that we can free the queue by freeing - // it twice, once in each endpoint. - // TODO we should check the return value of `heap_claim` - heap_claim(heapCapability, freeBuffer); - - if (int claimed = heap_claim_fast(timeout, outQueueSend, outQueueReceive); - claimed != 0) - { - return claimed; - } - if (!check_pointer<PermissionSet{Permission::Load, - Permission::Store, - Permission::LoadStoreCapability}>( - outQueueReceive, sizeof(void *)) || - !check_pointer<PermissionSet{Permission::Load, - Permission::Store, - Permission::LoadStoreCapability}>( - outQueueSend, sizeof(void *))) - { - // Free twice because we claimed it once in addition to the original - // allocation. - heap_free(heapCapability, freeBuffer); - heap_free(heapCapability, freeBuffer); - return -EPERM; - } - *outQueueSend = sendSealed; - *outQueueReceive = receiveSealed; + new (unsealed) MessageQueue(elementSize, elementCount); + *outQueue = sealed; return 0; } @@ -110,99 +88,131 @@ struct SObjStruct *heapCapability, struct SObjStruct *queueHandle) { - if (!check_timeout_pointer(timeout)) + if (token_obj_unseal(handle_key(), queueHandle) != nullptr) { - return -EPERM; + token_obj_destroy(heapCapability, handle_key(), queueHandle); + return 0; } - Debug::log("Destroying queue {}", queueHandle); - auto token = receive_key(); - auto *end = token_unseal(token, Sealed<QueueEndpoint>{queueHandle}); - // This function takes either endpoint, so we need to try unsealing with - // both keys. - if (!end) + if (token_obj_unseal(send_key(), queueHandle) != nullptr) { - token = send_key(); - end = token_unseal(token, Sealed<QueueEndpoint>{queueHandle}); + token_obj_destroy(heapCapability, send_key(), queueHandle); + return 0; } - if (!end) + if (token_obj_unseal(receive_key(), queueHandle) != nullptr) { - return -EINVAL; + token_obj_destroy(heapCapability, receive_key(), queueHandle); + return 0; } - // Don't bother with a lock guard: we will destroy this lock if we reach the - // end. If we lose a race here, this will trap and we will implicitly return - // `-ECOMPARTMENTFAIL`. - if (!end->lock.try_lock(timeout)) - { - return -ETIMEDOUT; - } - if (heap_free(heapCapability, end->allocation) != 0) - { - end->lock.unlock(); - return -EPERM; - } - token_obj_destroy(heapCapability, token, queueHandle); - return 0; + return -EINVAL; } int queue_send_sealed(Timeout *timeout, struct SObjStruct *handle, const void *src) { - auto *end = token_unseal(send_key(), Sealed<QueueEndpoint>{handle}); - // If we failed to unseal, or if the timeout pointer is invalid, invalid - // argument error. - if (!end || !check_timeout_pointer(timeout)) + MessageQueue *queue = unseal(send_key(), handle); + if (!queue || !check_timeout_pointer(timeout)) { return -EINVAL; } - return queue_send(timeout, &end->handle, src); + return queue_send(timeout, queue, src); } int queue_receive_sealed(Timeout *timeout, struct SObjStruct *handle, void *dst) { - auto *end = token_unseal(receive_key(), Sealed<QueueEndpoint>{handle}); - // If we failed to unseal, or if the timeout is on the heap, invalid - // argument error. - if (!end || !check_timeout_pointer(timeout)) + MessageQueue *queue = unseal(receive_key(), handle); + if (!queue || !check_timeout_pointer(timeout)) { return -EINVAL; } - return queue_receive(timeout, &end->handle, dst); + return queue_receive(timeout, queue, dst); } int multiwaiter_queue_receive_init_sealed(struct EventWaiterSource *source, struct SObjStruct *handle) { - auto *end = token_unseal(receive_key(), Sealed<QueueEndpoint>{handle}); - if (!end) + MessageQueue *queue = unseal(receive_key(), handle); + if (!queue) { return -EINVAL; } - multiwaiter_queue_receive_init(source, &end->handle); + multiwaiter_queue_receive_init(source, queue); return 0; } int multiwaiter_queue_send_init_sealed(struct EventWaiterSource *source, struct SObjStruct *handle) { - auto *end = token_unseal(send_key(), Sealed<QueueEndpoint>{handle}); - if (!end) + MessageQueue *queue = unseal(send_key(), handle); + if (!queue) { return -EINVAL; } - multiwaiter_queue_send_init(source, &end->handle); + multiwaiter_queue_send_init(source, queue); return 0; } -int queue_items_remaining_sealed(struct SObjStruct *queueHandle, size_t *items) +int queue_items_remaining_sealed(struct SObjStruct *handle, size_t *items) { - auto *end = token_unseal(receive_key(), Sealed<QueueEndpoint>{queueHandle}); + MessageQueue *queue = unseal(send_key(), handle); // This function takes either endpoint, so we need to try unsealing with // both keys. - if (!end) + if (!queue) { - end = token_unseal(send_key(), Sealed<QueueEndpoint>{queueHandle}); + if (auto *unsealed = + token_unseal(receive_key(), Sealed<RestrictedEndpoint>{handle})) + { + queue = unsealed->handle; + } } - queue_items_remaining(&end->handle, items); + if (!queue) + { + return -EINVAL; + } + queue_items_remaining(queue, items); + return 0; +} + +int queue_receive_handle_create_sealed(struct Timeout *timeout, + struct SObjStruct *heapCapability, + struct SObjStruct *handle, + struct SObjStruct **outHandle) +{ + MessageQueue *queue = + token_unseal(handle_key(), Sealed<MessageQueue>(handle)); + if (!queue) + { + return -EINVAL; + } + auto [unsealed, sealed] = token_allocate<RestrictedEndpoint>( + timeout, heapCapability, receive_key()); + if (!unsealed) + { + return -ENOMEM; + } + unsealed->handle = queue; + *outHandle = sealed; + return 0; +} + +int queue_send_handle_create_sealed(struct Timeout *timeout, + struct SObjStruct *heapCapability, + struct SObjStruct *handle, + struct SObjStruct **outHandle) +{ + MessageQueue *queue = + token_unseal(handle_key(), Sealed<MessageQueue>(handle)); + if (!queue) + { + return -EINVAL; + } + auto [unsealed, sealed] = + token_allocate<RestrictedEndpoint>(timeout, heapCapability, send_key()); + if (!unsealed) + { + return -ENOMEM; + } + unsealed->handle = queue; + *outHandle = sealed; return 0; }
diff --git a/sdk/lib/queue/xmake.lua b/sdk/lib/queue/xmake.lua index 53ec1f2..113b580 100644 --- a/sdk/lib/queue/xmake.lua +++ b/sdk/lib/queue/xmake.lua
@@ -9,6 +9,7 @@ add_files("queue.cc") compartment("message_queue") + add_deps("unwind_error_handler") add_deps("message_queue_library") set_default(false) add_files("queue_compartment.cc")
diff --git a/sdk/lib/stdio/printf.cc b/sdk/lib/stdio/printf.cc index 8fc5f51..579b6b6 100644 --- a/sdk/lib/stdio/printf.cc +++ b/sdk/lib/stdio/printf.cc
@@ -88,7 +88,9 @@ *++p = upper ? toupper(c) : c; } while (num /= base); if (lenp) + { *lenp = p - nbuf; + } return (p); } @@ -158,7 +160,9 @@ while ((ch = static_cast<unsigned char>(*fmt++)) != '%' || stop) { if (ch == '\0') + { return (retval); + } putchar(ch); } percent = fmt - 1; @@ -229,21 +233,31 @@ n = n * 10 + ch - '0'; ch = *fmt; if (ch < '0' || ch > '9') + { break; + } } if (dot) + { dwidth = n; + } else + { width = n; + } goto reswitch; // NOLINT case 'b': num = static_cast<unsigned int>(va_arg(ap, int)); p = va_arg(ap, char *); for (q = ksprintn(nbuf, num, *p++, nullptr, 0); *q;) + { putchar(*q--); + } if (num == 0) + { break; + } for (tmp = 0; *p;) { @@ -252,15 +266,23 @@ { putchar(tmp ? ',' : '<'); for (; (n = *p) > ' '; ++p) + { putchar(n); + } tmp = 1; } else + { for (; *p > ' '; ++p) + { continue; + } + } } if (tmp) + { putchar('>'); + } break; case 'c': putchar(va_arg(ap, int)); @@ -269,15 +291,21 @@ up = va_arg(ap, unsigned char *); p = va_arg(ap, char *); if (!width) + { width = 16; + } while (width--) { putchar(hex2ascii(*up >> 4)); putchar(hex2ascii(*up & 0x0f)); up++; if (width) + { for (q = p; *q; q++) + { putchar(*q); + } + } } break; case 'd': @@ -292,7 +320,9 @@ cflag = 1; } else + { hflag = 1; + } goto reswitch; // NOLINT case 'j': jflag = 1; @@ -304,23 +334,39 @@ qflag = 1; } else + { lflag = 1; + } goto reswitch; // NOLINT case 'n': if (jflag) + { *(va_arg(ap, intmax_t *)) = retval; + } else if (qflag) + { *(va_arg(ap, long long *)) = retval; + } else if (lflag) + { *(va_arg(ap, long *)) = retval; + } else if (zflag) + { *(va_arg(ap, size_t *)) = retval; + } else if (hflag) + { *(va_arg(ap, short *)) = static_cast<short>(retval); + } else if (cflag) + { *(va_arg(ap, char *)) = retval; + } else + { *(va_arg(ap, int *)) = retval; + } break; case 'o': base = 8; @@ -338,8 +384,10 @@ case 'r': base = radix; if (sign) + { goto handle_sign; // NOLINT - goto handle_nosign; // NOLINT + } + goto handle_nosign; // NOLINT case 's': p = va_arg(ap, char *); if (p == nullptr) @@ -400,39 +448,71 @@ handle_nosign: sign = 0; if (jflag) + { num = va_arg(ap, uintmax_t); + } else if (qflag) + { num = va_arg(ap, unsigned long long); + } else if (tflag) + { num = va_arg(ap, ptrdiff_t); + } else if (lflag) + { num = va_arg(ap, unsigned long); + } else if (zflag) + { num = va_arg(ap, size_t); + } else if (hflag) + { num = static_cast<unsigned short>(va_arg(ap, int)); + } else if (cflag) + { num = static_cast<unsigned char>(va_arg(ap, int)); + } else + { num = va_arg(ap, unsigned int); + } goto number; // NOLINT handle_sign: if (jflag) + { num = va_arg(ap, intmax_t); + } else if (qflag) + { num = va_arg(ap, long long); + } else if (tflag) + { num = va_arg(ap, ptrdiff_t); + } else if (lflag) + { num = va_arg(ap, long); + } else if (zflag) + { num = va_arg(ap, ssize_t); + } else if (hflag) + { num = static_cast<short>(va_arg(ap, int)); + } else if (cflag) + { num = static_cast<char>(va_arg(ap, int)); + } else + { num = va_arg(ap, int); + } number: if (sign && static_cast<intmax_t>(num) < 0) { @@ -444,22 +524,36 @@ if (sharpflag && num != 0) { if (base == 8) + { tmp++; + } else if (base == 16) + { tmp += 2; + } } if (neg) + { tmp++; + } if (!ladjust && padc == '0') + { dwidth = width - tmp; + } width -= tmp + (dwidth > n ? dwidth : n); dwidth -= n; if (!ladjust) + { while (width-- > 0) + { putchar(' '); + } + } if (neg) + { putchar('-'); + } if (sharpflag && num != 0) { if (base == 8) @@ -473,19 +567,29 @@ } } while (dwidth-- > 0) + { putchar('0'); + } while (*p) + { putchar(*p--); + } if (ladjust) + { while (width-- > 0) + { putchar(' '); + } + } break; default: while (percent < fmt) + { putchar(*percent++); + } /* * Since we ignore an formatting argument it is no * longer safe to obey the remaining formatting @@ -524,7 +628,9 @@ }; int retval = kvprintf(format, callback, &info, 10, ap); if (info.remain >= 1) + { *info.str++ = '\0'; + } return (retval); }
diff --git a/sdk/lib/unwind_error_handler/unwind.S b/sdk/lib/unwind_error_handler/unwind.S index 6e56a98..6c74c06 100644 --- a/sdk/lib/unwind_error_handler/unwind.S +++ b/sdk/lib/unwind_error_handler/unwind.S
@@ -1,16 +1,29 @@ +#include <unwind-assembly.h> + +/** + * A direct re-implementation of unwind.h's cleanup_unwind() as a stackless + * error handler. + * + * If there is no registered CleanupList structure (equivalently, there's no + * CHERIOT_DURING block active at the time of the fault), then this requests + * unwnding out of the compartment. Otherwise, we will longjmp() out to the + * indicated handler (that is, the CHERIOT_HANDLER block associated with the + * current CHERIOT_DURING block), having reset the compartment error handler + * invocation counter to zero. + */ .section .compartment_error_handler_stackless,"aw",@progbits .globl compartment_error_handler_stackless .p2align 2 .type compartment_error_handler_stackless,@function compartment_error_handler_stackless: -// Get the head of the error list. +// Get the head of the error list (see cleanup_list_head) cgettop t0, csp csetaddr csp, csp, t0 - clc cs0, -8(csp) + clc cs0, -INVOCATION_LOCAL_UNWIND_LIST_OFFSET(csp) beqz s0, .Lforce_unwind -// Pop the top error from the list. */ - clc ct0, 0(cs0) - csc ct0, -8(csp) +// Pop the top error from the list + clc ct0, CleanupList_offset_next(cs0) + csc ct0, -INVOCATION_LOCAL_UNWIND_LIST_OFFSET(csp) // Mark this error handler as having finished. We may still trap again // and reenter this, but now that we've popped the top element from the // stack we will run some different cleanup code next time. */ @@ -19,11 +32,12 @@ clc ct2, %cheriot_compartment_lo_i(.Llookup_reset)(ct2) cjalr ct2 // longjmp to the error handler. - clc cs1, 16(cs0) - clc csp, 24(cs0) - clc cra, 32(cs0) - clc cs0, 8(cs0) + clc cs1, (CleanupList_offset_env + __jmp_buf_offset___cs1)(cs0) + clc csp, (CleanupList_offset_env + __jmp_buf_offset___csp)(cs0) + clc cra, (CleanupList_offset_env + __jmp_buf_offset___cra)(cs0) + clc cs0, (CleanupList_offset_env + __jmp_buf_offset___cs0)(cs0) cjr cra + .Lforce_unwind: li a0, 1 cret
diff --git a/sdk/privileged-compartment.ldscript b/sdk/privileged-compartment.ldscript index 9cb984f..c8f1b0a 100644 --- a/sdk/privileged-compartment.ldscript +++ b/sdk/privileged-compartment.ldscript
@@ -17,17 +17,19 @@ # Array of compartment exports *(.compartment_exports .compartment_exports.*); } + # Reserve space at the start for privileged compartments that need special sealing keys. + .compartment_sealing_keys : ALIGN(8) + { + # Start of the compartment's PCC region + HIDDEN(__compartment_pcc_start = .); + # The sealing keys for this compartment is before the import table, if they exist + *(.sealing_key1*) + *(.sealing_key2*) + } # Lay out the compartment imports section. This will end up on PCC. .compartment_import_table : ALIGN(8) { # Array of compartment imports - HIDDEN(__compartment_pcc_start = .); - # The sealing key for this compartment is before the import table. - HIDDEN(__sealingkey = .); - . = . + 8; - # Space for a second sealing key, used only by the allocator. - HIDDEN(__sealingkey2 = .); - . = . + 8; # The first import table entry is the compartment switcher. HIDDEN(.compartment_switcher = .); . = . + 8;
diff --git a/sdk/xmake.lua b/sdk/xmake.lua index da170b7..0f26d6e 100644 --- a/sdk/xmake.lua +++ b/sdk/xmake.lua
@@ -653,11 +653,9 @@ "\n\t\tSHORT(.software_revoker_end - .software_revoker_start);" .. "\n\t\tLONG(.software_revoker_globals);" .. "\n\t\tSHORT(SIZEOF(.software_revoker_globals));" .. - -- The import table offset is computed from the start by code - -- that assumes that the first two words are space for sealing - -- keys, so we set it to 16 here to provide a computed size of - -- 0. - "\n\t\tSHORT(16)" .. + -- The software revoker has no import table. + "\n\t\tLONG(0)" .. + "\n\t\tSHORT(0)" .. "\n\t\tLONG(.software_revoker_export_table);" .. "\n\t\tSHORT(.software_revoker_export_table_end - .software_revoker_export_table);\n" .. "\n\t\tLONG(0);" ..
diff --git a/tests.extra/regress-thread_exit_IRQ/README.md b/tests.extra/regress-thread_exit_IRQ/README.md new file mode 100644 index 0000000..e0e9f2f --- /dev/null +++ b/tests.extra/regress-thread_exit_IRQ/README.md
@@ -0,0 +1,6 @@ +This checks for a specific bug fixed in 2024/11, where it was possible to +invoke the scheduler's exception entrypoint, which must run with IRQs deferred, +with IRQs enabled if a thread exited from its initial activation via a slightly +unusual path. + +See https://github.com/CHERIoT-Platform/cheriot-rtos/pull/346
diff --git a/tests.extra/regress-thread_exit_IRQ/helper.cc b/tests.extra/regress-thread_exit_IRQ/helper.cc new file mode 100644 index 0000000..023b4fe --- /dev/null +++ b/tests.extra/regress-thread_exit_IRQ/helper.cc
@@ -0,0 +1,5 @@ +#include "helper.h" +[[cheri::interrupt_state(enabled)]] void* help(void) +{ + return __builtin_return_address(0); +}
diff --git a/tests.extra/regress-thread_exit_IRQ/helper.h b/tests.extra/regress-thread_exit_IRQ/helper.h new file mode 100644 index 0000000..578659e --- /dev/null +++ b/tests.extra/regress-thread_exit_IRQ/helper.h
@@ -0,0 +1,2 @@ +#include <compartment.h> +void* __cheri_compartment("helper") help(void);
diff --git a/tests.extra/regress-thread_exit_IRQ/top.cc b/tests.extra/regress-thread_exit_IRQ/top.cc new file mode 100644 index 0000000..57087ce --- /dev/null +++ b/tests.extra/regress-thread_exit_IRQ/top.cc
@@ -0,0 +1,5 @@ +#include "helper.h" +void __cheri_compartment("top") entry() +{ + asm volatile ("cmove cra, %0; cret" : : "C"(help())); +}
diff --git a/tests.extra/regress-thread_exit_IRQ/xmake.lua b/tests.extra/regress-thread_exit_IRQ/xmake.lua new file mode 100644 index 0000000..38462b2 --- /dev/null +++ b/tests.extra/regress-thread_exit_IRQ/xmake.lua
@@ -0,0 +1,29 @@ +set_project("CHERIoT Scheduler IRQ Exception PoC") +sdkdir = "../../sdk" +includes(sdkdir) +set_toolchains("cheriot-clang") + +option("board") + set_default("sail") + +compartment("helper") + add_files("helper.cc") + +compartment("top") + add_files("top.cc") + +firmware("top_compartment") + add_deps("freestanding", "debug") + add_deps("top", "helper") + on_load(function(target) + target:values_set("board", "$(board)") + target:values_set("threads", { + { + compartment = "top", + priority = 1, + entry_point = "entry", + stack_size = 0x200, + trusted_stack_frames = 1 + } + }, {expand = false}) + end)
diff --git a/tests/allocator-test.cc b/tests/allocator-test.cc index 97f19c9..17faf66 100644 --- a/tests/allocator-test.cc +++ b/tests/allocator-test.cc
@@ -666,7 +666,9 @@ !array.is_valid(), "Allocating too large an array succeeded: {}", array); array = heap_allocate_array(&t, MALLOC_CAPABILITY, 16, 2); TEST(array.is_valid(), "Allocating array failed: {}", array); - TEST(array.length() == 32, + // If there's heap fragmentation, this may be rounded up to 40 because we + // can't use the 8 bytes after the end for another object. + TEST((array.length() >= 32) && (array.length() <= 40), "Allocating array returned incorrect length: {}", array); ret = heap_free(MALLOC_CAPABILITY, array);
diff --git a/tests/ccompile-test.c b/tests/ccompile-test.c index 7463b7c..da5401c 100644 --- a/tests/ccompile-test.c +++ b/tests/ccompile-test.c
@@ -9,6 +9,7 @@ #include <assert.h> #include <cdefs.h> #include <cheri-builtins.h> +#include <cheri.h> #include <compartment.h> #include <ctype.h> #include <errno.h> @@ -38,4 +39,4 @@ #include <time.h> #include <timeout.h> #include <token.h> -#include <cheri.h> +#include <unwind.h>
diff --git a/tests/compartment_calls-test.cc b/tests/compartment_calls-test.cc index 2f0cff1..a8ed1ee 100644 --- a/tests/compartment_calls-test.cc +++ b/tests/compartment_calls-test.cc
@@ -22,26 +22,26 @@ int value = ConstantValue; ret = compartment_call_inner(value); - TEST(ret == 0, "compartment_call_inner returend {}", ret); + TEST(ret == 0, "compartment_call_inner returned {}", ret); ret = compartment_call_inner(value, value); - TEST(ret == 0, "compartment_call_inner returend {}", ret); + TEST(ret == 0, "compartment_call_inner returned {}", ret); ret = compartment_call_inner(value, value, &value); - TEST(ret == 0, "compartment_call_inner returend {}", ret); + TEST(ret == 0, "compartment_call_inner returned {}", ret); ret = compartment_call_inner(value, value, &value, value); - TEST(ret == 0, "compartment_call_inner returend {}", ret); + TEST(ret == 0, "compartment_call_inner returned {}", ret); ret = compartment_call_inner(value, value, &value, value, &value); - TEST(ret == 0, "compartment_call_inner returend {}", ret); + TEST(ret == 0, "compartment_call_inner returned {}", ret); ret = compartment_call_inner(value, value, &value, value, &value, value); - TEST(ret == 0, "compartment_call_inner returend {}", ret); + TEST(ret == 0, "compartment_call_inner returned {}", ret); ret = compartment_call_inner(value, value, &value, value, &value, value, value); - TEST(ret == 0, "compartment_call_inner returend {}", ret); + TEST(ret == 0, "compartment_call_inner returned {}", ret); } int test_compartment_call()
diff --git a/tests/crash_recovery-test.cc b/tests/crash_recovery-test.cc index 0254654..48dc278 100644 --- a/tests/crash_recovery-test.cc +++ b/tests/crash_recovery-test.cc
@@ -10,9 +10,18 @@ int crashes = 0; std::atomic<bool> expectFault; +static void test_irqs_are_enabled() +{ + void *r = __builtin_return_address(0); + TEST(__builtin_cheri_type_get(r) == CheriSealTypeReturnSentryEnabling, + "Calling context has IRQs disabled"); +} + extern "C" enum ErrorRecoveryBehaviour compartment_error_handler(struct ErrorState *frame, size_t mcause, size_t mtval) { + test_irqs_are_enabled(); + crashes++; if (mcause == 0x2) {
diff --git a/tests/misc-test.cc b/tests/misc-test.cc index f537067..dc09d77 100644 --- a/tests/misc-test.cc +++ b/tests/misc-test.cc
@@ -170,12 +170,59 @@ permissions); } +// This test is somewhat intimately familiar with parameters of CHERIoT's +// capability encoding and so might need revision if that changes. +void check_capability_set_inexact_at_most() +{ + void *p = malloc(3128); + + debug_log("Test Capability::BoundsProxy::set_inexact_at_most with {}", p); + + // Too many bits for mantissa, regardless of base alignment + { + Capability<void> q = {p}; + size_t reqlen = 2047; + q.bounds().set_inexact_at_most(reqlen); + debug_log("Requesting 2047 gives {}: {}", q.length(), q); + TEST(q.is_valid(), "set_inexact_at_most untagged"); + TEST(q.length() < 2047, "set_inexact_at_most failed to truncate"); + TEST(q.base() == q.address(), "set_inexact_at_most nonzero offset"); + } + + // Fits in mantissa, but not reachable from misaligned base + { + Capability<void> q = {p}; + q.address() += 2; + size_t reqlen = 1024; + q.bounds().set_inexact_at_most(reqlen); + debug_log("Requesting 1024 at align 2 gives {}: {}", q.length(), q); + TEST(q.is_valid(), "set_inexact_at_most untagged"); + TEST(q.length() < 1024, "set_inexact_at_most failed to truncate"); + TEST(q.base() == q.address(), "set_inexact_at_most nonzero offset"); + } + + // Fits in mantissa and reachable from misaligned base + { + Capability<void> q = {p}; + q.address() += 1; + size_t reqlen = 511; + q.bounds().set_inexact_at_most(reqlen); + debug_log("Requesting 511 at align 1 gives {}: {}", q.length(), q); + TEST(q.is_valid(), "set_inexact_at_most untagged"); + TEST(q.length() == 511, "set_inexact_at_most truncated unnecessarily"); + TEST(q.base() == q.address(), "set_inexact_at_most nonzero offset"); + } + + free(p); +} + int test_misc() { check_timeouts(); check_memchr(); check_memrchr(); check_pointer_utilities(); + check_capability_set_inexact_at_most(); debug_log("Testing shared objects."); check_shared_object("exampleK", SHARED_OBJECT(void, exampleK),
diff --git a/tests/multiwaiter-test.cc b/tests/multiwaiter-test.cc index d0f380f..d9d99e9 100644 --- a/tests/multiwaiter-test.cc +++ b/tests/multiwaiter-test.cc
@@ -70,16 +70,14 @@ TEST(events[0].value == 0, "Futex reports wake but none occurred"); TEST(events[1].value == 1, "Futex reports no wake"); - QueueHandle queue; - void *queueMemory; + MessageQueue *queue; t.remaining = 0; - ret = - queue_create(&t, MALLOC_CAPABILITY, &queue, &queueMemory, sizeof(int), 1); + ret = queue_create(&t, MALLOC_CAPABILITY, &queue, sizeof(int), 1); TEST(ret == 0, "Queue create failed:", ret); int val = 0; Timeout noWait{0}; - ret = queue_send(&noWait, &queue, &val); + ret = queue_send(&noWait, queue, &val); TEST(ret == 0, "Queue send failed: {}", ret); debug_log("Testing queue, blocked on send"); @@ -87,12 +85,12 @@ sleep(1); int val; Timeout noWait{0}; - int ret = queue_receive(&noWait, &queue, &val); + int ret = queue_receive(&noWait, queue, &val); TEST(ret == 0, "Background receive failed: {}", ret); TEST(val == 0, "Background receive returned incorrect value: {}", ret); debug_log("Background thread made queue ready to send"); }); - multiwaiter_queue_send_init(&events[0], &queue); + multiwaiter_queue_send_init(&events[0], queue); t.remaining = 6; ret = multiwaiter_wait(&t, mw, events, 1); TEST(ret == 0, "multiwaiter returned {}, expected 0", ret); @@ -103,23 +101,23 @@ sleep(1); int val = 1; Timeout noWait{0}; - int ret = queue_send(&noWait, &queue, &val); + int ret = queue_send(&noWait, queue, &val); TEST(ret == 0, "Background send failed: {}", ret); debug_log("Background thread made queue ready to receive"); }); - multiwaiter_queue_receive_init(&events[0], &queue); + multiwaiter_queue_receive_init(&events[0], queue); t = 10; ret = multiwaiter_wait(&t, mw, events, 1); TEST(ret == 0, "multiwaiter returned {}, expected 0", ret); TEST(events[0].value == 1, "Queue did not return ready to receive"); - ret = queue_receive(&noWait, &queue, &val); + ret = queue_receive(&noWait, queue, &val); TEST(ret == 0, "Queue ready to receive but receive returned {}", ret); TEST(val == 1, "Incorrect value returned from queue: {}", val); debug_log("Testing waiting on a queue and a futex"); futex = 0; setFutex(&futex, 1); - multiwaiter_queue_receive_init(&events[0], &queue); + multiwaiter_queue_receive_init(&events[0], queue); events[1] = {&futex, EventWaiterFutex, 0}; t.remaining = 6; ret = multiwaiter_wait(&t, mw, events, 2); @@ -128,7 +126,6 @@ "Queue reports ready to receive but should be empty."); TEST(events[1].value == 1, "Futex reports no wake"); - free(queueMemory); multiwaiter_delete(MALLOC_CAPABILITY, mw); return 0; }
diff --git a/tests/queue-test.cc b/tests/queue-test.cc index da18b87..b8c36dc 100644 --- a/tests/queue-test.cc +++ b/tests/queue-test.cc
@@ -4,7 +4,7 @@ #include "compartment.h" #include "token.h" #include <cstdlib> -#define TEST_NAME "Queue" +#define TEST_NAME "MessageQueue" #include "tests.hh" #include <FreeRTOS-Compat/queue.h> #include <debug.hh> @@ -28,77 +28,76 @@ void test_queue_unsealed() { - char bytes[ItemSize]; - static QueueHandle queue; - static void *queueMemory; - Timeout timeout{0, 0}; + char bytes[ItemSize]; + static MessageQueue *queue; + Timeout timeout{0, 0}; debug_log("Testing queue send operations"); auto checkSpace = [&](size_t expected, SourceLocation loc = SourceLocation::current()) { size_t items; - queue_items_remaining(&queue, &items); + queue_items_remaining(queue, &items); TEST(items == expected, - "Queue test line {} reports {} items, should contain {}", + "MessageQueue test line {} reports {} items, should contain {}", loc.line(), items, expected); }; - int rv = queue_create( - &timeout, MALLOC_CAPABILITY, &queue, &queueMemory, ItemSize, MaxItems); - TEST(queue.elementSize == ItemSize, - "Queue element size is {}, expected {}", - queue.elementSize, + int rv = + queue_create(&timeout, MALLOC_CAPABILITY, &queue, ItemSize, MaxItems); + TEST(queue->elementSize == ItemSize, + "MessageQueue element size is {}, expected {}", + queue->elementSize, ItemSize); - TEST(queue.queueSize == MaxItems, - "Queue size is {}, expected {}", - queue.queueSize, + TEST(queue->queueSize == MaxItems, + "MessageQueue size is {}, expected {}", + queue->queueSize, MaxItems); - TEST(rv == 0, "Queue creation failed with {}", rv); - rv = queue_send(&timeout, &queue, Message[0]); + TEST(rv == 0, "MessageQueue creation failed with {}", rv); + rv = queue_send(&timeout, queue, Message[0]); checkSpace(1); TEST(rv == 0, "Sending the first message failed with {}", rv); checkSpace(1); - rv = queue_send(&timeout, &queue, Message[1]); + rv = queue_send(&timeout, queue, Message[1]); TEST(rv == 0, "Sending the second message failed with {}", rv); checkSpace(2); - // Queue is full, it should time out. + // MessageQueue is full, it should time out. timeout.remaining = 5; - rv = queue_send(&timeout, &queue, Message[1]); + rv = queue_send(&timeout, queue, Message[1]); TEST(rv == -ETIMEDOUT, "Sending to a full queue didn't time out as expected, returned {}", rv); checkSpace(2); debug_log("Testing queue receive operations"); timeout.remaining = 10; - rv = queue_receive(&timeout, &queue, bytes); + rv = queue_receive(&timeout, queue, bytes); TEST(rv == 0, "Receiving the first message failed with {}", rv); TEST(memcmp(Message[0], bytes, ItemSize) == 0, "First message received but not as expected. Got {}", bytes); checkSpace(1); - rv = queue_receive(&timeout, &queue, bytes); + rv = queue_receive(&timeout, queue, bytes); TEST(rv == 0, "Receiving the second message failed with {}", rv); TEST(memcmp(Message[1], bytes, ItemSize) == 0, "Second message received but not as expected. Got {}", bytes); checkSpace(0); timeout.remaining = 5; - rv = queue_receive(&timeout, &queue, bytes); + rv = queue_receive(&timeout, queue, bytes); TEST( rv == -ETIMEDOUT, "Receiving from an empty queue didn't time out as expected, returned {}", rv); // Check that the items remaining calculations are correct after overflow. - queue_send(&timeout, &queue, Message[1]); + queue_send(&timeout, queue, Message[1]); checkSpace(1); - queue_receive(&timeout, &queue, bytes); + queue_receive(&timeout, queue, bytes); checkSpace(0); - queue_send(&timeout, &queue, Message[1]); + queue_send(&timeout, queue, Message[1]); checkSpace(1); - queue_receive(&timeout, &queue, bytes); + queue_receive(&timeout, queue, bytes); checkSpace(0); - rv = queue_destroy(MALLOC_CAPABILITY, &queue); - TEST(rv == 0, "Queue deletion failed with {}", rv); + rv = queue_destroy(MALLOC_CAPABILITY, queue); + TEST(rv == 0, "MessageQueue deletion failed with {}", rv); debug_log("All queue library tests successful"); } @@ -108,10 +107,18 @@ Timeout t{1}; SObj receiveHandle; SObj sendHandle; + SObj queue; char bytes[ItemSize]; - int ret = queue_create_sealed( - &t, MALLOC_CAPABILITY, &sendHandle, &receiveHandle, ItemSize, MaxItems); - TEST(ret == 0, "Queue creation failed with {}", ret); + int ret = + queue_create_sealed(&t, MALLOC_CAPABILITY, &queue, ItemSize, MaxItems); + TEST(ret == 0, "MessageQueue creation failed with {}", ret); + ret = queue_receive_handle_create_sealed( + &t, MALLOC_CAPABILITY, queue, &receiveHandle); + TEST( + ret == 0, "MessageQueue receive endpoint creation failed with {}", ret); + ret = queue_send_handle_create_sealed( + &t, MALLOC_CAPABILITY, queue, &sendHandle); + TEST(ret == 0, "MessageQueue send endpoint creation failed with {}", ret); t = UnlimitedTimeout; ret = queue_send_sealed(&t, receiveHandle, Message[1]); @@ -159,11 +166,14 @@ t = 1; ret = queue_destroy_sealed(&t, MALLOC_CAPABILITY, sendHandle); - TEST(ret == 0, "Queue send destruction failed with {}", ret); + TEST(ret == 0, "MessageQueue send destruction failed with {}", ret); t = 1; ret = queue_destroy_sealed(&t, MALLOC_CAPABILITY, receiveHandle); - TEST(ret == 0, "Queue receive destruction failed with {}", ret); + TEST(ret == 0, "MessageQueue receive destruction failed with {}", ret); + + ret = queue_destroy_sealed(&t, MALLOC_CAPABILITY, queue); + TEST(ret == 0, "MessageQueue destruction failed with {}", ret); TEST(heap_quota_remaining(MALLOC_CAPABILITY) == heapSpace, "Heap space leaked");
diff --git a/tests/stack-test.cc b/tests/stack-test.cc index c814bf9..b3b894f 100644 --- a/tests/stack-test.cc +++ b/tests/stack-test.cc
@@ -150,6 +150,12 @@ expect_handler(false); exhaust_thread_stack(); + debug_log("exhausting the compartment stack during a switcher call"); + expect_handler(false); + threadStackTestFailed = true; + exhaust_thread_stack_spill(callback); + TEST(threadStackTestFailed == false, "switcher did not return error"); + debug_log("modifying stack permissions on fault"); PermissionSet compartmentStackPermissions = get_stack_permissions(); for (auto permissionToRemove : compartmentStackPermissions)
diff --git a/tests/stack_integrity_thread.cc b/tests/stack_integrity_thread.cc index 7a629c5..c897432 100644 --- a/tests/stack_integrity_thread.cc +++ b/tests/stack_integrity_thread.cc
@@ -94,6 +94,40 @@ TEST(false, "Should be unreachable"); } +/** + * Arrange to exhaust the stack inside the cross-compartment switcher's spill of + * callee-saved state. The result should simply be an error return, rather than + * a forced-unwind. + */ +void exhaust_thread_stack_spill(__cheri_callback void (*fn)()) +{ + register auto rfn asm("ct1") = fn; + register uintptr_t res asm("ca0") = 0; + + __asm__ volatile( + // Save the stack to put back later + "cmove cs0, csp\n" + + // Shrink the available stack space + "cgetbase s1, csp\n" + "addi s1, s1, %[stackleft]\n" + "csetaddr csp, csp, s1\n" + + // Make the call + "1:\n" + "auipcc ct2, %%cheriot_compartment_hi(.compartment_switcher)\n" + "clc ct2, %%cheriot_compartment_lo_i(1b)(ct2)\n" + "cjalr ct2\n" + + "cmove csp, cs0\n" + : /* outs */ "+C"(res) + : /* ins */[stackleft] "i"(sizeof(void *)) + : /* clobbers */ "ct2", "cs0", "cs1"); + + *threadStackTestFailed = false; + TEST(res == -ENOTENOUGHSTACK, "Bad return {}", res); +} + void set_csp_permissions_on_fault(PermissionSet newPermissions) { __asm__ volatile(
diff --git a/tests/stack_tests.h b/tests/stack_tests.h index fbc757b..93ad94b 100644 --- a/tests/stack_tests.h +++ b/tests/stack_tests.h
@@ -8,6 +8,8 @@ __cheri_callback void (*fn)(), bool *outLeakedSwitcherCapability); __cheri_compartment("stack_integrity_thread") void exhaust_thread_stack(); +__cheri_compartment("stack_integrity_thread") void exhaust_thread_stack_spill( + __cheri_callback void (*fn)()); __cheri_compartment("stack_integrity_thread") void set_csp_permissions_on_fault( PermissionSet newPermissions); __cheri_compartment("stack_integrity_thread") void set_csp_permissions_on_call(
diff --git a/tests/static_sealing-test.cc b/tests/static_sealing-test.cc index 046d704..09a7407 100644 --- a/tests/static_sealing-test.cc +++ b/tests/static_sealing-test.cc
@@ -19,6 +19,5 @@ { // Get a pointer to it and ask for it to be unsealed. Sealed<TestType> value{STATIC_SEALED_VALUE(test)}; - test_static_sealed_object(value); - return 0; + return test_static_sealed_object(value); }
diff --git a/tests/static_sealing.h b/tests/static_sealing.h index 5a3402b..99858dd 100644 --- a/tests/static_sealing.h +++ b/tests/static_sealing.h
@@ -10,5 +10,5 @@ int value; }; -void __cheri_compartment("static_sealing_inner") +int __cheri_compartment("static_sealing_inner") test_static_sealed_object(Sealed<TestType>);
diff --git a/tests/static_sealing_inner.cc b/tests/static_sealing_inner.cc index 872bf6f..b8d56a7 100644 --- a/tests/static_sealing_inner.cc +++ b/tests/static_sealing_inner.cc
@@ -4,10 +4,11 @@ #define TEST_NAME "Static sealing (inner compartment)" #include "static_sealing.h" #include "tests.hh" +#include <fail-simulator-on-error.h> using namespace CHERI; -void test_static_sealed_object(Sealed<TestType> obj) +int test_static_sealed_object(Sealed<TestType> obj) { // Get our static sealing key. SKey key = STATIC_SEALING_TYPE(SealingType); @@ -48,4 +49,5 @@ Permission::Global}>(unsealed, 1)), "Incorrect permissions on unsealed statically sealed object {}", unsealed); + return 0; }
diff --git a/tests/test-runner.cc b/tests/test-runner.cc index 0c85f14..c9f3acb 100644 --- a/tests/test-runner.cc +++ b/tests/test-runner.cc
@@ -24,7 +24,7 @@ // On Sail, report the number of instructions, the cycle count is // meaningless. __asm__ volatile("csrr %0, minstret" : "=r"(cycles)); -#elifdef IBEX +#elif defined(IBEX) __asm__ volatile("csrr %0, mcycle" : "=r"(cycles)); #else __asm__ volatile("rdcycle %0" : "=r"(cycles)); @@ -54,7 +54,9 @@ crashDetected = true; } else + { debug_log("{} finished in {} cycles", msg, cycles - startCycles); + } } } // namespace