Boost _ Firedancer v0.1 34290 - [Blockchain_DLT - Medium] bank tile overflow

Submitted on Thu Aug 08 2024 11:40:14 GMT-0400 (Atlantic Standard Time) by @gln for Boost | Firedancer v0.1

Report ID: #34290

Report type: Blockchain/DLT

Report severity: Medium

Target: https://github.com/firedancer-io/firedancer/tree/e60d9a6206efaceac65a5a2c3a9e387a79d1d096

Impacts:

  • Process to process RCE between sandboxed tiles

Description

Brief/Intro

Bank tile incorrectly pre-allocates memory for storing incoming transactions.

As a result buffer overflow will occur when it tries to process microblock containing several txns.

Vulnerability Details

The bank tile calls fd_bank_abi_txn_init() function to process incoming transactions.

Let's look at the code of this function https://github.com/firedancer-io/firedancer/blob/main/src/disco/bank/fd_bank_abi.c#L270

int
fd_bank_abi_txn_init( fd_bank_abi_txn_t * out_txn,
                      uchar *             out_sidecar,
                      void const *        bank,
                      fd_blake3_t *       blake3,
                      uchar *             payload,
                      ulong               payload_sz,
                      fd_txn_t *          txn,
                      int                 is_simple_vote ) {
  out_txn->signatures_cnt = txn->signature_cnt;
  out_txn->signatures_cap = txn->signature_cnt;
  out_txn->signatures     = (void*)(payload + txn->signature_off);

  fd_blake3_init( blake3 );
  fd_blake3_append( blake3, "solana-tx-message-v1", 20UL );
  fd_blake3_append( blake3, payload + txn->message_off, payload_sz - txn->message_off );
  fd_blake3_fini( blake3, out_txn->message_hash );

  out_txn->is_simple_vote_tx = !!is_simple_vote;

  if( FD_LIKELY( txn->transaction_version==FD_TXN_VLEGACY ) ) {
    sanitized_txn_abi_legacy_message1_t * legacy = &out_txn->message.legacy;
    sanitized_txn_abi_legacy_message0_t * message = &legacy->message.owned;

    out_txn->message.tag = 0UL;

    legacy->is_writable_account_cache_cnt = txn->acct_addr_cnt;
    legacy->is_writable_account_cache_cap = txn->acct_addr_cnt;
    legacy->is_writable_account_cache     = out_sidecar;
    int _is_upgradeable_loader_present = is_upgradeable_loader_present( txn, payload, NULL );
    for( ushort i=0; i<txn->acct_addr_cnt; i++ ) {
      int is_writable = fd_txn_is_writable( txn, i ) &&
                        /* Agave does this check, but we don't need to here because pack
                           rejects these transactions before they make it to the bank.

                           !fd_bank_abi_builtin_keys_and_sysvars_tbl_contains( (const fd_acct_addr_t*)(payload + txn->acct_addr_off + i*32UL) ) */
                        (!is_key_called_as_program( txn, i ) || _is_upgradeable_loader_present);
      legacy->is_writable_account_cache[ i ] = !!is_writable;
    }
    out_sidecar += txn->acct_addr_cnt;
    out_sidecar = (void*)fd_ulong_align_up( (ulong)out_sidecar, 8UL );

    message->account_keys_cnt = txn->acct_addr_cnt;
    message->account_keys_cap = txn->acct_addr_cnt;
    message->account_keys     = (void*)(payload + txn->acct_addr_off);

    message->instructions_cnt = txn->instr_cnt;
    message->instructions_cap = txn->instr_cnt;
    message->instructions     = (void*)out_sidecar;
    for( ulong i=0; i<txn->instr_cnt; i++ ) {
      fd_txn_instr_t * instr = &txn->instr[ i ];
      sanitized_txn_abi_compiled_instruction_t * out_instr = &message->instructions[ i ];

      out_instr->accounts_cnt = instr->acct_cnt;
      out_instr->accounts_cap = instr->acct_cnt;
      out_instr->accounts     = payload + instr->acct_off;

      out_instr->data_cnt = instr->data_sz;
      out_instr->data_cap = instr->data_sz;
      out_instr->data     = payload + instr->data_off;

      out_instr->program_id_index = instr->program_id;
    }
    out_sidecar += txn->instr_cnt*sizeof(sanitized_txn_abi_compiled_instruction_t);

    fd_memcpy( message->recent_blockhash, payload + txn->recent_blockhash_off, 32UL );
    message->header.num_required_signatures        = txn->signature_cnt;
    message->header.num_readonly_signed_accounts   = txn->readonly_signed_cnt;
    message->header.num_readonly_unsigned_accounts = txn->readonly_unsigned_cnt;
    return FD_BANK_ABI_TXN_INIT_SUCCESS;
    ...
}

Transaction is being processed and parsed data is written into 'out_sidecar' and 'out_txn' buffers.

Let's see how pointers to these buffer are handled inside bank tile:

static inline void
after_frag( void *             _ctx,
            ulong              in_idx,
            ulong              seq,
            ulong *            opt_sig,
            ulong *            opt_chunk,
            ulong *            opt_sz,
            ulong *            opt_tsorig,
            int *              opt_filter,
            fd_mux_context_t * mux ) {
  ...
  fd_bank_ctx_t * ctx = (fd_bank_ctx_t *)_ctx;

  uchar * dst = (uchar *)fd_chunk_to_laddr( ctx->out_mem, ctx->out_chunk );
  ulong txn_cnt = (*opt_sz-sizeof(fd_microblock_bank_trailer_t))/sizeof(fd_txn_p_t);

  ulong sanitized_txn_cnt = 0UL;
  ulong sidecar_footprint_bytes = 0UL;
  for( ulong i=0UL; i<txn_cnt; i++ ) {
    fd_txn_p_t * txn = (fd_txn_p_t *)( dst + (i*sizeof(fd_txn_p_t)) );

1.    void * abi_txn = ctx->txn_abi_mem + (sanitized_txn_cnt*FD_BANK_ABI_TXN_FOOTPRINT);
2.    void * abi_txn_sidecar = ctx->txn_sidecar_mem + sidecar_footprint_bytes;

    int result = fd_bank_abi_txn_init( abi_txn, abi_txn_sidecar, ctx->_bank, ctx->blake3, txn->payload, txn->payload_sz, TXN(txn), !!(txn->flags & FD_TXN_P_FLAGS_IS_SIMPLE_VOTE) );
    ...
    fd_txn_t * txn1 = TXN(txn);
    sidecar_footprint_bytes += FD_BANK_ABI_TXN_FOOTPRINT_SIDECAR( txn1->acct_addr_cnt, txn1->addr_table_adtl_cnt, txn1->instr_cnt, txn1->addr_table_lookup_cnt );
    sanitized_txn_cnt++;
  }

Lines #1, #2 - on each loop iteration both pointers are being incremented.

Initially , both txn_abi_mem and txn_sidecar_mem bufferrs were allocated like this:

static void
unprivileged_init( fd_topo_t *      topo,
                   fd_topo_tile_t * tile,
                   void *           scratch ) {
  FD_SCRATCH_ALLOC_INIT( l, scratch );
  fd_bank_ctx_t * ctx = FD_SCRATCH_ALLOC_APPEND( l, alignof( fd_bank_ctx_t ), sizeof( fd_bank_ctx_t ) );
  ...
  ctx->txn_abi_mem = FD_SCRATCH_ALLOC_APPEND( l, FD_BANK_ABI_TXN_ALIGN, MAX_TXN_PER_MICROBLOCK*FD_BANK_ABI_TXN_FOOTPRINT );
3.  ctx->txn_sidecar_mem = FD_SCRATCH_ALLOC_APPEND( l, FD_BANK_ABI_TXN_ALIGN, FD_BANK_ABI_TXN_FOOTPRINT_SIDECAR_MAX );

The issue is that on line #3 txn_sidecar_mem was allocated to store single transaction of max size (FD_BANK_ABI_TXN_FOOTPRINT_SIDECAR_MAX).

To simplify things we will focus on legacy txns.

The sizes of txn_abi_mem and txn_sidecar_mem buffers are 7688 and 18096 bytes.

The maximum number of txns in microblock is 31.

So, when fd_bank_abi_txn_init() will try to process several transactions and store them in these buffers, heap overflow will occur.

To trigger the issue I modified fuzz_txn_parse fuzzer, basically after parsing txn and making sure it is valid the fuzzer tries to store it in pre-allocated buffers.

The logic is exactly the same as in fd_bank_abi_txn_init() function.

Impact Details

Buffer overflow in bank tile during txn processing. Possibility of inter-tile RCE.

Proof of concept

Proof of Concept

How to reproduce:

  1. get archive by using provided gist link

  2. decode and unpack it

$ base64 -d poc.txt > arch.tgz
  1. copy provided fuzzer over fuzz_txn_parse.c fuzzer and build firedancer with 'make -j fuzz-test'

  2. run fuzz_txn_parse fuzzer with included test case:

$./fuzz_txn_parse bug2.bin

Max size 18096
Processing tx 0, footprint_bytes=0
Processing tx 1, footprint_bytes=6304
Processing tx 2, footprint_bytes=12608
Processing tx 3, footprint_bytes=18912
=================================================================
==265260==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x629000004be0 at pc 0x64c9aed8ca98 bp 0x7ffe20d73230 sp 0x7ffe20d73228
WRITE of size 1 at 0x629000004be0 thread T0
    #0 0x64c9aed8ca97 in LLVMFuzzerTestOneInput src/ballet/txn/fuzz_txn_parse.c:292:48
    #1 0x64c9aecb2543 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (build/linux/clang/x86_64/fuzz-test/fuzz_txn_parse+0x5a543) (BuildId: 6f80b66d1c683ddf319ed6b58a068270d87210e6)
    #2 0x64c9aec9c2bf in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (build/linux/clang/x86_64/fuzz-test/fuzz_txn_parse+0x442bf) (BuildId: 6f80b66d1c683ddf319ed6b58a068270d87210e6)
    #3 0x64c9aeca2016 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (build/linux/clang/x86_64/fuzz-test/fuzz_txn_parse+0x4a016) (BuildId: 6f80b66d1c683ddf319ed6b58a068270d87210e6)
    #4 0x64c9aeccbe32 in main (build/linux/clang/x86_64/fuzz-test/fuzz_txn_parse+0x73e32) (BuildId: 6f80b66d1c683ddf319ed6b58a068270d87210e6)
    #5 0x762ce5e29d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #6 0x762ce5e29e3f in __libc_start_main csu/../csu/libc-start.c:392:3
    #7 0x64c9aec96b84 in _start (build/linux/clang/x86_64/fuzz-test/fuzz_txn_parse+0x3eb84) (BuildId: 6f80b66d1c683ddf319ed6b58a068270d87210e6)

0x629000004be0 is located 816 bytes to the right of 18096-byte region [0x629000000200,0x6290000048b0)
allocated by thread T0 here:
    #0 0x64c9aed4ebbe in malloc (build/linux/clang/x86_64/fuzz-test/fuzz_txn_parse+0xf6bbe) (BuildId: 6f80b66d1c683ddf319ed6b58a068270d87210e6)
    #1 0x64c9aed8c260 in LLVMFuzzerTestOneInput src/ballet/txn/fuzz_txn_parse.c:264:26
    #2 0x64c9aecb2543 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (build/linux/clang/x86_64/fuzz-test/fuzz_txn_parse+0x5a543) (BuildId: 6f80b66d1c683ddf319ed6b58a068270d87210e6)
    #3 0x64c9aec9c2bf in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (build/linux/clang/x86_64/fuzz-test/fuzz_txn_parse+0x442bf) (BuildId: 6f80b66d1c683ddf319ed6b58a068270d87210e6)
    #4 0x64c9aeca2016 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (build/linux/clang/x86_64/fuzz-test/fuzz_txn_parse+0x4a016) (BuildId: 6f80b66d1c683ddf319ed6b58a068270d87210e6)
    #5 0x64c9aeccbe32 in main (build/linux/clang/x86_64/fuzz-test/fuzz_txn_parse+0x73e32) (BuildId: 6f80b66d1c683ddf319ed6b58a068270d87210e6)
    #6 0x762ce5e29d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow src/ballet/txn/fuzz_txn_parse.c:292:48 in LLVMFuzzerTestOneInput
Shadow bytes around the buggy address:
  0x0c527fff8920: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c527fff8930: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c527fff8940: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c527fff8950: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c527fff8960: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c527fff8970: fa fa fa fa fa fa fa fa fa fa fa fa[fa]fa fa fa
  0x0c527fff8980: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c527fff8990: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c527fff89a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c527fff89b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c527fff89c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==265260==ABORTING

Last updated