Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] Double Free in TinyCBOR #258

Closed
Samsung-PSIRT opened this issue Nov 5, 2024 · 8 comments · May be fixed by #261
Closed

[Bug] Double Free in TinyCBOR #258

Samsung-PSIRT opened this issue Nov 5, 2024 · 8 comments · May be fixed by #261

Comments

@Samsung-PSIRT
Copy link

Samsung-PSIRT commented Nov 5, 2024

the following function has side-effect if it fails - makes illusion of memory allocation, while in fact this memory is freed. Then external code tries to free it (because the pointer clearly changed NULL -> some value) and crashes at double free.

Client code:

    // CBOR_PRIVATE_API CborError _cbor_value_dup_string(const CborValue *value, void **buffer, size_t *buflen, CborValue *next);
    // CBOR_INLINE_API CborError cbor_value_dup_text_string(const CborValue *value, char **buffer, size_t *buflen, CborValue *next);
    char* copy = NULL;
    size_t copy_len = 0;
    CborValue copy_next;
    err = cbor_value_dup_text_string(&value, &copy, &copy_len, &copy_next);
    if (copy)
        free(copy); <<<<<<<<<<< DOUBLE FREE

Root cause is here: tinycbor/tinycbor/src/cborparser_dup_string.c

CborError _cbor_value_dup_string(const CborValue *value, void **buffer, size_t *buflen, CborValue *next)
{
    assert(buffer);
    assert(buflen);
    *buflen = SIZE_MAX;
    CborError err = _cbor_value_copy_string(value, NULL, buflen, NULL);
    if (err)
        return err;

    ++*buflen;
    *buffer = malloc(*buflen); <<<<<<<<<<<< ISSUE HERE - CREATES SIDE EFFECT
    if (!*buffer) {
        /* out of memory */
        return CborErrorOutOfMemory;
    }
    err = _cbor_value_copy_string(value, *buffer, buflen, next);
    if (err) {
        free(*buffer); <<<<<<<<<< FREE
        //*buffer = NULL; <<<<<<< AT LEAST THIS WOULD FIX THE ISSUE (thread-unsafe anyway)
        return err;
    }
    return CborNoError;
}

This implementation shows bad practice: making irreversible changes to external entities no matter how the call ends.
This is natural expectation that if a transaction (here: function call) fails, then no side-effects will happen outside of the boundaries of this transaction. In this case _cbor_value_copy_string() fails but external pointer *buffer becomes non-NULL anyway, which causes natural wish to free it (especially if the developer does not check return values).

Crash value given by libFuzzer to cause this error is: 22 bytes

unsigned char data[] = {
    0x80, 0xc8, 0x0c, 0x03, 0x00, 0x30, 0x50, 0x00, 0x00, 0x96, 0xc8, 0x03, 0x00, 0x30, 0x50, 0x00,
    0x00, 0x96, 0xc8, 0x03, 0x00, 0x30
};

Sanitizer says:

==3262412==ERROR: AddressSanitizer: attempting double-free on 0x5020000458b0 in thread T0:
    #0 0x5555556a413a in free (fuzz/TinyCBOR/harness_TinyCBOR_libFuzzer+0x15013a) (BuildId: 49c1aa6bdab23065a6460b92c3bee0684bfa5bb2)
    #1 0x5555556e03be in fuzz_textstring(CborValue const&) fuzz/TinyCBOR/harness_TinyCBOR.cpp:265:9
    #2 0x5555556de53a in fuzz_cbor_decoder(CborValue&) fuzz/TinyCBOR/harness_TinyCBOR.cpp:360:38
    #3 0x5555556de5b7 in fuzz_cbor_decoder(CborValue&) fuzz/TinyCBOR/harness_TinyCBOR.cpp:373:17
    #4 0x5555556de5b7 in fuzz_cbor_decoder(CborValue&) fuzz/TinyCBOR/harness_TinyCBOR.cpp:373:17
    #5 0x5555556de5b7 in fuzz_cbor_decoder(CborValue&) fuzz/TinyCBOR/harness_TinyCBOR.cpp:373:17
    #6 0x5555556ddeb2 in LLVMFuzzerTestOneInput fuzz/TinyCBOR/harness_TinyCBOR.cpp:474:5
    #7 0x5555555ecf1a in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) crtstuff.c
    #8 0x5555555ec529 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) crtstuff.c
    #9 0x5555555ee112 in fuzzer::Fuzzer::ReadAndExecuteSeedCorpora(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile>>&) crtstuff.c
    #10 0x5555555ee630 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile>>&) crtstuff.c
    #11 0x5555555db005 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) crtstuff.c
    #12 0x5555556065b6 in main (/fuzz/TinyCBOR/harness_TinyCBOR_libFuzzer+0xb25b6) (BuildId: 49c1aa6bdab23065a6460b92c3bee0684bfa5bb2)
    #13 0x7ffff7a261c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 6d64b17fbac799e68da7ebd9985ddf9b5cb375e6)
    #14 0x7ffff7a2628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 6d64b17fbac799e68da7ebd9985ddf9b5cb375e6)
    #15 0x5555555cf8c4 in _start (fuzz/TinyCBOR/harness_TinyCBOR_libFuzzer+0x7b8c4) (BuildId: 49c1aa6bdab23065a6460b92c3bee0684bfa5bb2)

0x5020000458b0 is located 0 bytes inside of 1-byte region [0x5020000458b0,0x5020000458b1)
freed by thread T0 here:
    #0 0x5555556a413a in free (fuzz/TinyCBOR/harness_TinyCBOR_libFuzzer+0x15013a) (BuildId: 49c1aa6bdab23065a6460b92c3bee0684bfa5bb2)
    #1 0x5555556fae4a in _cbor_value_dup_string tinycbor/tinycbor/src/cborparser_dup_string.c:109:9
    #2 0x5555556e35d0 in cbor_value_dup_text_string tinycbor/tinycbor/src/cbor.h:394:12
    #3 0x5555556e0378 in fuzz_textstring(CborValue const&) /fuzz/TinyCBOR/harness_TinyCBOR.cpp:263:11
    #4 0x5555556de53a in fuzz_cbor_decoder(CborValue&) /fuzz/TinyCBOR/harness_TinyCBOR.cpp:360:38
    #5 0x5555556de5b7 in fuzz_cbor_decoder(CborValue&) /fuzz/TinyCBOR/harness_TinyCBOR.cpp:373:17
    #6 0x5555556de5b7 in fuzz_cbor_decoder(CborValue&) /fuzz/TinyCBOR/harness_TinyCBOR.cpp:373:17
    #7 0x5555556de5b7 in fuzz_cbor_decoder(CborValue&) /fuzz/TinyCBOR/harness_TinyCBOR.cpp:373:17
    #8 0x5555556ddeb2 in LLVMFuzzerTestOneInput /fuzz/TinyCBOR/harness_TinyCBOR.cpp:474:5
    #9 0x5555555ecf1a in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) crtstuff.c
    #10 0x5555555ec529 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) crtstuff.c
    #11 0x5555555ee112 in fuzzer::Fuzzer::ReadAndExecuteSeedCorpora(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile>>&) crtstuff.c
    #12 0x5555555ee630 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile>>&) crtstuff.c
    #13 0x5555555db005 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) crtstuff.c
    #14 0x5555556065b6 in main (/fuzz/TinyCBOR/harness_TinyCBOR_libFuzzer+0xb25b6) (BuildId: 49c1aa6bdab23065a6460b92c3bee0684bfa5bb2)
    #15 0x7ffff7a261c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 6d64b17fbac799e68da7ebd9985ddf9b5cb375e6)
    #16 0x7ffff7a2628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 6d64b17fbac799e68da7ebd9985ddf9b5cb375e6)
    #17 0x5555555cf8c4 in _start (/fuzz/TinyCBOR/harness_TinyCBOR_libFuzzer+0x7b8c4) (BuildId: 49c1aa6bdab23065a6460b92c3bee0684bfa5bb2)

previously allocated by thread T0 here:
    #0 0x5555556a43d3 in malloc (/fuzz/TinyCBOR/harness_TinyCBOR_libFuzzer+0x1503d3) (BuildId: 49c1aa6bdab23065a6460b92c3bee0684bfa5bb2)
    #1 0x5555556fad19 in _cbor_value_dup_string /tinycbor/tinycbor/src/cborparser_dup_string.c:102:15
    #2 0x5555556e35d0 in cbor_value_dup_text_string /tinycbor/tinycbor/src/cbor.h:394:12
    #3 0x5555556e0378 in fuzz_textstring(CborValue const&) /fuzz/TinyCBOR/harness_TinyCBOR.cpp:263:11
    #4 0x5555556de53a in fuzz_cbor_decoder(CborValue&) /fuzz/TinyCBOR/harness_TinyCBOR.cpp:360:38
    #5 0x5555556de5b7 in fuzz_cbor_decoder(CborValue&) /fuzz/TinyCBOR/harness_TinyCBOR.cpp:373:17
    #6 0x5555556de5b7 in fuzz_cbor_decoder(CborValue&) /fuzz/TinyCBOR/harness_TinyCBOR.cpp:373:17
    #7 0x5555556de5b7 in fuzz_cbor_decoder(CborValue&) /fuzz/TinyCBOR/harness_TinyCBOR.cpp:373:17
    #8 0x5555556ddeb2 in LLVMFuzzerTestOneInput /fuzz/TinyCBOR/harness_TinyCBOR.cpp:474:5
    #9 0x5555555ecf1a in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) crtstuff.c
    #10 0x5555555ec529 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) crtstuff.c
    #11 0x5555555ee112 in fuzzer::Fuzzer::ReadAndExecuteSeedCorpora(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile>>&) crtstuff.c
    #12 0x5555555ee630 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile>>&) crtstuff.c
    #13 0x5555555db005 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) crtstuff.c
    #14 0x5555556065b6 in main (/fuzz/TinyCBOR/harness_TinyCBOR_libFuzzer+0xb25b6) (BuildId: 49c1aa6bdab23065a6460b92c3bee0684bfa5bb2)
    #15 0x7ffff7a261c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 6d64b17fbac799e68da7ebd9985ddf9b5cb375e6)
    #16 0x7ffff7a2628a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 6d64b17fbac799e68da7ebd9985ddf9b5cb375e6)
    #17 0x5555555cf8c4 in _start (/fuzz/TinyCBOR/harness_TinyCBOR_libFuzzer+0x7b8c4) (BuildId: 49c1aa6bdab23065a6460b92c3bee0684bfa5bb2)

SUMMARY: AddressSanitizer: double-free (/fuzz/TinyCBOR/harness_TinyCBOR_libFuzzer+0x15013a) (BuildId: 49c1aa6bdab23065a6460b92c3bee0684bfa5bb2) in free
@thiagomacieira
Copy link
Member

I disagree that you should dereference a pointer when the dup function in question returned an error, but the fix is reasonable.

@thiagomacieira
Copy link
Member

I don't understand how this condition can be reached, at least not with in-memory buffers. With the following test:

    unsigned char buf[] = {
        0x50, 0x00, 0x00, 0x96, 0xc8, 0x03, 0x00, 0x30
    };
    size_t len = sizeof(buf);

    CborParser parser;
    CborValue value;
    uint8_t *buffer = nullptr;
    size_t buflen;
    cbor_parser_init(buf, len, 0, &parser, &value);
    cbor_value_dup_byte_string(&value, &buffer, &buflen, nullptr);

Inside of _cbor_value_dup_string, we exit on the first return err, before either of the points where you pointed out the side-effect could happen.

If you have a testcase that can reach those points, please provide them.

thiagomacieira added a commit to thiagomacieira/tinycbor that referenced this issue Nov 5, 2024
We were returning from the function with the memory we had allocated and
freed, if the second iteration over the string produced a failure that
didn't happen on the first one. This can't happen with pure memory
buffers, but can happen with an external data source that fails to
produce the same contents twice.

I'm documenting that the values in all error conditions except for OOM
are undefined, so one mustn't attempt to use them, even to free. This
does not change behaviour of the library, just documents.

But this commit does make it clear the OOM condition will return a valid
`*buflen` and `next`, the latter of which is new behaviour with this
commit.

Fixes intel#258.

Signed-off-by: Thiago Macieira <[email protected]>
thiagomacieira added a commit to thiagomacieira/tinycbor that referenced this issue Nov 6, 2024
We were returning from the function with the memory we had allocated and
freed, if the second iteration over the string produced a failure that
didn't happen on the first one. This can't happen with pure memory
buffers, but can happen with an external data source that fails to
produce the same contents twice.

I'm documenting that the values in all error conditions except for OOM
are undefined, so one mustn't attempt to use them, even to free. This
does not change behaviour of the library, just documents.

But this commit does make it clear the OOM condition will return a valid
`*buflen` and `next`, the latter of which is new behaviour with this
commit.

Fixes intel#258.

Signed-off-by: Thiago Macieira <[email protected]>
@Samsung-PSIRT
Copy link
Author

crash-9a7c570f506fb55b4166b7dcfada2e08cee83d79.zip

This file would be helpful.
If not, please let us know.

@thiagomacieira
Copy link
Member

crash-9a7c570f506fb55b4166b7dcfada2e08cee83d79.zip

This file would be helpful. If not, please let us know.

That file contains exactly the string "[6.4]". Are you sure you attached the correct file for this bug report? That's the string that was the source of the #259 issue.

In any case, I need a full reproducer: source application and data input both. As I said above, the issue is that parsing the same bytes twice should produce the same error twice, so we shouldn't have reached the point where we call malloc in the first place. I want to understand if you've somehow found another bug.

@Samsung-PSIRT
Copy link
Author

The input file is right.
If you tell me in detail what you need, i will reigster it as an attachment.
ps. We found a bug using libfuzzer/AFL++.

@thiagomacieira
Copy link
Member

libfuzzer/ AFL only drives the TinyCBOR API. I want to know the portion of the code that did use the API and how the inputs were constructed.

I wonder if this problem is a result of #259 because you're using the json2cbor code with an old (buggy) version of TinyCBOR. The following code has no problems for me:

#include <cbor.h>

int main()
{
    static const unsigned char buf[] = {
      0x5b, 0x36, 0x2e, 0x34, 0x5d
    };
    CborParser parser;
    CborValue it;
    CborError err = cbor_parser_init(buf, sizeof(buf), 0, &parser, &it);
    if (err)
        return err;
    err = cbor_value_advance(&it);
    return err;
}

Neither ASan nor Valgrind complain.

With #261 updating the source and the documentation, I consider the problem fixed.

@Samsung-PSIRT
Copy link
Author

Do you think whether it is a real security issue or not?

@thiagomacieira
Copy link
Member

I do not. First, the problem as described lies in the caller code for freeing a pointer after the function returned an error, so I've clarified the documentation to be clearer.

Second, as far as I can tell and with all the testcases you've supplied and the library's own, it's impossible to reach that point in the code. So even if we consider the API to be dangerous, I don't see how the situation of the double free can arise in the first place.

So, no, this is a doc issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants