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

AnalyzeCode functionality #935

Merged
merged 1 commit into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
39 changes: 39 additions & 0 deletions cosmwasm/packages/sgx-vm/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ pub struct CosmCache<S: Storage + 'static, A: Api + 'static, Q: Querier + 'stati
type_querier: PhantomData<Q>,
}

#[derive(PartialEq, Debug)]
pub struct AnalysisReport {
pub has_ibc_entry_points: bool,
pub required_features: HashSet<String>,
}

impl<S, A, Q> CosmCache<S, A, Q>
where
S: Storage + 'static,
Expand Down Expand Up @@ -115,6 +121,39 @@ where
}
}

pub fn deserialize_wasm(wasm_code: &[u8]) -> VmResult<Module> {
deserialize_buffer(wasm_code).map_err(|err| {
VmError::static_validation_err(format!(
"Wasm bytecode could not be deserialized. Deserialization error: \"{}\"",
err
))
})
}

/// Returns true if and only if all IBC entry points ([`REQUIRED_IBC_EXPORTS`])
/// exist as exported functions. This does not guarantee the entry points
/// are functional and for simplicity does not even check their signatures.
pub fn has_ibc_entry_points(module: &impl ExportInfo) -> bool {
let available_exports = module.exported_function_names(None);
REQUIRED_IBC_EXPORTS
.iter()
.all(|required| available_exports.contains(*required))
}

/// Performs static anlyzation on this Wasm without compiling or instantiating it.
///
/// Once the contract was stored via [`save_wasm`], this can be called at any point in time.
/// It does not depend on any caching of the contract.
pub fn analyze(&self, checksum: &Checksum) -> VmResult<AnalysisReport> {
// Here we could use a streaming deserializer to slightly improve performance. However, this way it is DRYer.
let wasm = self.load_wasm(checksum)?;
let module = deserialize_wasm(&wasm)?;
Ok(AnalysisReport {
has_ibc_entry_points: has_ibc_entry_points(&module),
required_features: required_features_from_module(&module),
})
}

/// Returns an Instance tied to a previously saved Wasm.
/// Depending on availability, this is either generated from a cached instance, a cached module or Wasm code.
pub fn get_instance(
Expand Down
20 changes: 20 additions & 0 deletions go-cosmwasm/api/bindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ typedef struct Buffer {
uintptr_t cap;
} Buffer;

/**
* The result type of the FFI function analyze_code.
*
* Please note that the unmanaged vector in `required_features`
* has to be destroyed exactly once. When calling `analyze_code`
* from Go this is done via `C.destroy_unmanaged_vector`.
*/
typedef struct AnalysisReport {
bool has_ibc_entry_points;
/**
* An UTF-8 encoded comma separated list of reqired features.
* This is never None/nil.
*/
Buffer required_features;
} AnalysisReport;

typedef struct EnclaveRuntimeConfig {
uint8_t module_cache_size;
} EnclaveRuntimeConfig;
Expand Down Expand Up @@ -186,6 +202,10 @@ Buffer query(cache_t *cache,
uint64_t *gas_used,
Buffer *err);

AnalysisReport analyze_code(cache_t *cache,
ByteSliceView checksum,
Buffer *error_msg);

/**
* frees a cache reference
*
Expand Down
34 changes: 19 additions & 15 deletions go-cosmwasm/api/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,25 @@ func Query(
return receiveVector(res), uint64(gasUsed), nil
}

func AnalyzeCode(
cache Cache,
codeHash []byte,
) (*v1types.AnalysisReport, error) {
cs := sendSlice(codeHash)
defer runtime.KeepAlive(codeHash)
errMsg := C.Buffer{}
report, err := C.analyze_code(cache.ptr, cs, &errMsg)

if err != nil {
return nil, errorWithMessage(err, errMsg)
}
res := v1types.AnalysisReport{
HasIBCEntryPoints: bool(report.has_ibc_entry_points),
RequiredFeatures: string(receiveVector(report.required_features)),
}
return &res, nil
}

// KeyGen Send KeyGen request to enclave
func KeyGen() ([]byte, error) {
errmsg := C.Buffer{}
Expand Down Expand Up @@ -307,18 +326,3 @@ func errorWithMessage(err error, b C.Buffer) error {
}
return fmt.Errorf("%s", string(msg))
}

func AnalyzeCode(cache Cache, checksum []byte) (*v1types.AnalysisReport, error) {
cs := makeView(checksum)
defer runtime.KeepAlive(checksum)
errmsg := newUnmanagedVector(nil)
report, err := C.analyze_code(cache.ptr, cs, &errmsg)
if err != nil {
return nil, errorWithMessage(err, errmsg)
}
res := types.AnalysisReport{
HasIBCEntryPoints: bool(report.has_ibc_entry_points),
RequiredFeatures: string(copyAndDestroyUnmanagedVector(report.required_features)),
}
return &res, nil
}
37 changes: 4 additions & 33 deletions go-cosmwasm/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,41 +236,12 @@ func (w *Wasmer) Query(
return resp.Ok, gasUsed, nil
}

// AnalyzeCode will statically analyze the code.
// AnalyzeCode returns a report of static analysis of the wasm contract (uncompiled).
// This contract must have been stored in the cache previously (via Create).
// Only info currently returned is if it exposes all ibc entry points, but this may grow later
// Currently just reports if it exposes all IBC entry points.
func (w *Wasmer) AnalyzeCode(
codeHash []byte,
) (*v1types.AnalysisReport, error) {
data, gasUsed, err := api.AnalyzeCode(codeHash)
if err != nil {
return nil, nil, gasUsed, err
}

key := data[0:64]
data = data[64:]

var respV010 v010types.InitResult
jsonErrV010 := json.Unmarshal(data, &respV010)

if jsonErrV010 == nil {
// v0.10 response
if respV010.Err != nil {
return nil, nil, gasUsed, fmt.Errorf("%v", respV010.Err)
}
return respV010.Ok, key, gasUsed, nil
}

var respV1 v1types.ContractResult
jsonErrV1 := json.Unmarshal(data, &respV1)

if jsonErrV1 == nil {
// v1 response
if respV1.Err != "" {
return nil, nil, gasUsed, fmt.Errorf(respV1.Err)
}
return respV1.Ok, key, gasUsed, nil
}

// unidentified response 🤷
return nil, nil, gasUsed, fmt.Errorf("cannot detect response type, v0.10: %v or v0.16: %v", jsonErrV010, jsonErrV1)
return api.AnalyzeCode(w.cache, codeHash)
}
40 changes: 40 additions & 0 deletions go-cosmwasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,46 @@ fn do_query(
Ok(res?)
}

/// The result type of the FFI function analyze_code.
///
/// Please note that the unmanaged vector in `required_features`
/// has to be destroyed exactly once. When calling `analyze_code`
/// from Go this is done via `C.destroy_unmanaged_vector`.
#[repr(C)]
#[derive(Copy, Clone, Default, Debug, PartialEq)]
pub struct AnalysisReport {
pub has_ibc_entry_points: bool,
/// An UTF-8 encoded comma separated list of reqired features.
/// This is never None/nil.
pub required_features: Buffer,
}

#[no_mangle]
pub extern "C" fn analyze_code(
cache: *mut cache_t,
checksum: ByteSliceView,
error_msg: Option<&mut Buffer>,
) -> AnalysisReport {
let r = match to_cache(cache) {
Some(c) => catch_unwind(AssertUnwindSafe(move || do_analyze_code(c, checksum)))
.unwrap_or_else(|_| Err(Error::panic())),
None => Err(Error::unset_arg(CACHE_ARG)),
};
handle_c_error_default(r, error_msg)
}

fn do_analyze_code(
cache: &mut Cache<GoApi, GoStorage, GoQuerier>,
checksum: ByteSliceView,
) -> Result<AnalysisReport, Error> {
let checksum: Checksum = checksum
.read()
.ok_or_else(|| Error::unset_arg(CHECKSUM_ARG))?
.try_into()?;
let report = cache.analyze(&checksum)?;
Ok(report.into())
}

#[no_mangle]
pub extern "C" fn key_gen(err: Option<&mut Buffer>) -> Buffer {
match untrusted_key_gen() {
Expand Down