Skip to content
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

fix: read/write context/permission #93

Merged
merged 20 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions api/lib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,18 @@ func createEventsContract(t *testing.T, cache Cache) []byte {
return createContract(t, cache, "./testdata/events.wasm")
}

func createNumberContract(t *testing.T, cache Cache) []byte {
return createContract(t, cache, "./testdata/number.wasm")
}

func createIntermediateNumberContract(t *testing.T, cache Cache) []byte {
return createContract(t, cache, "./testdata/intermediate_number.wasm")
}

func createCallNumberContract(t *testing.T, cache Cache) []byte {
return createContract(t, cache, "./testdata/call_number.wasm")
}

func createContract(t *testing.T, cache Cache, wasmFile string) []byte {
wasm, err := ioutil.ReadFile(wasmFile)
require.NoError(t, err)
Expand Down Expand Up @@ -1204,3 +1216,165 @@ func TestEventManager(t *testing.T) {
require.NoError(t, err)
require.Equal(t, expectedAttributes, attributes)
}

// This is used for TestDynamicReadWritePermission
type MockQuerier_read_write struct {
Bank BankQuerier
Custom CustomQuerier
usedGas uint64
}

var _ types.Querier = MockQuerier_read_write{}

func (q MockQuerier_read_write) GasConsumed() uint64 {
return q.usedGas
}

func DefaultQuerier_read_write(contractAddr string, coins types.Coins) Querier {
balances := map[string]types.Coins{
contractAddr: coins,
}
return MockQuerier_read_write{
Bank: NewBankQuerier(balances),
Custom: NoCustom{},
usedGas: 0,
}
}

func (q MockQuerier_read_write) Query(request types.QueryRequest, _gasLimit uint64) ([]byte, error) {
marshaled, err := json.Marshal(request)
if err != nil {
return nil, err
}
q.usedGas += uint64(len(marshaled))
if request.Bank != nil {
return q.Bank.Query(request.Bank)
}
if request.Custom != nil {
return q.Custom.Query(request.Custom)
}
if request.Staking != nil {
return nil, types.UnsupportedRequest{"staking"}
}
if request.Wasm != nil {
// This value is set for use with TestDynamicReadWritePermission.
// 42 is meaningless.
return []byte(`{"value":42}`), nil
}
return nil, types.Unknown{}
}

func TestDynamicReadWritePermission(t *testing.T) {
cache, cleanup := withCache(t)
defer cleanup()
checksum_number := createNumberContract(t, cache)
checksum_intermediate_number := createIntermediateNumberContract(t, cache)
checksum_call_number := createCallNumberContract(t, cache)

// init callee
gasMeter1 := NewMockGasMeter(TESTING_GAS_LIMIT)
igasMeter1 := GasMeter(gasMeter1)
calleeStore := NewLookup(gasMeter1)
calleeEnv := MockEnv()
calleeEnv.Contract.Address = "number_addr"
calleeEnvBin, err := json.Marshal(calleeEnv)
require.NoError(t, err)

// init intermediate
gasMeter2 := NewMockGasMeter(TESTING_GAS_LIMIT)
igasMeter2 := GasMeter(gasMeter2)
intermediateStore := NewLookup(gasMeter2)
intermediateEnv := MockEnv()
intermediateEnv.Contract.Address = "intermediate_number_addr"
intermediateEnvBin, err := json.Marshal(intermediateEnv)
require.NoError(t, err)

// init caller
gasMeter3 := NewMockGasMeter(TESTING_GAS_LIMIT)
igasMeter3 := GasMeter(gasMeter3)
callerStore := NewLookup(gasMeter3)
callerEnv := MockEnv()
callerEnv.Contract.Address = "call_number_address"
callerEnvBin, err := json.Marshal(callerEnv)
require.NoError(t, err)

// prepare querier
balance := types.Coins{}
info := MockInfoBin(t, "someone")
querier := DefaultQuerier_read_write(calleeEnv.Contract.Address, balance)

// make api mock with GetContractEnv
api := NewMockAPI()
mockGetContractEnv := func(addr string, inputSize uint64) (Env, *Cache, KVStore, Querier, GasMeter, []byte, uint64, uint64, error) {
if addr == calleeEnv.Contract.Address {
return calleeEnv, &cache, calleeStore, querier, GasMeter(NewMockGasMeter(TESTING_GAS_LIMIT)), checksum_number, 0, 0, nil
} else if addr == intermediateEnv.Contract.Address {
return intermediateEnv, &cache, intermediateStore, querier, GasMeter(NewMockGasMeter(TESTING_GAS_LIMIT)), checksum_intermediate_number, 0, 0, nil
} else {
return Env{}, nil, nil, nil, nil, []byte{}, 0, 0, fmt.Errorf("unexpected address")
}
}
api.GetContractEnv = mockGetContractEnv

// instantiate number contract
start := time.Now()
msg := []byte(`{"value":42}`)
res, _, _, cost, err := Instantiate(cache, checksum_number, calleeEnvBin, info, msg, &igasMeter1, calleeStore, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG)
diff := time.Now().Sub(start)
require.NoError(t, err)
requireOkResponse(t, res, 0)
assert.Equal(t, uint64(0xd50318f0), cost)
t.Logf("Time (%d gas): %s\n", cost, diff)

// instantiate intermediate_number contract
start = time.Now()
msg = []byte(`{"callee_addr":"number_addr"}`)
res, _, _, cost, err = Instantiate(cache, checksum_intermediate_number, intermediateEnvBin, info, msg, &igasMeter2, intermediateStore, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG)
diff = time.Now().Sub(start)
require.NoError(t, err)
requireOkResponse(t, res, 0)
assert.Equal(t, uint64(0xeb087500), cost)
t.Logf("Time (%d gas): %s\n", cost, diff)

// instantiate call_number contract
start = time.Now()
msg = []byte(`{"callee_addr":"intermediate_number_addr"}`)
res, _, _, cost, err = Instantiate(cache, checksum_call_number, callerEnvBin, info, msg, &igasMeter3, callerStore, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG)
diff = time.Now().Sub(start)
require.NoError(t, err)
requireOkResponse(t, res, 0)
assert.Equal(t, uint64(0xedd72560), cost)
t.Logf("Time (%d gas): %s\n", cost, diff)

// fail to execute when calling `add`
// The intermediate_number contract is intentionally designed so that the `add` function has read-only permission.
// The following test fails because of inheritance from read-only permission to read-write permission.
gasMeter4 := NewMockGasMeter(TESTING_GAS_LIMIT)
igasMeter4 := GasMeter(gasMeter4)
intermediateStore.SetGasMeter(gasMeter4)
msg4 := []byte(`{"add":{"value":5}}`)

start = time.Now()
_, _, _, cost, err = Execute(cache, checksum_call_number, callerEnvBin, info, msg4, &igasMeter4, callerStore, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG)
diff = time.Now().Sub(start)
require.ErrorContains(t, err, "It is not possible to inherit from read-only permission to read-write permission")
assert.Equal(t, uint64(0x19369c5f0), cost)
t.Logf("Time (%d gas): %s\n", cost, diff)

// succeed to execute when calling `sub`
// The intermediate_number contract is designed so that the `sub` function has read-write permission.
// The following test succeeds because the permissions are properly inherited.
gasMeter5 := NewMockGasMeter(TESTING_GAS_LIMIT)
igasMeter5 := GasMeter(gasMeter5)
intermediateStore.SetGasMeter(gasMeter5)
msg5 := []byte(`{"sub":{"value":5}}`)

start = time.Now()
_, _, _, cost, err = Execute(cache, checksum_call_number, callerEnvBin, info, msg5, &igasMeter5, callerStore, api, &querier, TESTING_GAS_LIMIT, TESTING_PRINT_DEBUG)
diff = time.Now().Sub(start)
require.NoError(t, err)
requireOkResponse(t, res, 0)
assert.Equal(t, uint64(0x535da85b0), cost)
t.Logf("Time (%d gas): %s\n", cost, diff)

}
Binary file modified api/libwasmvm.aarch64.so
Binary file not shown.
Binary file modified api/libwasmvm.dylib
Binary file not shown.
Binary file modified api/libwasmvm.x86_64.so
Binary file not shown.
Binary file added api/testdata/call_number.wasm
Binary file not shown.
Binary file added api/testdata/intermediate_number.wasm
Binary file not shown.
Binary file added api/testdata/number.wasm
Binary file not shown.
8 changes: 4 additions & 4 deletions libwasmvm/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions libwasmvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ serde_json = "1.0"
thiserror = "1.0"
hex = "0.4"
wasmer = "2.2.1"
serde = { version = "=1.0.103", default-features = false, features = ["derive"] }


[dev-dependencies]
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
Expand Down
84 changes: 79 additions & 5 deletions libwasmvm/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use cosmwasm_vm::{
read_region_vals_from_env, write_value_to_env, Backend, BackendApi, BackendError,
BackendResult, Checksum, Environment, FunctionMetadata, GasInfo, InstanceOptions, Querier,
Storage, WasmerVal,
BackendResult, Checksum, Environment, FunctionMetadata, GasInfo, Instance, InstanceOptions,
Querier, Storage, WasmerVal,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::convert::TryInto;
use std::mem::MaybeUninit;
use wasmer::Module;
use wasmer::{Module, Type};

use crate::cache::{cache_t, to_cache};
use crate::db::Db;
Expand All @@ -29,7 +31,11 @@ const MAX_REGIONS_LENGTH_OUTPUT: usize = 64 * MI;
pub struct api_t {
_private: [u8; 0],
}

// This contains property about the function of callee
#[derive(Serialize, Deserialize)]
struct CalleeProperty {
is_read_only: bool,
}
// These functions should return GoError but because we don't trust them here, we treat the return value as i32
// and then check it when converting to GoError manually
#[repr(C)]
Expand Down Expand Up @@ -254,8 +260,17 @@ impl BackendApi for GoApi {
Err(e) => return (Err(BackendError::unknown(e.to_string())), gas_info),
};
callee_instance.env.set_serialized_env(&contract_env);
callee_instance.set_storage_readonly(caller_env.is_storage_readonly());
gas_info.cost += instantiate_cost;
// set read-write permission to callee instance
let is_read_write_permission = match get_read_write_permission(
&mut callee_instance,
caller_env.is_storage_readonly(),
func_info,
) {
Ok(permission) => permission,
Err(e) => return (Err(e), gas_info),
};
callee_instance.set_storage_readonly(is_read_write_permission);

// check callstack
match caller_env.try_pass_callstack(&mut callee_instance.env) {
Expand Down Expand Up @@ -384,6 +399,65 @@ fn into_backend(db: Db, api: GoApi, querier: GoQuerier) -> Backend<GoApi, GoStor
}
}

fn get_read_write_permission(
callee_instance: &mut Instance<GoApi, GoStorage, GoQuerier>,
is_caller_permission: bool,
func_info: &FunctionMetadata,
) -> Result<bool, BackendError> {
callee_instance.set_storage_readonly(true);
let callee_info = FunctionMetadata {
module_name: String::from(&func_info.module_name),
name: "_get_callable_points_properties".to_string(),
signature: ([], [Type::I32]).into(),
};
let callee_ret = match callee_instance.call_function_strict(
&callee_info.signature,
&callee_info.name,
&[],
) {
Ok(ret) => {
let ret_datas = match read_region_vals_from_env(
&callee_instance.env,
&ret,
MAX_REGIONS_LENGTH_OUTPUT,
true,
) {
Ok(v) => v,
Err(e) => return Err(BackendError::dynamic_link_err(e)),
};
Ok(ret_datas)
}
Err(e) => Err(BackendError::dynamic_link_err(e.to_string())),
};
let callee_ret = match callee_ret {
Ok(ret) => ret,
Err(e) => return Err(e),
};
let callee_func_map: HashMap<String, CalleeProperty> =
match serde_json::from_slice(&callee_ret[0]) {
Ok(ret) => ret,
Err(e) => return Err(BackendError::dynamic_link_err(e.to_string())),
};
let callee_info = match callee_func_map.get(&func_info.name) {
Some(val) => val,
None => {
return Err(BackendError::dynamic_link_err(format!(
"callee_func_map has not key:{}",
&func_info.name
)))
}
};

if is_caller_permission && !callee_info.is_read_only {
// An error occurs because read-only permission cannot be inherited from read-write permission
return Err(BackendError::dynamic_link_err(
"It is not possible to inherit from read-only permission to read-write permission",
));
}

Ok(callee_info.is_read_only)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down