Skip to content

Fix: Undefined Behavior in BoxedBytes::from_concat on Zero-Length Input#2395

Merged
andrei-marinica merged 1 commit into
masterfrom
boxed-bytes-empty-fix
Jun 4, 2026
Merged

Fix: Undefined Behavior in BoxedBytes::from_concat on Zero-Length Input#2395
andrei-marinica merged 1 commit into
masterfrom
boxed-bytes-empty-fix

Conversation

@andrei-marinica

@andrei-marinica andrei-marinica commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

BoxedBytes::from_concat called alloc(Layout { size: 0, align: 1 }) when all
input slices were empty or the slice list was empty. Calling the global allocator
with a zero-sized layout violates Rust's allocator contract and produces undefined
behavior.

Root Cause

The unsafe allocation path had no guard for result_len == 0. The fix adds an
early return via BoxedBytes::empty() before entering the unsafe block.
BoxedBytes::empty() boxes a [u8; 0] ZST, which the stdlib handles entirely at
compile time using NonNull::dangling() — the allocator is never invoked.

Impact by Allocator

MultiversX smart contracts can be compiled with one of four allocators, each with
different behavior when alloc(size=0) is called:

Allocator alloc(size=0) behavior dealloc behavior Net impact
AllocationForbidden (default) Calls signal_error("memory allocation forbidden") — hard contract abort Same — abort Denial of service. Any endpoint receiving empty byte arguments and calling from_concat unconditionally terminates the contract with a user-visible error, regardless of whether the business logic would otherwise succeed.
WeeAlloc Returns a pointer into an existing free-list node without consuming it Inserts a duplicate entry back into the free list Heap corruption. The free list now contains the same cell twice. A subsequent allocation can return memory that is still reachable via the duplicate entry, producing two live allocations aliasing the same address. This can silently corrupt any live variable allocated after the empty concat — including balances, owner addresses, or storage keys — within the same contract execution. The corruption is deferred and invisible at the call site.
LeakingAllocator Returns 0 as *mut u8 (null pointer) when no pages have been requested yet No-op Null pointer in Box. Technically UB; as_ptr() returns null. In practice harmless because dealloc is a no-op and no data is read through the zero-length slice, but the pointer value is wrong.
StaticAllocator64K Returns a pointer into the arena without advancing head No-op Aliased zero-length pointer. Repeated zero-size allocations all return the same arena address. No memory is corrupted because dealloc is a no-op and nothing is written, but the pointer aliases the arena base.

Severity assessment

  • WeeAlloc: High. Silent heap corruption within a live contract execution.
    An attacker who can supply empty byte arguments to an endpoint that calls
    from_concat can trigger the free-list corruption and cause subsequent
    allocations to alias live contract state.
  • AllocationForbidden: Medium. Reliable denial of service against the
    contract endpoint. No state corruption, but the contract aborts instead of
    executing.
  • LeakingAllocator / StaticAllocator64K: Low. Technically UB; no
    practical corruption observed due to no-op dealloc.

Fix

if result_len == 0 {
    return BoxedBytes::empty();
}

Added before the unsafe block in from_concat. BoxedBytes::empty() never
calls the allocator under any of the four contract allocators.

Tests Added

  • test_concat_single_empty_slice — regression: single &b"" previously hit the UB path
  • test_concat_single_nonempty_slice — single non-empty slice round-trips correctly
  • test_concat_leading_empty_slices — empty slices before a non-empty slice
  • test_concat_trailing_empty_slices — empty slices after a non-empty slice
  • test_concat_result_is_empty_instance — all-empty concat returns a valid, dereferenceable instance

@andrei-marinica andrei-marinica requested a review from Copilot June 3, 2026 14:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@andrei-marinica andrei-marinica requested a review from Copilot June 3, 2026 14:14
@andrei-marinica andrei-marinica marked this pull request as ready for review June 3, 2026 14:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

chain/core/src/types/boxed_bytes.rs:85

  • alloc(layout) may return a null pointer on OOM. Using it unconditionally with add/copy_nonoverlapping is UB in a safe function. Check for null and call alloc::alloc::handle_alloc_error(layout) (or otherwise handle OOM) before using the pointer.
            let layout = Layout::from_size_align(result_len, core::mem::align_of::<u8>()).unwrap();
            let result_ptr = alloc(layout);
            let mut current_index = 0usize;

Comment thread chain/core/src/types/boxed_bytes.rs
@andrei-marinica andrei-marinica changed the title BoxedBytes from_concat empty result fix Fix: Undefined Behavior in BoxedBytes::from_concat on Zero-Length Input Jun 3, 2026
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Contract comparison - from 4267d66 to eba6564

Path                                                                                             size                  has-allocator                     has-format
large-storage.wasm 1644 false None
linked-list-repeat.wasm 6945 false without message
map-repeat.wasm 7483 false without message
queue-repeat.wasm 5536 false None
set-repeat.wasm 6463 false None
single-value-repeat.wasm 4134 false None
vec-repeat.wasm 4826 false None
send-tx-repeat.wasm 1279 false None
str-repeat-mb-builder-basic.wasm 747 false None
str-repeat-mb-builder-cached.wasm 1082 false without message
str-repeat.wasm 2892 false without message
very-large-storage.wasm 977 false None
multiversx-price-aggregator-sc.wasm 17823 false without message
multiversx-wegld-swap-sc.wasm 4393 false None
adder.wasm 699 false None
bonding-curve-contract.wasm 13998 false None
check-pause.wasm 1250 false None
crowdfunding.wasm 3513 false None
crypto-bubbles.wasm 2508 false None
kitty-auction.wasm 9295 false without message
kitty-genetic-alg.wasm 3681 false without message
kitty-ownership.wasm 13017 false without message
crypto-zombies.wasm 9125 false without message
digital-cash.wasm 9565 false None
empty.wasm 244 false None
esdt-transfer-with-fee.wasm 7376 false without message
factorial.wasm 579 false None
fractional-nfts.wasm 8141 false without message
lottery.wasm 11972 false without message
multisig.wasm 13335 false without message
multisig-full.wasm 14979 false without message
multisig-view.wasm 5568 false None
nft-minter.wasm 9594 false without message
nft-storage-prepay.wasm 2583 false None
nft-subscription.wasm 8716 false without message
order-book-factory.wasm 3331 false None
order-book-pair.wasm 14176 false None
ping-pong-egld.wasm 6332 false None
proxy-pause.wasm 4101 false None
rewards-distribution.wasm 9498 false without message
seed-nft-minter.wasm 14164 false without message
token-release.wasm 6907 false without message
abi-tester-ev.wasm 760 false None
abi-tester.wasm 8465 true without message
alloc-mem-leaking.wasm 21833 ➡️ 21845 🔴 (+12) false without message
alloc-mem-fail.wasm 17164 ➡️ 17214 🔴 (+50) true without message
alloc-features.wasm 21669 ➡️ 21681 🔴 (+12) false without message
basic-features-small-int-bug.wasm 824 false None
basic-features.wasm 87105 false without message
basic-features-storage-bytes.wasm 541 false None
big-float-features.wasm 6357 false without message
builtin-func-features.wasm 3768 false None
first-contract.wasm 3370 false None
second-contract.wasm 1145 false None
child.wasm 3937 false without message
parent.wasm 1938 false None
forwarder.wasm 49028 false without message
forwarder-blind.wasm 14018 false without message
forwarder-legacy.wasm 33700 false without message
forwarder-raw-init-sync-call.wasm 2853 false None
forwarder-raw-init-async-call.wasm 2295 false None
forwarder-raw.wasm 13035 false None
local-esdt-and-nft.wasm 12462 false without message
mesh-node.wasm 15701 false without message
proxy-test-first.wasm 5595 false without message
proxy-test-second.wasm 2291 false without message
recursive-caller.wasm 5076 false without message
transfer-role-features.wasm 8424 false without message
vault-upgrade.wasm 698 false None
vault.wasm 8866 false None
crowdfunding-erc20.wasm 4828 false without message
erc1155.wasm 11820 false without message
erc1155-marketplace.wasm 10699 false without message
erc1155-user-mock.wasm 1209 false None
erc20.wasm 1860 false None
erc721.wasm 2232 false None
lottery-erc20.wasm 12782 false without message
esdt-system-sc-mock.wasm 4637 false None
exchange-features.wasm 1494 false None
formatted-message-features.wasm 3562 false without message
multi-contract-example-feature.wasm 680 false None
multi-contract-features-view.wasm 1113 false None
multi-contract-alt-impl.wasm 353 false None
multi-contract-features.wasm 681 false None
panic-message-features.wasm 13036 false with message
panic-message-std.wasm 16074 false with message
payable-features.wasm 5708 false None
rust-snippets-generator-test.wasm 4627 false None
rust-testing-framework-tester.wasm 8404 false None
forbidden-opcodes.wasm 842 false None
scenario-tester.wasm 1374 false None
std-contract.wasm 3469 true without message
use-module.wasm 33106 false without message
use-module-view.wasm 736 false None

@andrei-marinica andrei-marinica merged commit 2f79413 into master Jun 4, 2026
27 checks passed
@andrei-marinica andrei-marinica deleted the boxed-bytes-empty-fix branch June 4, 2026 09:37
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 this pull request may close these issues.

2 participants