Skip to content

Commit 0692d6b

Browse files
committed
feat(stack-guest-abi): a step loop, so a guest operation pauses on its HTTP requests instead of blocking
A guest export that reaches ZeroKMS blocked on the host's synchronous transport_send import. A host whose HTTP client cannot block — fetch in a JavaScript edge runtime — cannot satisfy that import, and in Go the client held its lock for the whole round trip. The new `step` module runs an operation's work as a future polled once. A request the work makes is recorded and the future stays pending; the operation is kept in a table under a handle and its requests are handed to the host. The host performs them and resumes the operation with the responses, in order, until it is done; or it aborts it, which drops the future and wipes everything it held. Each request stores the waker it was polled with and resuming wakes it first, so combinators that poll an inner future only when woken (buffered, as the ZeroKMS client's 500-key chunking uses) do not stall. Handles are issued in increasing order and never reused, at most 64 operations may be paused, an unknown handle is STATUS_STATE, and work that pauses with nothing to wait on is STATUS_INTERNAL. STATUS_NEEDS_IO (29) is how an export says it paused. It is not a failure: call::run records no error for it. The module holds no encoding; each guest decides how a step's requests and responses cross. Refs #1064.
1 parent 2aaab14 commit 0692d6b

7 files changed

Lines changed: 689 additions & 3 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/stack-guest-abi/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@ vitaminc-protected = { workspace = true }
3636
# host holds, run natively and under Miri. `std` only: the `fork` and
3737
# `timeout` defaults spawn processes and threads Miri cannot interpret.
3838
proptest = { version = "1.7", default-features = false, features = ["std"] }
39+
# `step`'s tests drive the loop through the combinators the ZeroKMS client
40+
# runs its requests with (`buffered`, `join`).
41+
futures = { version = "0.3", default-features = false, features = ["std", "async-await"] }

‎packages/stack-guest-abi/src/buffers.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ pub fn wipe_all() {
135135
// The recorded last error is one of these buffers; its slot must not
136136
// outlive the wipe and name freed memory.
137137
crate::last_error::forget();
138+
crate::step::forget_pending();
138139
let live = BUFFERS.with(|b| core::mem::take(&mut *b.borrow_mut()));
139140
for (ptr, len) in live {
140141
// SAFETY: every entry was registered by `register`, which leaked a

‎packages/stack-guest-abi/src/call.rs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use std::panic::{catch_unwind, AssertUnwindSafe};
1616

1717
use crate::buffers;
1818
use crate::last_error;
19+
use crate::status::STATUS_NEEDS_IO;
1920

2021
/// Run an export's body: clear the last error, run `f` (a panic is an
2122
/// internal failure), and then clear the last error again if `f` succeeded,
@@ -34,6 +35,11 @@ pub fn run<T>(f: impl FnOnce() -> Result<T, u32>) -> Result<T, u32> {
3435
last_error::clear();
3536
Ok(out)
3637
}
38+
// A paused operation has not failed: nothing is recorded.
39+
Err(STATUS_NEEDS_IO) => {
40+
last_error::clear();
41+
Err(STATUS_NEEDS_IO)
42+
}
3743
Err(status) => {
3844
last_error::ensure(status);
3945
Err(status)
@@ -93,6 +99,18 @@ pub fn take_last_error() -> Option<Vec<u8>> {
9399
mod tests {
94100
use super::*;
95101
use crate::status::{STATUS_ENCODING, STATUS_INTERNAL};
102+
103+
/// A paused operation is not a failure: it records no error, and clears
104+
/// one recorded on the way to the pause.
105+
#[test]
106+
fn a_pause_records_no_error() {
107+
let paused = run::<()>(|| {
108+
let _ = last_error::malformed("on the way");
109+
Err(STATUS_NEEDS_IO)
110+
});
111+
assert_eq!(paused, Err(STATUS_NEEDS_IO));
112+
assert!(!last_error::is_set());
113+
}
96114
use crate::value::HostValue;
97115

98116
/// A packed result, read the way the Go host reads it.

‎packages/stack-guest-abi/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ pub mod call;
104104
pub mod headers;
105105
pub mod last_error;
106106
pub mod status;
107+
pub mod step;
107108
pub mod value;
108109

109110
#[cfg(target_arch = "wasm32")]

‎packages/stack-guest-abi/src/status.rs‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
//! run this call against. Codes 5–10 are the outcomes of a request to
1616
//! ZeroKMS, so a host can distinguish a refused credential from a tampered
1717
//! ciphertext without parsing strings; 11 and 12 were appended by the
18-
//! crypto guest, 13–27 by the credential guest, and 28 by the crypto guest
19-
//! again.
18+
//! crypto guest, 13–27 by the credential guest, and 28 and 29 by the
19+
//! crypto guest again. 29 is not a failure: see [`STATUS_NEEDS_IO`].
2020
//!
2121
//! Each code is documented here as the *verdict* it carries to a host — the
2222
//! thing the host can act on. Which of a library's errors reach which code,
@@ -122,10 +122,16 @@ pub const STATUS_AUTH_REFRESH_REQUIRED: u32 = 27;
122122
/// and only that — never a verdict on the record's integrity.
123123
pub const STATUS_CONTEXT_MISMATCH: u32 = 28;
124124

125+
/// Not a failure: the operation is paused, waiting for the host to perform
126+
/// HTTP requests on its behalf (the step loop, [`crate::step`]). The host
127+
/// takes the requests and the operation's handle from the guest, performs
128+
/// them, and resumes it with the responses. No error is recorded.
129+
pub const STATUS_NEEDS_IO: u32 = 29;
130+
125131
/// The last code in the table. A guest appending a code of its own starts
126132
/// at `LAST_STATUS + 1` and moves this constant with it, so two guests can
127133
/// never claim one number.
128-
pub const LAST_STATUS: u32 = STATUS_CONTEXT_MISMATCH;
134+
pub const LAST_STATUS: u32 = STATUS_NEEDS_IO;
129135

130136
#[cfg(test)]
131137
mod tests {
@@ -164,6 +170,7 @@ mod tests {
164170
STATUS_AUTH_OTHER,
165171
STATUS_AUTH_REFRESH_REQUIRED,
166172
STATUS_CONTEXT_MISMATCH,
173+
STATUS_NEEDS_IO,
167174
];
168175
for (i, code) in codes.iter().enumerate() {
169176
assert_eq!(*code, i as u32 + 1, "code {i} is out of sequence");

0 commit comments

Comments
 (0)