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;