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 .data or .bss, not .rodata.
-
In the toolchain: A linker script could force .rodata to be read-only, but this would require stripping the W flag from input sections, which may not be supported by rust-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
Environment
0.205.0(CKB2023 consensus active)get_consensusRPC — RFC 0048/0049 at epoch0x0)1.1.0(latest on crates.io)riscv64imac-unknown-none-elftargetrust-lldProblem Description
When deploying a CKB contract built with
ckb-std 1.1.0anddefault_alloc!()as a type script on a CKB2023-enabled chain, the transaction fails during verification with:This occurs even though:
Root Cause
The
.rodatasection header in compiled contracts has theW(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!())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
.rodatasection withWAMflags (Write + Alloc + Merge) violates this check.ELF Analysis — Minimal Contract (no
ckb-std)A bare
#![no_std]#![no_main]contract with justmain() -> i8 { 0 }produces a clean binary and executes successfully:Reproduction Steps
1. Create a minimal contract using
ckb-std2. Build
3. Verify the binary
4. Deploy on CKB2023 chain
Result:
MemWriteOnExecutablePageverification failureExpected 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
MemWriteOnExecutablePagebecause CKB-VM detects a writable flag on a section that shares a page with executable code, or interprets the.rodataW flag as a W^X violation.Investigation Findings
The
.rodataW flag comes fromckb-stdinternalsThe
default_alloc!()macro andckb-std's buddy allocator place metadata in sections that the Rust compiler marks as writable. This happens during compilation ofckb-std's own objects, which are then linked into the final binary.Minimal contract (no
ckb-std) worksA contract with zero dependencies and no allocator compiles cleanly with only a
.textsection (AXflags, noW) 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 WAMflags. 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