Remove a potential call to fwrite with null buffer. Add UBsan testing #239
No reviewers
Labels
No labels
api-spec
bug
CLA requested
CLA valid
compliance
DO NOT MERGE
duplicate
enhancement
feature branch
good first issue
HEADER FORMATTING ISSUE
help wanted
Information
invalid
Jira status: AWAITING REVIEW
Jira status: CLOSED
Jira status: DONE
Jira status: IN PROGRESS
Jira status: IN REVIEW
Jira status: OPEN
Jira status: REOPENED
Jira status: RESOLVED
mirrored
mirror internally
needs: backports
needs: ci
needs: design review
needs: preceding PR
needs: review
needs: work
notabug
open for community contribution
Product Backlog
question
ready for merge
security
stale
type: bug
type: enhancement
type: question
web pages
wontfix
writing
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gh-ARMmbed/mbed-crypto!239
Loading…
Reference in a new issue
No description provided.
Delete branch "psa-its-file-fixes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This PR handles issue https://github.com/ARMmbed/mbed-crypto/issues/203.
First commit removes a potential call to fwrite with a null buffer, and second extends testing.
Tested locally on Ubuntu 16.04 with gcc 5.4 - after extending the testing in CMakeLists and before adding the null check Asan type build fails (and therefore all.sh too). With these two commits - everything passes.
Before merging, this PR will also need a changelog entry to credit nanokatze who has filed this issue.
@ -214,9 +214,12 @@ psa_status_t psa_its_set( psa_storage_uid_t uid,n = fwrite( &header, 1, sizeof( header ), stream );Is this enough? The Doxygen for this function says, among others, it will return
PSA_ERROR_INVALID_ARGUMENT, if:If the docs are up to date, I suggest returning the above error.
@ -214,9 +214,12 @@ psa_status_t psa_its_set( psa_storage_uid_t uid,n = fwrite( &header, 1, sizeof( header ), stream );It is not required for the implementation to report this error code in the case of an invalid memory reference, as a memory access violation is also a reasonable outcome of such a call. But an implementation is permitted to detect and report invalid memory references using this error code.
It would be helpful if the spec provided guidance on how memory buffers are handled in the API, for comparison see the PSA-FF and PSA Crypto specifications.
In this API
p_datashould be permitted to have any value ifdata_length==0. In that specific case:fwrite()has fully defined behaviour (do nothing)psa_its_set()has agreed behaviour (but it is not explicit in the spec), when it will create a zero-length data asset.However, I can see that avoiding the potential UB when
(data_length > 0 && p_data == NULL)is important due to the risk of leaving a half-populated ITS storage file in the system. So maybe you need:further up the function, before creating/opening the file and storing any data.
This is ok in that it makes the test pass, but the check could be made more robust at zero cost.
@ -214,9 +214,12 @@ psa_status_t psa_its_set( psa_storage_uid_t uid,n = fwrite( &header, 1, sizeof( header ), stream );data_length == 0 && p_data == NULLis a valid use case. It creates an empty file. Crypto is unlikely to need it, but this implementation of ITS is meant to be generic.data_length != 0 && p_data == NULLis not a valid input. In the PSA Crypto API, there is implementation guidance to use the normal handling of invalid pointer dereferences (typically terminating the application and if possible logging the fault), and not return an error from the function. (The rationale for this guidance is that returning an error has no benefit since it's a programmer error and not a recoverable error, it makes errors harder to debug, it confuses static analyzers that have heuristics around nullable pointers by making them think that a null input is ok, and the code to check for null is pure overhead with no benefit.) I recommend following this guidance elsewhere in PSA.This should be
if( data_length == 0 ). Checking for null silently writes nothing instead of faulting ifp_datais null butdata_length != 0.Updated the PR. Did not add an additional test to fill in the testing gap after removing MEMORY_BUFFER_ALLOC, since it's bound to be removed anyway here: https://github.com/ARMmbed/mbedtls/pull/2469
I'd prefer not to have that check, mainly because it fools static analyzers into thinking that it's ok to call this function with
p_data == NULL, also because it makes calls with NULL harder to debug. However, the ITS interface explicitly documents that the function returnsPSA_ERROR_INVALID_ARGUMENTin that case. So we need to make a call between friendliness to static analysis (and ease of debugging) and compliance. I vote for static analysis. @Patater What's your call?I think not being compliant will complicate compliance testing, which hurts maintainability as much as fooling static analyzers does. Can we change the standard?
How important compliance is depends on what the objective of
psa_its.cis. It started out as solely for testing Mbed Crypto without relying on any third-party code. But I believe @paulhowardarm now intends to use it in production on Linux.PSA storage is now officially at 1.0, so we can't change it until some future release (1.1 or 2.0).
Ah, this is for PSA ITS File. We don't care about compliance with this code. I'm up for smaller code size and more effective static analysis: which means no NULL check here. We don't intend to do compliance testing on our PSA ITS file implementation.
@simonqhughes is working on implementing PSA ITS for Linux. He used our adapter as a starting point, but has built on it quite a bit. He intends his implementation to pass compliance. @paulhowardarm should get in touch and use that implementation.
I filed a change request internally: https://github.com/ARMmbed/psa_trusted_storage_api/issues/86
I don't think that it is required that the API returns
INVALID_ARGUMENTin case of an invalid buffer pointer (not all are easy to detect unless we are in a PSA-FF type environment).buf==NULLis an example of a argument error can can result in that error code.The basic principle: as for almost any library API ever written, passing garbage pointers can have undefined behaviour, ranging from application termination to loss of control flow when overwriting the stack to bricking the device if writing to some flash control register.
The concern here is that because:
p_data==NULL && len>0tofwrite()isUNDEFINED BEHAVIOUR, ANDfwrite()will fall into this UB trap, ANDSo can we manage without the NULL check in
psa_it_set():data_len > 0check eliminate the problem cases?psa_its_set()where it can see a potential data flow path which results inp_data==NULL? Can we ensure that doesn't happen?Side note: On the the whole UB is evil, it would have been much better if the ISO C standards committee had been less lazy and documented reasonable constraints on implementation behaviour in these circumstances
Wrapping the
fwritecall inif (data_len > 0)(with noelse) makes the function behave correctly when called with valid arguments (p_datais valid ordata_lenis zero).In PSA Crypto, at the moment, no. In the future, maybe, but of course only with
data_len==0. Unless there's a bug in PSA crypto (or a compiler bug, or a runtime glitch…).If they'd done that, they'd be called the ISO Rust standards committee. There's no realistic way to limit the behavior of
free(p); *p = 3;on common hardware. Most processors don't even have a nasal demon peripheral.Sorry, I meant: does adding just the
data_len>0check stop the UB sanitizer complaining about UB calls tofwrite().Well, those are the bugs we want UB Sanitizer to detect - definite cases of passing NULL pointers without zero length data.
By 'reasonable constraints' I meant that the implementation doesn't do "anything surprising". In your scenario, causing an exception, or overwriting some other object with
3is "reasonable behaviour" - even if the consequences are hard to enumerate. The compiler deciding to generate a program that computes PI to a million decimal places instead of attempting to do what the program appears to do is not "reasonable behaviour", but this is permitted by the standard. For more background see https://blog.regehr.org/archives/213Adding the check for
data_len > 0fixes the actual problem and also causes UBsan not to complain. UBsan analyzed this correctly.In practice,
free(p); *p = 3;(or some variant likefree(p); p->foo = 3;) corrupts data structures used by malloc and the behavior is truly undefined up to the capabilities of the hardware. Especially for us in the security business, where the choice to run this particular code is likely to have been made by an adversary whose objective is to do something that the defender considers unreasonable. Removing this undefined behavior hole means imposing constraints on memory management, and then C wouldn't be C anymore. I'm not saying that I like it, but C is what it is.You underestimate the potential interpretation of the standard. The C standard considers that the compiler and runtime and system are all "the C abstract machine" that interprets the program.
We might expect that
free(p); *p = 3;results in some machine code like:after which all hell could break loose in the system because we have just scribbled on allocator meta-data. This is only a moderately bad result.
The C standard says that because writing to memory that has been freed is undefined behaviour, the program is incorrect and the "C abstract machine" can do whatever it wants. In practice the compiler detecting this might eliminate output code that only serves to implement scenarios that exhibit undefined behaviour - so here it might choose to make the entire function containing the above code a NOP.
It is not possible for the developer to reason about the impact that such unexpected transformations will have on the operation of their code.
I'm well aware of what undefined behavior is, thank you. My remark was about how to approach UB when thinking about security. As a defender, the worst result is that the attacker gets to execute arbitrary code in your execution space.
And the secondmost reliable way to avoid UB is to root it out with static analysis. (The most reliable is of course to program in a language that doesn't have UB.) Hence my push towards 1. actually using static analysis tools and 2. making the code friendly to these tools.
Sorry, I should have not started my reply with "you" - this wasn't meant to be a personal response.
I totally agree in the importance of using static tooling to identify UB, and ensuring that the code is both friendly to such tools and that all reports of issues from the tool are followed up.
My comment was attempting to dispel the myth that the results of UB in source code are merely the side effects of memory scribbles or numeric overflows (or similar) that result from the execution of the expected machine code. UB makes these expectations invalid in the long run (new versions or different toolchains), if not at present.
Which just reinforces the point about the need for using static tools that help identify and eliminate UB.
Ok, so do we agree on removing this check?
Done.