CKB-VM Changes Under the Meepo Hardfork

CryptapeCryptape
7 min read

In this article, we covered the new features introduced in the Meepo Hardfork. This follow-up looks at the issues this hardfork resolved, the rationale behind each fix, and related technical details relevant to developers.

Fix: Nervos DAO Occupied Capacity Vulnerability

We'd like to thank phroi for identifying and reporting this issue. His contribution made it possible for us to address the DAO vulnerability promptly.

There are two types of Cells throughout three phases (each phase is represented as a CKB transaction) of the complete lifecycle of Nervos DAO:

  1. Deposit Phrase — a deposit cell is created.

  2. Withdraw Phase 1 — The deposit Cell is destroyed and replaced by a withdrawing cell. You can also think of it as the deposit Cell gets updated to a withdrawing Cell.

  3. Withdraw Phase 2 — After the lock period in Withdraw Phase 1, now the withdrawing Cell gets destroyed. The capacity, or CKBytes, in it — together with the interest earned from locking them in Nervos DAO — can be repurposed to build other types of Cells in this phase.

The formula to calculate Nervos DAO interest is related to the blockchain consensus parameters, as well as information exposed by deposit and withdrawing Cells:

  • Interest period — Starts from the block where the Deposit transaction is included, and ends at the block where the Withdraw Phase 1 transaction is included (One can also think of this as from the creation of the Deposit Cell to its destruction). No interest is generated between Withdraw Phase 1 and Withdraw Phase 2 — that duration is simply the lock period for liquidity reasons.

  • Capital—The capital of a Nervos DAO deposit is the total CKBytes in a deposit Cell minus the occupied capacity (CKBytes used for data storage). Occupied capacity includes bytes for the Lock Script, Type Script, capacity field, as well as Cell data used by Nervos DAO.

    For instance: a user deposits 20,000 CKBytes into Nervos DAO using the SECP256K1/blake160 Lock Script included in genesis block. The 20,000 CKBytes capacity of the deposit Cell consists of:

    • Lock Script: 53 bytes

    • Nervos DAO Type Script: 33 bytes

    • Capacity field: 8 bytes

    • Nervos DAO on-chain Script: 8 bytes

In this case:

  • Total occupied capacity = 53 + 33 + 8 + 8 = 102 bytes

  • Capital = 20,000 − 102 = 19,898 CKBytes

In summary, the lifecycle of a Cell in Nervos DAO looks like:

plain cells -> deposit cell -> withdrawing cell -> plain cells

A deposit Cell is created from plain cells in the Deposit Phase, then consumed to create a withdrawing Cell in the Withdraw Phase 1, and finally consumed in Withdraw Phase 2 into plain cells again, but with more CKBytes due to interest.

The Bug & Attack

The current implementation of the Nervos DAO on-chain Script has a bug: Instead of using the occupied capacity of the deposit Cell in the calculation, it mistakenly uses the occupied capacity of the withdraw Cell.

Things would work fine if the withdrawing Cell and the deposit Cell use the same Lock Script, but if the withdrawing Cell uses a smaller Lock Script than the deposit Cell, it creates some free CKBytes used as data storage, and the capital of Nervos DAO deposit.

Assuming a user deposits the same 20,000 CKBytes following the above process into Nervos DAO, but uses a different Lock Script that allows for arbitrary data in addition to the signature, which is forbidden by the SECP256K1/blake160 on-chain Script from genesis block. In the Lock Script’s args field, the user fills in 2,000 bytes of data, including the signature and the arbitrary data. In this case, the Cell capacity consists of:

  • Lock Script: 2,033 bytes

  • Nervos DAO Type Script: 33 bytes

  • Capacity field: 8 bytes

  • Nervos DAO on-chain Script: 8 bytes

Occupied capacity = 2,033 + 33 + 8 + 8 = 2,082 CKBytes

Capital = 20,000 − 2,082 = 17,918 CKBytes

This is the actual attack on Nervos DAO. In Withdraw Phase 1, the user swaps the old Lock Script in the deposit Cell into a standard SECP256K1/blake160 on-chain Script as used above. The occupied capacity for the withdrawing Cell, is thus reduced to 102 CKbytes. Since the current Nervos DAO uses the occupied capacity of the withdrawing Cell, the actual used capital for interest calculation, is therefore: 20,000 - 102 = 19,898 CKBytes. However, the actual CKBytes locked in Nervos DAO, as of the true capital, is 17,918 CKBytes. This means the user can earn an extra interest of 1,980 CKBytes in the original deposit Cell that is used for data storage, and also being locked in Nervos DAO for interest. This violates the DAO’s original design and forms a potential exploit on Nervos DAO.

Note that right now, Nervos DAO on-chain Script does NOT enforce any restrictions on the Lock Scripts of withdrawing Cells in Withdraw Phase 1, making the above attack possible.

We’ve reviewed SECP256K1/blake160, SECP256K1/multisig on-chain Script, and pw-lock on-chain Script. All these Scripts prevent storing arbitrary data in the Lock Script, so most existing Nervos DAO deposit Cells are unaffected by this vulnerability. However, since such vulnerability exists, it should still be fixed to prevent future Lock Scripts from introducing risk.

How to Fix

The most straightforward way to fix this issue is to upgrade the DAO on-chain Script. Unfortunately, it’s not possible because it’s secured by the zero lock Script — a special Lock Script that is unlockable, making the upgrades impossible.

However, we can still resolve this problem at the consensus layer, similar to the Nervos DAO implementation. The solution is to require the withdrawing Cell to use a Lock Script of the exact same size as the deposit Cell. This way, the occupied capacity of the withdrawing Cell will is identical to that of the deposit Cell, ensuring the correct capital is used in the interest calculation. The fix is implemented here.

Fix: Error Code Handling in Memory Protection

The CKB-VM uses the W ^ X security policy, which implements executable space protection by ensuring every memory page (a fixed-size block in a program's virtual address space, the memory layout it uses) is either writable or executable, but never both. Without such protection, a program can write (as data “W”) CPU instructions into a memory area intended for data and then execute them (as executable “X”; or read-execute “RX”). This can be dangerous if the writer is malicious.

The issue reports an incorrect error code that conflated the “freezed” and “executable” states. Both are non‑writable, but they are distinct: the freezed memory includes executable memory, but not vice versa. For example, the .rodata data like constants, literal strings are placed in freezed memory.

Fix: ELF Loading Crash in Rare Cases

In this commit, we fixed a bug that could cause a crash during ELF loading. The problem is a rare, hard-to-detect corner case where the computed end could be zero, as shown in the code below:

let end = addr.wrapping_add(size);

Only carefully crafted ELF files could trigger this issue. To detect such issues, we run a long‑running fuzzing process on dedicated machines. Fuzzing has been an essential practice for the CKB team, uncovering numerous bugs throughout development.

Fix: x0 Register Overwrite in Macro-Operation Fusion

In the previous hard fork, we introduced Macro-Operation Fusion (MOP), a hardware optimization used in many modern micro-architectures. MOP merges multiple adjacent macro-operations into a single macro-operation prior to or during decoding. The fused operation is then decoded into fused µOPs.

In this commit, we fixed some rare cases where the x0 register could be overwritten by the MOP implementation. Per the RISC‑V spec, x0 must always read as zero; any overwrite violates the spec and can cause incorrect downstream behavior. This corner case was uncovered through fuzzing again.

Conclusion

The Meepo Hardfork strengthens correctness and safety across CKB: it fixes the DAO vulnerability and other low-level issues. Continuous fuzzing and review caught these edge cases before they could harm users. With these fixes in place, Meepo unlocks modularity while preserving security at the same time, setting the stage for more capable and reliable dApps.

You can view known security vulnerabilities and report new vulnerabilities privately to maintainers in the CKB Security Advisories.

0
Subscribe to my newsletter

Read articles from Cryptape directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Cryptape
Cryptape