-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handling of LengthNotEnough Error #14
Comments
Hey @jlguochn, thank you for publicly expressing your interest in iCKB by auditing the proposal and L1 scripts source code as part of the Scalebit external audit, I personally appreciate a lot!! 🙏 BackgroundIn the beginning I was using the high-level calls (which internally handles this error) until @XuJiandong raised an issue on the high level calls usage, that made me switch to the low-level syscalls. At that point I raised this issue on the Back to us, as per Partial Loading section in VM Syscalls RFC:
Usage of low-level syscall in the iCKB ScriptsLet's take a step back and examine all the low-level syscalls in the current v1-core version: user@host:~/ickb/v1-core/scripts/contracts$ grep --recursive --line-number -i 'syscalls' .
./limit_order/src/entry.rs:12: syscalls::load_cell_data,
./ickb_logic/src/utils.rs:5: syscalls::{load_cell_data, SysError},
./ickb_logic/src/celltype.rs:11: syscalls::SysError,
./owned_owner/src/entry.rs:7: syscalls::{load_cell_data, SysError},
./utils/src/utils.rs:9: syscalls::{load_cell_data, load_header, load_input_by_field},
./utils/src/dao.rs:4: syscalls::{load_cell_data, SysError}, Any of the following calls could potentially raise a
So let's examine all their uses in the current v1-core version: user@host:~/ickb/v1-core/scripts/contracts$ grep --recursive --line-number -E 'load_cell_data\(|load_header\(|load_input_by_field\(' .
./limit_order/src/entry.rs:166: if load_cell_data(&mut data, 0, index, source)? != data.len() {
./ickb_logic/src/utils.rs:20: let mut raw_data = match load_cell_data(&mut data, 0, index, source) {
./owned_owner/src/entry.rs:80: let d = match load_cell_data(&mut data, 0, index, source) {
./utils/src/utils.rs:40: match load_cell_data(&mut data, 0, index, source) {
./utils/src/utils.rs:56: match load_header(&mut data, AR_OFFSET, index, source) {
./utils/src/utils.rs:76: match load_input_by_field(&mut d, 0, index, source, InputField::OutPoint) {
./utils/src/dao.rs:19: match load_cell_data(&mut data, 0, index, source) {
./utils/src/dao.rs:28: match load_cell_data(&mut data, 0, index, source) { Let's group the use-cases by expected data length. Data must be at least of the expected length./ickb_logic/src/utils.rs:20let mut data = [0u8; RECEIPT_SIZE];
let mut raw_data = match load_cell_data(&mut data, 0, index, source) {
Ok(RECEIPT_SIZE) | Err(SysError::LengthNotEnough(_)) => data.as_slice(),
Ok(_) => return Err(Error::Encoding),
Err(err) => return Err(Error::from(err)),
}; Receipt data must be at least ./owned_owner/src/entry.rs:81let mut data = [0u8; OWNED_DISTANCE_SIZE];
let d = match load_cell_data(&mut data, 0, index, source) {
Ok(OWNED_DISTANCE_SIZE) | Err(SysError::LengthNotEnough(_)) => i32::from_le_bytes(data),
Ok(_) => return Err(Error::Encoding),
Err(err) => return Err(Error::from(err)),
}; Owner data must be at least ./utils/src/utils.rs:40:let mut data = [0u8; UDT_SIZE];
match load_cell_data(&mut data, 0, index, source) {
Ok(UDT_SIZE) | Err(SysError::LengthNotEnough(_)) => Ok(u128::from_le_bytes(data)),
Ok(_) => Err(SysError::Encoding),
Err(err) => Err(err),
} UDT data must be at least ./utils/src/utils.rs:56let mut data = [0u8; AR_SIZE];
match load_header(&mut data, AR_OFFSET, index, source) {
Ok(AR_SIZE) | Err(SysError::LengthNotEnough(_)) => Ok(u64::from_le_bytes(data)),
Ok(_) => Err(SysError::Encoding),
Err(err) => Err(err),
} AR data must be at least Data must be of the exact expected length./limit_order/src/entry.rs:166let mut data = [0u8; UDT_SIZE + ORDER_SIZE];
if load_cell_data(&mut data, 0, index, source)? != data.len() {
return Err(Error::Encoding);
} The data length must be exactly of ./utils/src/utils.rs:76let mut d = [0u8; OUT_POINT_SIZE];
match load_input_by_field(&mut d, 0, index, source, InputField::OutPoint) {
Ok(OUT_POINT_SIZE) => Ok(MetaPoint {
tx_hash: Some(d[..TX_HASH_SIZE].try_into().unwrap()),
index: i64::from(u32::from_le_bytes(d[TX_HASH_SIZE..].try_into().unwrap())),
}),
Ok(_) => Err(SysError::Encoding),
Err(err) => Err(err),
} The data length must be exactly of ./utils/src/dao.rs:19pub fn is_deposit_data(index: usize, source: Source) -> bool {
let mut data = DAO_DEPOSIT_DATA;
match load_cell_data(&mut data, 0, index, source) {
Ok(DAO_DEPOSIT_DATA_SIZE) => data == DAO_DEPOSIT_DATA,
_ => false,
}
} Already discussed in: #9 The
As per NervosDAO RFC, the deposit data:
Given this reasoning, by all means if the cell data is not exactly of 8 bytes filled with all zeros, then is not a deposit and this function must return ./utils/src/dao.rs:28:pub fn is_withdrawal_request_data(index: usize, source: Source) -> bool {
let mut data = DAO_DEPOSIT_DATA;
match load_cell_data(&mut data, 0, index, source) {
Ok(DAO_DEPOSIT_DATA_SIZE) => data != DAO_DEPOSIT_DATA,
_ => false,
}
} Similarly to before, the
As per NervosDAO RFC, the withdrawal request data:
Given this reasoning, by all means if the cell data is not exactly of 8 bytes (not all zeroed), then is not a withdrawal request and this function must return Discussion
@jlguochn could you point out where and how exactly? |
Thank you for your detailed explanation regarding the handling of the LengthNotEnough error and background. Your breakdown is very detailed and clear. The reason I initially raised this issue is that I am not certain whether all instances that lead to a LengthNotEnough error necessarily indicate that the data meets expectations. To me, LengthNotEnough seems more like a potential error rather than an expected condition. In my view, this behavior essentially treats the truncated data as valid without any further checks, but I am unsure whether all truncated data necessarily meets expectations, which might introduce potential risks or deviate from best practices. Based on your detailed description, I haven't found direct exploitation vectors or security vulnerabilities. |
Well, that's the thing, we don't know which other data will be there: anyone can develop a lock on top of our script that needs to store additional data for whatever reason. There is no way to check this additional data validity, so we need to restrict our checks to the data owned by our Script. Underlying idea: we need to validate just the right amount of data, not too little (as it could result in vulnerabilities), not too much (as it could result in future compatibility issues with other scripts). |
In several functions, such as:
v1-core/scripts/contracts/utils/src/utils.rs
Line 41 in 92be3c3
v1-core/scripts/contracts/utils/src/utils.rs
Line 57 in 92be3c3
v1-core/scripts/contracts/ickb_logic/src/utils.rs
Line 21 in 92be3c3
It seems that the LengthNotEnough error is not being handled in a specific way. While this approach might be valid in certain contexts, it could potentially allow incomplete data to be treated as valid without additional checks.
Perhaps it would be worth considering a more explicit handling of the LengthNotEnough error to ensure that the data is fully valid before proceeding. Alternatively, logging such occurrences could help in diagnosing potential issues.
The text was updated successfully, but these errors were encountered: