Skip to content

UB/Unsoundness with Region::from_slice in bls host calls #2557

@benluelo

Description

@benluelo

MRE:

$ rustc --version
rustc 1.91.0-nightly (5eda692e7 2025-09-11)
# opt-level=z OR opt-level=s will trigger the issue
RUSTFLAGS='-C target-cpu=mvp -C link-arg=-s -C opt-level=s' cargo rustc -p contract --target wasm32-unknown-unknown --release -Zbuild-std=std,panic_abort -j1
[package]
name = "contract"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
cosmwasm-std = { version = "2.2.1", features = ["cosmwasm_2_1"] }
serde = { version = "1.0.228", features = ["derive"] }
use cosmwasm_std::{
    entry_point, imports::bls12_381_hash_to_g1, Api, Binary, DepsMut, Env, ExternalApi,
    HashFunction, MessageInfo, Response, StdResult, VerificationError,
};

#[entry_point]
pub fn instantiate(_: DepsMut, _: Env, _: MessageInfo, _: ()) -> StdResult<Response> {
    Ok(Response::new())
}

#[derive(serde::Deserialize)]
pub struct ExecuteMsg {
    pub msg: String,
    pub dst: String,
}

#[entry_point]
pub fn execute(_: DepsMut, _: Env, _: MessageInfo, msg: ExecuteMsg) -> StdResult<Response> {
    let res = ExternalApi::new().bls12_381_hash_to_g1(
        HashFunction::Sha256,
        msg.msg.as_bytes(),
        msg.dst.as_bytes(),
    )?;

    Ok(Response::new().add_attribute("res", Binary::new(res.into()).to_string()))
}

calling this contract with {"msg":"msg","dst":"dst"} should result in an attribute res: q8Jck7Y8tSiv6Vksx0WEMEoLHp/ZhmHTs20IWaPIWaptMl2rwTf5m/jjdot4eRgv; however it instead results in res: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.

I believe this is due to Region::from_slice being unsound. in the bls* host functions, Region::from_slice is called with a shared reference to an array on the stack, which is casted to a slice. This slice is then deconstructed in Region::from_slice, and then the pointer to this slice is given to the import to write to. However, since the pointer is pointing to a shared reference, this is undefined behaviour. The compiler sees that this value is never written to, and as such just inlines the original value into all callsites, since the value can never change given that it is a shared reference. (I have only observed this when compiling with opt-level=s or opt-level=z)

I was able to "fix" this in a few ways: using out.as_bytes().try_into().unwrap() rather than just using point directly for the returned value, or by using a vec for point instead of an array. I don't think these are particularly good solutions however, as I'm not sure if they're actually sound or not.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions