The fd_mux_during_frag_fn is called after the mux has received a new frag.
/* fd_mux_during_frag_fn is called after the mux has received a new frag from an in, but before the mux has checked that it was overrun. This callback is not invoked if the mux is backpressured, as it would not try and read a frag from an in in the first place (instead, leaving it on the in mcache to backpressure the upstream producer). in_idx will be the index of the in that the frag was received from. If the producer of the frags is respecting flow control, it is safe to read frag data in any of the callbacks, but it is suggested to copy or read frag data within this callback, as if the producer does not respect flow control, the frag may be torn or corrupt due to an overrun by the reader. If the frag being read from has been overwritten while this callback is running, the frag will be ignored and the mux will not call the process function. Instead it will recover from the overrun and continue with new frags. This function cannot fail. If opt_filter is set to non-zero, it means the frag should be filtered and not passed on to downstream consumers of the mux. The ctx is a user-provided context object from when the mux tile was initialized. seq, sig, chunk, and sz are the respective fields from the mcache fragment that was received. If the producer is not respecting flow control, these may be corrupt or torn and should not be trusted, except for seq which is read atomically. */typedefvoid (fd_mux_during_frag_fn)( void* ctx, ulong in_idx, ulong seq, ulong sig, ulong chunk, ulong sz,int* opt_filter );
Specifically, the parameters seq, sig, chunk, and sz originate from the received mcache fragment. Since the producer could be compromised, these fields are considered untrusted.
Vulnerability Details
In the file fd_poh.c located at src/app/fdctl/run/tiles, when the during_frag process receives data from fd_pack, at the code point [1], the data is directly copied to ctx->_txns without any checks.
And in the subsequent call to the after_frag process, the ctx->_txns in this segment is used by the publish_microblock function. However, when the publish_microblock function uses the data in ctx->_txns as parameters for fd_memcpy , at the code point [2], there are no checks, leading to arbitrary control over the memory source and size, which in turn causes memory corruption issues. There is even a risk of code execution.
Impact Details
Process-to-process memory corruption may lead to the process-to-process RCE between sandboxed tiles.
The attack surface of this vulnerability is when an attacker has arbitrary code execution rights over fd_pack, and then launches a process to process RCE attack on fd_poh. Therefore, we modify the relevant code of the fd_pack process to simulate the situation where the attacker has already obtained the ability to execute code.
The project side realized that the modified content shown by the git diff needs to be synchronized to the local environment. By executing make -j fddev and sudo fddev --no-sandbox, a crash can be triggered.
Proof of Concept
After making the following modifications to the code, executing make -j fddev and then running sudo fddev --no-sandbox will trigger a crash.
static inline void
during_frag( void * _ctx,
ulong in_idx,
ulong seq,
ulong sig,
ulong chunk,
ulong sz,
int * opt_filter ) {
(void)seq;
(void)sig;
(void)opt_filter;
//...
// fd_poh.c:1393
if( FD_UNLIKELY( chunk<ctx->bank_in[ in_idx ].chunk0 || chunk>ctx->bank_in[ in_idx ].wmark || sz>USHORT_MAX ) )
FD_LOG_ERR(( "chunk %lu %lu corrupt, not in range [%lu,%lu]", chunk, sz, ctx->bank_in[ in_idx ].chunk0, ctx->bank_in[ in_idx ].wmark ));
uchar * src = (uchar *)fd_chunk_to_laddr( ctx->bank_in[ in_idx ].mem, chunk );
fd_memcpy( ctx->_txns, src, sz-sizeof(fd_microblock_trailer_t) ); // [1]
fd_memcpy( ctx->_microblock_trailer, src+sz-sizeof(fd_microblock_trailer_t), sizeof(fd_microblock_trailer_t) );
FD_TEST( ctx->_microblock_trailer->bank_idx<ctx->bank_cnt );
/* Indicate to pack tile we are done processing the transactions so
it can pack new microblocks using these accounts. This has to be
done before filtering the frag, otherwise we would not notify
pack that accounts are unlocked in certain cases.
TODO: This is way too late to do this. Ideally we would release
the accounts right after we execute and commit the results to the
accounts database. It has to happen before because otherwise
there's a race where the bank releases the accounts, they get
reuused in another bank, and that bank sends to PoH and gets its
microblock pulled first -- so the bank commit and poh mixin order
is not the same. Ideally we would resolve this a bit more
cleverly and without holding the account locks this much longer. */
fd_fseq_update( ctx->bank_busy[ ctx->_microblock_trailer->bank_idx ], ctx->_microblock_trailer->bank_busy_seq );
*opt_filter = is_frag_for_prior_leader_slot;
//fd_poh.c:1418
//...
}
static void
publish_microblock( fd_poh_ctx_t * ctx,
fd_mux_context_t * mux,
ulong sig,
ulong slot,
ulong hashcnt_delta,
ulong txn_cnt ) {
uchar * dst = (uchar *)fd_chunk_to_laddr( ctx->shred_out_mem, ctx->shred_out_chunk );
FD_TEST( slot>=ctx->reset_slot );
fd_entry_batch_meta_t * meta = (fd_entry_batch_meta_t *)dst;
meta->parent_offset = 1UL+slot-ctx->reset_slot;
meta->reference_tick = (ctx->hashcnt/ctx->hashcnt_per_tick) % ctx->ticks_per_slot;
meta->block_complete = !ctx->hashcnt;
dst += sizeof(fd_entry_batch_meta_t);
fd_entry_batch_header_t * header = (fd_entry_batch_header_t *)dst;
header->hashcnt_delta = hashcnt_delta;
fd_memcpy( header->hash, ctx->hash, 32UL );
dst += sizeof(fd_entry_batch_header_t);
ulong payload_sz = 0UL;
ulong included_txn_cnt = 0UL;
for( ulong i=0UL; i<txn_cnt; i++ ) {
fd_txn_p_t * txn = (fd_txn_p_t *)(ctx->_txns + i*sizeof(fd_txn_p_t));
if( FD_UNLIKELY( !(txn->flags & FD_TXN_P_FLAGS_EXECUTE_SUCCESS) ) ) continue;
fd_memcpy( dst, txn->payload, txn->payload_sz ); //[2] <==== txn->payload and txn->payload_sz is arbitrary controlled. crash here.
payload_sz += txn->payload_sz;
dst += txn->payload_sz;
included_txn_cnt++;
}
header->txn_cnt = included_txn_cnt;
/* We always have credits to publish here, because we have a burst
value of 3 credits, and at most we will publish_tick() once and
then publish_became_leader() once, leaving one credit here to
publish the microblock. */
ulong tspub = (ulong)fd_frag_meta_ts_comp( fd_tickcount() );
ulong sz = sizeof(fd_entry_batch_meta_t)+sizeof(fd_entry_batch_header_t)+payload_sz;
fd_mux_publish( mux, sig, ctx->shred_out_chunk, sz, 0UL, 0UL, tspub );
ctx->shred_out_chunk = fd_dcache_compact_next( ctx->shred_out_chunk, sz, ctx->shred_out_chunk0, ctx->shred_out_wmark );
}