nervosnetwork / nervosnetwork/ckb-std
.rodata section has writable (`W`) flag causing MemWriteOnExecutablePage on CKB2023 (CKB-VM v2)
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 28
- Forks
- 22
- PR merge metrics
- No merged PRs in 30d
Description
Environment
| Component | Version |
|---|---|
| CKB Node | 0.205.0 (CKB2023 consensus active) |
| CKB-VM | v2 (CKB2023 hardfork, confirmed via get_consensus RPC — RFC 0048/0049 at epoch 0x0) |
| ckb-std | 1.1.0 (latest on crates.io) |
| Rust Toolchain | nightly, riscv64imac-unknown-none-elf target |
| Linker | rust-lld |
Problem Description
When deploying a CKB contract built with ckb-std 1.1.0 and default_alloc!() as a type script on a CKB2023-enabled chain, the transaction fails during verification with:
TransactionFailedToVerify: Verification failed Script(TransactionScriptError {
source: Outputs[0].Type,
cause: VM Internal Error: MemWriteOnExecutablePage
})
This occurs even though:
- CKB2023 is confirmed active (hardfork features at epoch 0)
- The ELF binary has properly page-aligned program headers (4KB boundaries)
- Code sections are mapped to separate pages from data sections
Root Cause
The .rodata section header in compiled contracts has the W (writable) flag set, which violates W^X (Write XOR Execute) memory protection enforced by CKB-VM.
ELF Analysis — Failing Contract (with ckb-std + default_alloc!())
Section Headers:
[ 1] .text PROGBITS 0x0000000000200000 004252 00 AX 4096 ← code: A+X ✓
[ 2] .rodata PROGBITS 0x0000000000205000 000168 00 WAM 4096 ← PROBLEM: W flag!
[ 4] .data PROGBITS 0x0000000000208000 0000a8 00 WA 4096 ← data: W+A ✓
[ 5] .bss NOBITS 0x0000000000209000 082000 00 WA 4096 ← heap: W+A ✓
Program Headers:
LOAD 0x001000 0x200000 0x004252 0x004252 R E ← .text
LOAD 0x006000 0x205000 0x000168 0x000168 R ← .rodata (marked R, but section header has W)
LOAD 0x009000 0x208000 0x0000a8 0x0000a8 RW ← .data
LOAD 0x00a000 0x209000 0x000000 0x082000 RW ← .bss (532 KB heap)
The section header flags come from the compiled input objects. CKB-VM appears to validate section header flags (not just program header flags) for W^X compliance. The .rodata section with WAM flags (Write + Alloc + Merge) violates this check.
ELF Analysis — Minimal Contract (no ckb-std)
A bare #![no_std] #![no_main] contract with just main() -> i8 { 0 } produces a clean binary and executes successfully:
Section Headers:
[ 1] .text PROGBITS 0x0000000000200000 000004 00 AX 4096 ← only code, no W flag
Reproduction Steps
1. Create a minimal contract using ckb-std
# Cargo.toml
[package]
name = "test-contract"
version = "0.1.0"
edition = "2021"
[dependencies]
ckb-std = "1.1.0"
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
// src/main.rs
#![no_std]
#![no_main]
use ckb_std::default_alloc;
extern crate alloc;
default_alloc!();
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub fn main() -> i8 {
0 // Always success
}
2. Build
cargo build --release --target riscv64imac-unknown-none-elf
3. Verify the binary
readelf -SW target/riscv64imac-unknown-none-elf/release/test-contract | grep rodata
# Output shows: .rodata PROGBITS ... WAM ← W flag present
4. Deploy on CKB2023 chain
- Deploy the binary as a cell
- Create a transaction that uses this cell as a type script (not just a dep)
- Submit the transaction
Result: MemWriteOnExecutablePage verification failure
Expected Behavior
On CKB2023 (CKB-VM v2), the contract should execute successfully. The CKB-VM v2 architecture maps code and data to separate memory pages, eliminating W^X conflicts for properly structured binaries.
Actual Behavior
Transaction verification fails with MemWriteOnExecutablePage because CKB-VM detects a writable flag on a section that shares a page with executable code, or interprets the .rodata W flag as a W^X violation.
Investigation Findings
The .rodata W flag comes from ckb-std internals
The default_alloc!() macro and ckb-std's buddy allocator place metadata in sections that the Rust compiler marks as writable. This happens during compilation of ckb-std's own objects, which are then linked into the final binary.
Minimal contract (no ckb-std) works
A contract with zero dependencies and no allocator compiles cleanly with only a .text section (AX flags, no W) and executes without issue.
This affects all real contracts
Every contract in our protocol (vault, factory, pool, dex, launchpad, registry) uses ckb-std + default_alloc!() and exhibits the same .rodata WAM flags. The vault is the only one currently tested as an executing type script — but any of them would fail if used as a type script on CKB2023.
Impact
This issue blocks the use of ckb-std-based contracts as executing type scripts on any CKB2023 chain (devnet, testnet, mainnet). Without type script execution, contracts cannot enforce protocol rules on-chain — they can only serve as passive data cells with off-chain validation.
Possible Fixes
-
In
ckb-std: Ensure compiled objects do not place writable data in.rodata. The buddy allocator metadata and other static data should be placed in.dataor.bss, not.rodata. -
In the toolchain: A linker script could force
.rodatato be read-only, but this would require stripping the W flag from input sections, which may not be supported byrust-lld. -
In CKB-VM: If CKB-VM v2 only checks program headers (not section headers), the binary layout may already be compliant — but if it checks section headers, the compiled objects themselves need to be fixed.
Related
- CKB 0.205.0 release: https://github.com/nervosnetwork/ckb/releases/tag/v0.205.0
- CKB2023 RFC: https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0051-ckb2023/0051-ckb2023.md
- ckb-std on crates.io: https://crates.io/crates/ckb-std
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the ckb_std::default_alloc!() macro and the allocator-related objects it pulls into the release binary; reproduce with the shown Cargo project and inspect sections using readelf -SW. Compare the .rodata flags and type-script execution result against the no_std/no_main minimal contract. Done means the allocator-backed contract no longer triggers MemWriteOnExecutablePage and its read-only data is not marked writable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- blockchain, build-system
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100