diff --git a/Cargo.lock b/Cargo.lock
index 9bc253ea5fc..fbcc3c93ae3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1824,6 +1824,16 @@ dependencies = [
"parity-scale-codec",
]
+[[package]]
+name = "demo-async-critical"
+version = "0.1.0"
+dependencies = [
+ "futures",
+ "gear-wasm-builder",
+ "gstd",
+ "parity-scale-codec",
+]
+
[[package]]
name = "demo-async-custom-entry"
version = "0.1.0"
@@ -7471,6 +7481,7 @@ version = "1.0.5"
dependencies = [
"blake2-rfc",
"demo-async",
+ "demo-async-critical",
"demo-async-custom-entry",
"demo-async-init",
"demo-async-recursion",
diff --git a/Cargo.toml b/Cargo.toml
index bb888dda646..762d1f62513 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ members = [
"core-processor",
"core-errors",
"examples/async",
+ "examples/async-critical",
"examples/async-custom-entry",
"examples/async-init",
"examples/async-signal-entry",
@@ -367,6 +368,7 @@ try-runtime-cli = { version = "0.10.0-dev", git = "https://github.com/gear-tech/
# Examples
test-syscalls = { path = "examples/syscalls", default-features = false }
demo-async = { path = "examples/async" }
+demo-async-critical = { path = "examples/async-critical" }
demo-async-custom-entry = { path = "examples/async-custom-entry" }
demo-async-init = { path = "examples/async-init" }
demo-async-recursion = { path = "examples/async-recursion" }
diff --git a/examples/async-critical/Cargo.toml b/examples/async-critical/Cargo.toml
new file mode 100644
index 00000000000..83b971f56aa
--- /dev/null
+++ b/examples/async-critical/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "demo-async-critical"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+repository.workspace = true
+
+[dependencies]
+gstd.workspace = true
+parity-scale-codec.workspace = true
+futures.workspace = true
+
+[build-dependencies]
+gear-wasm-builder.workspace = true
+
+[features]
+debug = ["gstd/debug"]
+default = ["std"]
+std = []
diff --git a/examples/async-critical/build.rs b/examples/async-critical/build.rs
new file mode 100644
index 00000000000..4c502a3ddee
--- /dev/null
+++ b/examples/async-critical/build.rs
@@ -0,0 +1,21 @@
+// This file is part of Gear.
+
+// Copyright (C) 2021-2023 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+fn main() {
+ gear_wasm_builder::build();
+}
diff --git a/examples/async-critical/src/lib.rs b/examples/async-critical/src/lib.rs
new file mode 100644
index 00000000000..7ae8e99ce47
--- /dev/null
+++ b/examples/async-critical/src/lib.rs
@@ -0,0 +1,40 @@
+// This file is part of Gear.
+
+// Copyright (C) 2021-2023 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#![no_std]
+
+#[cfg(feature = "std")]
+mod code {
+ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
+}
+
+#[cfg(feature = "std")]
+pub use code::WASM_BINARY_OPT as WASM_BINARY;
+
+use gstd::{Decode, Encode};
+
+#[derive(Debug, Encode, Decode)]
+pub enum HandleAction {
+ Simple,
+ Panic,
+ InHandleReply,
+ InHandleSignal,
+}
+
+#[cfg(target_arch = "wasm32")]
+mod wasm;
diff --git a/examples/async-critical/src/wasm.rs b/examples/async-critical/src/wasm.rs
new file mode 100644
index 00000000000..8304255138a
--- /dev/null
+++ b/examples/async-critical/src/wasm.rs
@@ -0,0 +1,111 @@
+// This file is part of Gear.
+
+// Copyright (C) 2023 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+use crate::HandleAction;
+use gstd::{critical, exec, msg, prelude::*, ActorId};
+
+static mut REPLY_SET_HOOK: bool = false;
+static mut SIGNAL_SET_HOOK: bool = false;
+static mut INITIATOR: ActorId = ActorId::zero();
+
+#[gstd::async_main(handle_reply = my_handle_reply, handle_signal = my_handle_signal)]
+async fn main() {
+ unsafe { INITIATOR = msg::source() };
+
+ let action: HandleAction = msg::load().expect("Failed to read handle action");
+
+ match action {
+ HandleAction::Simple => {
+ // call `gr_source` outside because it is forbidden in `handle_signal`
+ let source = msg::source();
+
+ // should not send anything because execution will be completed
+ critical::set_hook(move || {
+ msg::send_bytes(source, b"critical", 0).unwrap();
+ });
+
+ // wait occurs inside so hook is saved
+ gstd::msg::send_bytes_for_reply(source, b"for_reply", 0, 0)
+ .expect("Failed to send message")
+ .await
+ .expect("Received error reply");
+ }
+ HandleAction::Panic => {
+ // call `gr_source` outside because it is forbidden in `handle_signal`
+ let source = msg::source();
+
+ // should send message because panic occurs below
+ critical::set_hook(move || {
+ msg::send_bytes(source, b"critical", 0).unwrap();
+ });
+
+ // wait occurs inside so hook is saved
+ gstd::msg::send_bytes_for_reply(msg::source(), b"for_reply", 0, 0)
+ .expect("Failed to send message")
+ .await
+ .expect("Received error reply");
+
+ // panic occurs so `handle_signal` will execute hook
+ panic!();
+ }
+ HandleAction::InHandleReply => {
+ unsafe {
+ REPLY_SET_HOOK = true;
+ }
+
+ gstd::msg::send_bytes_for_reply(msg::source(), b"for_reply", 0, 0)
+ .expect("Failed to send message")
+ .await
+ .expect("Received error reply");
+ }
+ HandleAction::InHandleSignal => {
+ unsafe {
+ SIGNAL_SET_HOOK = true;
+ }
+
+ gstd::msg::send_bytes_for_reply(msg::source(), b"for_reply", 0, 0)
+ .expect("Failed to send message")
+ .await
+ .expect("Received error reply");
+
+ panic!()
+ }
+ }
+}
+
+fn my_handle_reply() {
+ unsafe {
+ if REPLY_SET_HOOK {
+ // should panic in this entrypoint
+ critical::set_hook(move || {
+ msg::send_bytes(INITIATOR, b"from_handle_reply", 0).unwrap();
+ });
+ }
+ }
+}
+
+fn my_handle_signal() {
+ unsafe {
+ if SIGNAL_SET_HOOK {
+ // should panic in this entrypoint
+ critical::set_hook(move || {
+ msg::send_bytes(INITIATOR, b"from_handle_signal", 0).unwrap();
+ });
+ }
+ }
+}
diff --git a/gstd/src/async_runtime/futures.rs b/gstd/src/async_runtime/futures.rs
index bcf5f01e441..287cb182968 100644
--- a/gstd/src/async_runtime/futures.rs
+++ b/gstd/src/async_runtime/futures.rs
@@ -18,7 +18,7 @@
//! Module for future-management.
-use crate::{prelude::Box, MessageId};
+use crate::{critical, prelude::Box, MessageId};
use core::{
future::Future,
pin::Pin,
@@ -87,6 +87,7 @@ where
if Pin::new(&mut task.future).poll(&mut cx).is_ready() {
super::futures().remove(&msg_id);
super::locks().remove_message_entry(msg_id);
+ let _ = critical::take_hook();
} else {
super::locks().wait(msg_id);
}
diff --git a/gstd/src/async_runtime/mod.rs b/gstd/src/async_runtime/mod.rs
index a5f8d10a6ea..3b2697e223e 100644
--- a/gstd/src/async_runtime/mod.rs
+++ b/gstd/src/async_runtime/mod.rs
@@ -22,12 +22,13 @@ mod signals;
mod waker;
pub use self::futures::message_loop;
+pub(crate) use locks::Lock;
+pub(crate) use signals::ReplyPoll;
use self::futures::FuturesMap;
+use crate::critical;
use hashbrown::HashMap;
-pub(crate) use locks::Lock;
use locks::LocksMap;
-pub(crate) use signals::ReplyPoll;
use signals::WakeSignals;
static mut FUTURES: Option = None;
@@ -58,6 +59,9 @@ pub fn handle_signal() {
let msg_id = crate::msg::signal_from().expect(
"`gstd::async_runtime::handle_signal()` must be called only in `handle_signal` entrypoint",
);
+
+ critical::take_and_execute();
+
futures().remove(&msg_id);
locks().remove_message_entry(msg_id);
}
diff --git a/gstd/src/critical.rs b/gstd/src/critical.rs
new file mode 100644
index 00000000000..134ff7f2cfb
--- /dev/null
+++ b/gstd/src/critical.rs
@@ -0,0 +1,133 @@
+// This file is part of Gear.
+
+// Copyright (C) 2023 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+// TODO: describe denied sys-calls in entrypoint (#3580)
+//! Critical hook that guarantees code section execution.
+//!
+//! __Hook is set on per-message basis.__
+//!
+//! Code is executed in `handle_signal` entry point in case of failure
+//! only across [`exec::wait()`] calls because hook has to be saved.
+//!
+//! ```rust,no_run
+//! use gstd::{critical, msg};
+//!
+//! # async fn _dummy() {
+//! // get source outside of critical hook
+//! // because `gr_source` sys-call is forbidden inside `handle_signal` entry point
+//! let source = msg::source();
+//!
+//! critical::set_hook(move || {
+//! msg::send(source, "sends failed", 0).expect("Failed to send emergency message");
+//! });
+//!
+//! let msg = msg::send_for_reply(source, "send_for_reply", 0, 0)
+//! .expect("Failed to send message")
+//! // await on `MessageFuture` which calls `exec::wait()` inside
+//! // so program state will be saved and thus hook will too
+//! .await
+//! .expect("Received error reply");
+//!
+//! // if some code fails (panic, out of gas, etc) after `exec::wait()` and friends
+//! // then saved hook will be executed in `handle_signal`
+//!
+//! // your code
+//! // ...
+//!
+//! # }
+//! ```
+//!
+//! [`exec::wait()`]: crate::exec::wait
+
+use crate::{msg, MessageId};
+use alloc::boxed::Box;
+use hashbrown::HashMap;
+
+type HooksMap = HashMap>;
+
+static mut HOOKS: Option = None;
+
+fn hooks() -> &'static mut HooksMap {
+ unsafe { HOOKS.get_or_insert_with(HashMap::new) }
+}
+
+/// Sets critical hook.
+pub fn set_hook(f: F) {
+ if msg::reply_code().is_ok() {
+ panic!("`gstd::critical::set_hook()` must not be called in `handle_reply` entrypoint")
+ }
+
+ if msg::signal_code().is_ok() {
+ panic!("`gstd::critical::set_hook()` must not be called in `handle_signal` entrypoint")
+ }
+
+ hooks().insert(msg::id(), Box::new(f));
+}
+
+/// Removes current hook and returns it.
+///
+/// __Don't use it at all if you use
+/// [`#[gstd::async_init]`](crate::async_init) or
+/// [`#[gstd::async_main]`](crate::async_main).__
+///
+/// Must be called at the end of `init` or `handle`
+/// to not blow up map because hook is set on per-message basis:
+///
+/// ```rust,no_run
+/// use gstd::critical;
+///
+/// #[no_mangle]
+/// extern "C" fn handle() {
+/// critical::set_hook(|| {
+/// // some code...
+/// });
+///
+/// // handle code...
+///
+/// let _ = critical::take_hook();
+/// }
+/// ```
+pub fn take_hook() -> Option> {
+ hooks().remove(&msg::id())
+}
+
+/// Removes current hook and executes it.
+///
+/// __Don't use it at all if you use
+/// [`#[gstd::async_init]`](crate::async_init) or
+/// [`#[gstd::async_main]`](crate::async_main).__
+///
+/// Must be called inside `handle_signal`:
+///
+/// ```rust,no_run
+/// use gstd::critical;
+///
+/// #[no_mangle]
+/// extern "C" fn handle_signal() {
+/// critical::take_and_execute();
+/// }
+/// ```
+pub fn take_and_execute() {
+ let msg_id = msg::signal_from().expect(
+ "`gstd::critical::execute_hook_once()` must be called only in `handle_signal` entrypoint",
+ );
+
+ if let Some(mut f) = hooks().remove(&msg_id) {
+ f();
+ }
+}
diff --git a/gstd/src/lib.rs b/gstd/src/lib.rs
index 7a60e321812..379fc4e0a2a 100644
--- a/gstd/src/lib.rs
+++ b/gstd/src/lib.rs
@@ -149,6 +149,7 @@ extern crate galloc;
mod async_runtime;
mod common;
mod config;
+pub mod critical;
pub mod exec;
mod macros;
pub mod msg;
diff --git a/pallets/gear/Cargo.toml b/pallets/gear/Cargo.toml
index f449814e4a4..46dd5d103bb 100644
--- a/pallets/gear/Cargo.toml
+++ b/pallets/gear/Cargo.toml
@@ -115,6 +115,7 @@ demo-ping.workspace = true
demo-sync-duplicate.workspace = true
demo-custom.workspace = true
demo-delayed-reservation-sender = { workspace = true, features = ["debug"] }
+demo-async-critical = { workspace = true, features = ["debug"] }
test-syscalls = { workspace = true, features = ["debug"] }
page_size.workspace = true
frame-support-test = { workspace = true, features = ["std"] }
diff --git a/pallets/gear/src/tests.rs b/pallets/gear/src/tests.rs
index f48abb4cb90..10145f39e27 100644
--- a/pallets/gear/src/tests.rs
+++ b/pallets/gear/src/tests.rs
@@ -14135,6 +14135,234 @@ fn calculate_gas_wait() {
});
}
+#[test]
+fn critical_hook_works() {
+ use demo_async_critical::{HandleAction, WASM_BINARY};
+
+ init_logger();
+ new_test_ext().execute_with(|| {
+ assert_ok!(Gear::upload_program(
+ RuntimeOrigin::signed(USER_1),
+ WASM_BINARY.to_vec(),
+ DEFAULT_SALT.to_vec(),
+ vec![],
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+ let pid = get_last_program_id();
+
+ run_to_block(2, None);
+
+ assert!(Gear::is_initialized(pid));
+ assert!(Gear::is_active(pid));
+
+ assert_ok!(Gear::send_message(
+ RuntimeOrigin::signed(USER_1),
+ pid,
+ HandleAction::Simple.encode(),
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+
+ let mid = get_last_message_id();
+
+ run_to_block(3, None);
+
+ let (waited, _) = get_last_message_waited();
+ assert_eq!(mid, waited);
+ assert_eq!(dispatch_status(mid), None);
+
+ let msg = get_last_mail(USER_1);
+ assert_eq!(msg.payload_bytes(), b"for_reply");
+
+ assert_ok!(Gear::send_reply(
+ RuntimeOrigin::signed(USER_1),
+ msg.id(),
+ EMPTY_PAYLOAD.to_vec(),
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+
+ run_to_block(4, None);
+
+ assert_succeed(mid);
+ assert_eq!(MailboxOf::::iter_key(USER_1).count(), 0);
+ });
+}
+
+#[test]
+fn critical_hook_with_panic() {
+ use demo_async_critical::{HandleAction, WASM_BINARY};
+
+ init_logger();
+ new_test_ext().execute_with(|| {
+ assert_ok!(Gear::upload_program(
+ RuntimeOrigin::signed(USER_1),
+ WASM_BINARY.to_vec(),
+ DEFAULT_SALT.to_vec(),
+ vec![],
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+ let pid = get_last_program_id();
+
+ run_to_block(2, None);
+
+ assert!(Gear::is_initialized(pid));
+ assert!(Gear::is_active(pid));
+
+ assert_ok!(Gear::send_message(
+ RuntimeOrigin::signed(USER_1),
+ pid,
+ HandleAction::Panic.encode(),
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+
+ let mid = get_last_message_id();
+
+ run_to_block(3, None);
+
+ let msg = get_last_mail(USER_1);
+ assert_eq!(msg.payload_bytes(), b"for_reply");
+
+ assert_ok!(Gear::send_reply(
+ RuntimeOrigin::signed(USER_1),
+ msg.id(),
+ EMPTY_PAYLOAD.to_vec(),
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+
+ run_to_block(4, None);
+
+ assert_failed(
+ mid,
+ ErrorReplyReason::Execution(SimpleExecutionError::UserspacePanic),
+ );
+
+ let msg = get_last_mail(USER_1);
+ assert_eq!(msg.payload_bytes(), b"critical");
+ });
+}
+
+#[test]
+fn critical_hook_in_handle_reply() {
+ use demo_async_critical::{HandleAction, WASM_BINARY};
+
+ init_logger();
+ new_test_ext().execute_with(|| {
+ assert_ok!(Gear::upload_program(
+ RuntimeOrigin::signed(USER_1),
+ WASM_BINARY.to_vec(),
+ DEFAULT_SALT.to_vec(),
+ vec![],
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+ let pid = get_last_program_id();
+
+ run_to_block(2, None);
+
+ assert!(Gear::is_initialized(pid));
+ assert!(Gear::is_active(pid));
+
+ assert_ok!(Gear::send_message(
+ RuntimeOrigin::signed(USER_1),
+ pid,
+ HandleAction::InHandleReply.encode(),
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+
+ run_to_block(3, None);
+
+ let msg = get_last_mail(USER_1);
+ assert_eq!(msg.payload_bytes(), b"for_reply");
+
+ assert_ok!(Gear::send_reply(
+ RuntimeOrigin::signed(USER_1),
+ msg.id(),
+ EMPTY_PAYLOAD.to_vec(),
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+
+ let mid = get_last_message_id();
+
+ run_to_block(4, None);
+
+ assert_eq!(MailboxOf::::iter_key(USER_1).last(), None);
+ let status = dispatch_status(mid);
+ assert_eq!(status, Some(DispatchStatus::Failed));
+ });
+}
+
+#[test]
+fn critical_hook_in_handle_signal() {
+ use demo_async_critical::{HandleAction, WASM_BINARY};
+
+ init_logger();
+ new_test_ext().execute_with(|| {
+ assert_ok!(Gear::upload_program(
+ RuntimeOrigin::signed(USER_1),
+ WASM_BINARY.to_vec(),
+ DEFAULT_SALT.to_vec(),
+ vec![],
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+ let pid = get_last_program_id();
+
+ run_to_block(2, None);
+
+ assert!(Gear::is_initialized(pid));
+ assert!(Gear::is_active(pid));
+
+ assert_ok!(Gear::send_message(
+ RuntimeOrigin::signed(USER_1),
+ pid,
+ HandleAction::InHandleSignal.encode(),
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+
+ let mid = get_last_message_id();
+
+ run_to_block(3, None);
+
+ let msg = get_last_mail(USER_1);
+ assert_eq!(msg.payload_bytes(), b"for_reply");
+
+ assert_ok!(Gear::send_reply(
+ RuntimeOrigin::signed(USER_1),
+ msg.id(),
+ EMPTY_PAYLOAD.to_vec(),
+ 10_000_000_000,
+ 0,
+ false,
+ ));
+
+ run_to_block(4, None);
+
+ assert_eq!(MailboxOf::::iter_key(USER_1).last(), None);
+ let signal_msg_id = MessageId::generate_signal(mid);
+ let status = dispatch_status(signal_msg_id);
+ assert_eq!(status, Some(DispatchStatus::Failed));
+ });
+}
+
mod utils {
#![allow(unused)]
@@ -14498,6 +14726,7 @@ mod utils {
})
}
+ #[track_caller]
pub(super) fn dispatch_status(message_id: MessageId) -> Option {
let mut found_status: Option = None;
System::events().iter().for_each(|e| {
@@ -14522,6 +14751,7 @@ mod utils {
assert_eq!(status, DispatchStatus::Success)
}
+ #[track_caller]
fn get_last_event_error_and_reply_code(message_id: MessageId) -> (String, ReplyCode) {
let mut actual_error = None;