Embed runtime version as a custom section (#8688)

* emit a custom section from impl_runtime_apis!

This change emits a custom section from the impl_runtime_apis! proc macro.

Each implemented API will result to emitting a link section `runtime_apis`.
During linking all sections with this name will be concatenated and
placed into the final wasm binary under the same name.

* Introduce `runtime_version` proc macro

This macro takes an existing `RuntimeVersion` const declaration, parses
it and emits the version information in form of a linking section.
Ultimately such a linking section will result into a custom wasm
section.

* Parse custom wasm section for runtime version

* Apply suggestions from code review

Co-authored-by: David <dvdplm@gmail.com>

* Fix sc-executor integration tests

* Nits

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Refactor apis section deserialization

* Fix version decoding

* Reuse uncompressed value for CallInWasm

* Log on decompression error

* Simplify if

* Reexport proc-macro from sp_version

* Merge ReadRuntimeVersionExt

* Export `read_embedded_version`

* Fix test

* Simplify searching for custom section

Co-authored-by: David <dvdplm@gmail.com>
Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
This commit is contained in:
Sergei Shulepov
2021-05-12 15:39:08 +02:00
committed by GitHub
parent 4f7c1df31e
commit 0849bcce0e
27 changed files with 833 additions and 289 deletions
+102 -37
View File
@@ -29,7 +29,10 @@ use sp_core::traits::{Externalities, RuntimeCode, FetchRuntimeCode};
use sp_version::RuntimeVersion;
use std::panic::AssertUnwindSafe;
use std::path::{Path, PathBuf};
use sc_executor_common::wasm_runtime::{WasmModule, WasmInstance};
use sc_executor_common::{
wasm_runtime::{WasmModule, WasmInstance},
runtime_blob::RuntimeBlob,
};
use sp_wasm_interface::Function;
@@ -235,6 +238,9 @@ impl RuntimeCache {
None => {
let code = runtime_code.fetch_runtime_code().ok_or(WasmError::CodeNotFound)?;
#[cfg(not(target_os = "unknown"))]
let time = std::time::Instant::now();
let result = create_versioned_wasm_runtime(
&code,
code_hash.clone(),
@@ -246,9 +252,22 @@ impl RuntimeCache {
self.max_runtime_instances,
self.cache_path.as_deref(),
);
if let Err(ref err) = result {
log::warn!(target: "wasm-runtime", "Cannot create a runtime: {:?}", err);
match result {
Ok(ref result) => {
#[cfg(not(target_os = "unknown"))]
log::debug!(
target: "wasm-runtime",
"Prepared new runtime version {:?} in {} ms.",
result.version,
time.elapsed().as_millis(),
);
}
Err(ref err) => {
log::warn!(target: "wasm-runtime", "Cannot create a runtime: {:?}", err);
}
}
Arc::new(result?)
}
};
@@ -278,16 +297,11 @@ impl RuntimeCache {
pub fn create_wasm_runtime_with_code(
wasm_method: WasmExecutionMethod,
heap_pages: u64,
code: &[u8],
blob: RuntimeBlob,
host_functions: Vec<&'static dyn Function>,
allow_missing_func_imports: bool,
cache_path: Option<&Path>,
) -> Result<Arc<dyn WasmModule>, WasmError> {
use sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT;
let code = sp_maybe_compressed_blob::decompress(code, CODE_BLOB_BOMB_LIMIT)
.map_err(|e| WasmError::Other(format!("Decompression error: {:?}", e)))?;
match wasm_method {
WasmExecutionMethod::Interpreted => {
// Wasmi doesn't have any need in a cache directory.
@@ -297,7 +311,7 @@ pub fn create_wasm_runtime_with_code(
drop(cache_path);
sc_executor_wasmi::create_runtime(
&code,
blob,
heap_pages,
host_functions,
allow_missing_func_imports,
@@ -306,7 +320,6 @@ pub fn create_wasm_runtime_with_code(
}
#[cfg(feature = "wasmtime")]
WasmExecutionMethod::Compiled => {
let blob = sc_executor_common::runtime_blob::RuntimeBlob::new(&code)?;
sc_executor_wasmtime::create_runtime(
sc_executor_wasmtime::CodeSupplyMode::Verbatim { blob },
sc_executor_wasmtime::Config {
@@ -343,6 +356,55 @@ fn decode_version(version: &[u8]) -> Result<RuntimeVersion, WasmError> {
}
}
fn decode_runtime_apis(apis: &[u8]) -> Result<Vec<([u8; 8], u32)>, WasmError> {
use std::convert::TryFrom;
use sp_api::RUNTIME_API_INFO_SIZE;
apis.chunks(RUNTIME_API_INFO_SIZE)
.map(|chunk| {
// `chunk` can be less than `RUNTIME_API_INFO_SIZE` if the total length of `apis` doesn't
// completely divide by `RUNTIME_API_INFO_SIZE`.
<[u8; RUNTIME_API_INFO_SIZE]>::try_from(chunk)
.map(sp_api::deserialize_runtime_api_info)
.map_err(|_| {
WasmError::Other(format!(
"a clipped runtime api info declaration"
))
})
})
.collect::<Result<Vec<_>, WasmError>>()
}
/// Take the runtime blob and scan it for the custom wasm sections containing the version information
/// and construct the `RuntimeVersion` from them.
///
/// If there are no such sections, it returns `None`. If there is an error during decoding those
/// sections, `Err` will be returned.
pub fn read_embedded_version(
blob: &RuntimeBlob,
) -> Result<Option<RuntimeVersion>, WasmError> {
if let Some(version_section) = blob.custom_section_contents("runtime_version") {
// We do not use `decode_version` here because the runtime_version section is not supposed
// to ever contain a legacy version. Apart from that `decode_version` relies on presence
// of a special API in the `apis` field to treat the input as a non-legacy version. However
// the structure found in the `runtime_version` always contain an empty `apis` field. Therefore
// the version read will be mistakingly treated as an legacy one.
let mut decoded_version = sp_api::RuntimeVersion::decode(&mut &version_section[..])
.map_err(|_|
WasmError::Instantiation("failed to decode verison section".into())
)?;
// Don't stop on this and check if there is a special section that encodes all runtime APIs.
if let Some(apis_section) = blob.custom_section_contents("runtime_apis") {
decoded_version.apis = decode_runtime_apis(apis_section)?.into();
}
Ok(Some(decoded_version))
} else {
Ok(None)
}
}
fn create_versioned_wasm_runtime(
code: &[u8],
code_hash: Vec<u8>,
@@ -354,41 +416,44 @@ fn create_versioned_wasm_runtime(
max_instances: usize,
cache_path: Option<&Path>,
) -> Result<VersionedRuntime, WasmError> {
#[cfg(not(target_os = "unknown"))]
let time = std::time::Instant::now();
// The incoming code may be actually compressed. We decompress it here and then work with
// the uncompressed code from now on.
let blob = sc_executor_common::runtime_blob::RuntimeBlob::uncompress_if_needed(&code)?;
// Use the runtime blob to scan if there is any metadata embedded into the wasm binary pertaining
// to runtime version. We do it before consuming the runtime blob for creating the runtime.
let mut version: Option<_> = read_embedded_version(&blob)?;
let runtime = create_wasm_runtime_with_code(
wasm_method,
heap_pages,
&code,
blob,
host_functions,
allow_missing_func_imports,
cache_path,
)?;
// Call to determine runtime version.
let version_result = {
// `ext` is already implicitly handled as unwind safe, as we store it in a global variable.
let mut ext = AssertUnwindSafe(ext);
// If the runtime blob doesn't embed the runtime version then use the legacy version query
// mechanism: call the runtime.
if version.is_none() {
// Call to determine runtime version.
let version_result = {
// `ext` is already implicitly handled as unwind safe, as we store it in a global variable.
let mut ext = AssertUnwindSafe(ext);
// The following unwind safety assertion is OK because if the method call panics, the
// runtime will be dropped.
let runtime = AssertUnwindSafe(runtime.as_ref());
crate::native_executor::with_externalities_safe(
&mut **ext,
move || runtime.new_instance()?.call("Core_version".into(), &[])
).map_err(|_| WasmError::Instantiation("panic in call to get runtime version".into()))?
};
let version = match version_result {
Ok(version) => Some(decode_version(&version)?),
Err(_) => None,
};
#[cfg(not(target_os = "unknown"))]
log::debug!(
target: "wasm-runtime",
"Prepared new runtime version {:?} in {} ms.",
version,
time.elapsed().as_millis(),
);
// The following unwind safety assertion is OK because if the method call panics, the
// runtime will be dropped.
let runtime = AssertUnwindSafe(runtime.as_ref());
crate::native_executor::with_externalities_safe(
&mut **ext,
move || runtime.new_instance()?.call("Core_version".into(), &[])
).map_err(|_| WasmError::Instantiation("panic in call to get runtime version".into()))?
};
if let Ok(version_buf) = version_result {
version = Some(decode_version(&version_buf)?)
}
}
let mut instances = Vec::with_capacity(max_instances);
instances.resize_with(max_instances, || Mutex::new(None));