nordisk/heimdall
crates/heimdall-wasm/src/host.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! The engine, the signature gate before load, and one bounded run.
use std::fmt;
use std::io::Read;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::JoinHandle;
use guest_host::verify::{ModuleTrust, TrustError};
use heimdall_core::{GuestRef, Params};
use wasmtime::component::{
Component, ComponentType, InstancePre, Lift, Linker, Lower, WasmList, WasmStr,
};
use wasmtime::{Config, Engine, ResourceLimiter, Store, StoreContextMut, Trap};
use crate::codes;
use crate::limits::{HostConfig, RunLimits};
use crate::names::check_file_name;
use crate::outcome::{EmittedFile, GuestOutcome, Verdict};
/// The host could not be built.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HostError {
/// No trust roots: Heimdall has no unsigned mode.
NoTrustRoots,
/// wasmtime refused the engine configuration.
Engine(String),
}
impl fmt::Display for HostError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HostError::NoTrustRoots => write!(
f,
"no trust roots configured: every guest must be signed, there is no unsigned mode"
),
HostError::Engine(e) => write!(f, "the wasm engine could not be configured: {e}"),
}
}
}
impl std::error::Error for HostError {}
/// Which part of the signature check failed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrustFailure {
/// No `.wasm.p7s` beside the module.
Unsigned,
/// The signature is not over these bytes, or chains to no trusted root.
SignatureInvalid,
/// A trust root could not be read or parsed.
RootsUnreadable,
/// This build has no verifier (never the case for heimdall-wasm).
VerifierUnavailable,
}
/// Why a guest was not loaded. Nothing of the guest ran.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoadRefusal {
Unreadable {
path: PathBuf,
detail: String,
},
TooLarge {
bytes: u64,
max: u64,
},
DigestMissing {
sidecar: PathBuf,
},
DigestMismatch {
pin: &'static str,
expected: String,
actual: String,
},
Untrusted {
failure: TrustFailure,
detail: String,
},
NotAComponent,
Invalid {
detail: String,
},
ImportRefused {
imports: Vec<String>,
},
MissingExport {
name: String,
},
Link {
detail: String,
},
}
impl fmt::Display for LoadRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "refused: guest not loaded (")?;
match self {
LoadRefusal::Unreadable { path, detail } => {
write!(f, "{} could not be read: {detail}", path.display())
}
LoadRefusal::TooLarge { bytes, max } => {
write!(f, "module is {bytes} bytes, more than {max}")
}
LoadRefusal::DigestMissing { sidecar } => {
write!(f, "no blake3 pin at {}", sidecar.display())
}
LoadRefusal::DigestMismatch {
pin,
expected,
actual,
} => {
write!(f, "blake3 {actual} does not match the {pin} {expected}")
}
LoadRefusal::Untrusted { failure, detail } => {
write!(f, "signature check failed ({failure:?}): {detail}")
}
LoadRefusal::NotAComponent => write!(
f,
"a core module, not a component implementing heimdall:guest/job"
),
LoadRefusal::Invalid { detail } => write!(f, "not a valid component: {detail}"),
LoadRefusal::ImportRefused { imports } => {
write!(
f,
"imports {} not provided; the host links only emit-file (no WASI)",
imports.join(", ")
)
}
LoadRefusal::MissingExport { name } => write!(f, "no export {name:?}"),
LoadRefusal::Link { detail } => write!(f, "link failed: {detail}"),
}?;
write!(f, ") [{}]", self.code())
}
}
impl std::error::Error for LoadRefusal {}
impl LoadRefusal {
/// The stable `heimdall-wasm-<n>` code of this refusal.
pub fn code(&self) -> &'static str {
match self {
LoadRefusal::Unreadable { .. } => codes::LOAD_UNREADABLE,
LoadRefusal::TooLarge { .. } => codes::LOAD_TOO_LARGE,
LoadRefusal::DigestMissing { .. } => codes::LOAD_DIGEST_MISSING,
LoadRefusal::DigestMismatch { .. } => codes::LOAD_DIGEST_MISMATCH,
LoadRefusal::Untrusted { .. } => codes::LOAD_UNTRUSTED,
LoadRefusal::NotAComponent => codes::LOAD_NOT_A_COMPONENT,
LoadRefusal::Invalid { .. } => codes::LOAD_INVALID,
LoadRefusal::ImportRefused { .. } => codes::LOAD_IMPORT_REFUSED,
LoadRefusal::MissingExport { .. } => codes::LOAD_MISSING_EXPORT,
LoadRefusal::Link { .. } => codes::LOAD_LINK,
}
}
}
/// The wasm host: one engine, one linker that offers only `emit-file`.
pub struct Host {
cfg: HostConfig,
engine: Engine,
linker: Arc<Linker<State>>,
trust: ModuleTrust,
ticker: Arc<Ticker>,
}
impl Host {
/// Build the host. Refused without trust roots: there is no unsigned mode.
pub fn new(cfg: HostConfig) -> Result<Host, HostError> {
if cfg.trust_roots.is_empty() {
return Err(HostError::NoTrustRoots);
}
let mut c = Config::new();
c.wasm_component_model(true)
.consume_fuel(true)
.epoch_interruption(true)
.max_wasm_stack(cfg.stack_bytes)
// Determinism: the same guest and input give the same bytes out.
.cranelift_nan_canonicalization(true);
let engine = Engine::new(&c).map_err(|e| HostError::Engine(format!("{e:#}")))?;
let mut linker: Linker<State> = Linker::new(&engine);
linker
.root()
.func_wrap("emit-file", emit_file)
.map_err(|e| HostError::Engine(format!("{e:#}")))?;
let ticker = Arc::new(Ticker::start(engine.clone(), cfg.epoch_tick));
let trust = ModuleTrust::required(cfg.trust_roots.clone());
Ok(Host {
cfg,
engine,
linker: Arc::new(linker),
trust,
ticker,
})
}
/// The host's configuration.
pub fn config(&self) -> &HostConfig {
&self.cfg
}
/// Read, verify (blake3 pin + signature), compile and link a guest.
///
/// The module file is read ONCE; the bytes that are verified are the bytes
/// that are compiled. Nothing of the guest runs here.
pub fn load(&self, module: &Path) -> Result<Guest, LoadRefusal> {
self.load_inner(module, None)
}
/// [`Host::load`] that also demands the module's blake3 equals `blake3_hex`.
pub fn load_pinned(&self, module: &Path, blake3_hex: &str) -> Result<Guest, LoadRefusal> {
self.load_inner(module, Some(blake3_hex))
}
/// [`Host::load_pinned`] for a heimdall-core [`GuestRef`] (its `blake3` is the caller pin).
pub fn load_ref(&self, guest: &GuestRef) -> Result<Guest, LoadRefusal> {
self.load_pinned(&guest.path, &guest.blake3)
}
fn load_inner(&self, module: &Path, caller_pin: Option<&str>) -> Result<Guest, LoadRefusal> {
let bytes = read_capped(module, self.cfg.max_module_bytes)?;
let digest = blake3::hash(&bytes).to_hex().to_string();
// 1. The `.wasm.blake3` pin beside the module, then the caller's pin.
let sidecar = sidecar_path(module, "blake3");
let pinned = read_pin(&sidecar)?;
check_pin("sidecar .wasm.blake3", &pinned, &digest)?;
if let Some(pin) = caller_pin {
check_pin("caller pin", pin, &digest)?;
}
// 2. The signature (`.wasm.p7s`, CMS over the blake3), before compiling.
let signer = match self.trust.verify(module, &bytes) {
Ok(Some(cn)) => cn,
Ok(None) => {
return Err(LoadRefusal::Untrusted {
failure: TrustFailure::VerifierUnavailable,
detail: "no trust policy in force".into(),
})
}
Err(e) => return Err(untrusted(e)),
};
// 3. A component, compiled; only then the import/export check.
if bytes.len() >= 8 && bytes[..4] == *b"\0asm" && bytes[4..8] == [1, 0, 0, 0] {
return Err(LoadRefusal::NotAComponent);
}
let component = Component::new(&self.engine, &bytes).map_err(|e| LoadRefusal::Invalid {
detail: one_line(&format!("{e:#}")),
})?;
let ty = component.component_type();
let foreign: Vec<String> = ty
.imports(&self.engine)
.map(|(name, _)| name.to_string())
.filter(|name| name != "emit-file")
.collect();
if !foreign.is_empty() {
return Err(LoadRefusal::ImportRefused { imports: foreign });
}
if !ty.exports(&self.engine).any(|(name, _)| name == "run") {
return Err(LoadRefusal::MissingExport { name: "run".into() });
}
let pre = self
.linker
.instantiate_pre(&component)
.map_err(|e| LoadRefusal::Link {
detail: one_line(&format!("{e:#}")),
})?;
Ok(Guest {
pre,
digest_hex: digest,
signer,
stack_bytes: self.cfg.stack_bytes,
epoch_tick: self.cfg.epoch_tick,
_ticker: self.ticker.clone(),
})
}
}
/// A verified, compiled, linked guest. Each run gets a fresh store.
pub struct Guest {
pre: InstancePre<State>,
digest_hex: String,
signer: String,
stack_bytes: usize,
epoch_tick: std::time::Duration,
_ticker: Arc<Ticker>,
}
impl Guest {
/// Lowercase hex BLAKE3-256 of the module bytes that were verified.
pub fn digest_hex(&self) -> &str {
&self.digest_hex
}
/// The common name of the certificate that signed the module.
pub fn signer(&self) -> &str {
&self.signer
}
/// Run the guest once on `input` with `params`, under `limits`.
///
/// Always returns an outcome: a trap, a limit or even a panic inside the
/// host's own glue becomes a refusal, never an unwind into the caller.
pub fn run(&self, input: &[u8], params: &Params, limits: &RunLimits) -> GuestOutcome {
match catch_unwind(AssertUnwindSafe(|| self.run_inner(input, params, limits))) {
Ok(o) => o,
Err(p) => {
let what = p
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| p.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "panic".into());
GuestOutcome::refused(codes::HOST_FAULT, format!("host fault ({what})"), 0)
}
}
}
fn run_inner(&self, input: &[u8], params: &Params, limits: &RunLimits) -> GuestOutcome {
let mut store = Store::new(self.pre.engine(), State::new(limits.clone()));
store.limiter(|s| &mut s.limiter);
if let Err(e) = store.set_fuel(limits.fuel) {
return GuestOutcome::refused(codes::HOST_FAULT, format!("host fault (fuel: {e})"), 0);
}
let tick = self.epoch_tick.as_nanos().max(1);
let ticks = limits.timeout.as_nanos().div_ceil(tick).max(1);
store.set_epoch_deadline(u64::try_from(ticks).unwrap_or(u64::MAX));
store.epoch_deadline_trap();
let used = |store: &Store<State>| limits.fuel.saturating_sub(store.get_fuel().unwrap_or(0));
let instance = match self.pre.instantiate(&mut store) {
Ok(i) => i,
Err(e) => {
let (code, why) = self.classify(&e, store.data(), limits);
let fuel = used(&store);
return GuestOutcome::trapped(code, why, fuel);
}
};
type Run<'a> = (&'a [u8], &'a [Param]);
type Ret = (Result<Output, Refusal>,);
let func = match instance.get_typed_func::<Run<'_>, Ret>(&mut store, "run") {
Ok(f) => f,
Err(e) => {
let fuel = used(&store);
return GuestOutcome::refused(
codes::INTERFACE_MISMATCH,
format!("guest interface mismatch ({})", one_line(&format!("{e:#}"))),
fuel,
);
}
};
let params: Vec<Param> = params
.iter()
.map(|(k, v)| Param {
key: k.clone(),
value: v.clone(),
})
.collect();
let result = func.call(&mut store, (input, ¶ms));
let result = match result {
Ok((r,)) => r,
Err(e) => {
let (code, why) = self.classify(&e, store.data(), limits);
let fuel = used(&store);
return GuestOutcome::trapped(code, why, fuel);
}
};
let fuel = used(&store);
let max = limits.max_text_bytes;
let files = std::mem::take(&mut store.data_mut().files);
match result {
Ok(Output { note }) if note.len() > max => GuestOutcome::trapped(
codes::OUTPUT_TOO_LARGE,
format!("output note is {} bytes, more than {max}", note.len()),
fuel,
),
Err(Refusal { reason }) if reason.len() > max => GuestOutcome::trapped(
codes::OUTPUT_TOO_LARGE,
format!("refusal reason is {} bytes, more than {max}", reason.len()),
fuel,
),
Ok(Output { note }) => GuestOutcome {
verdict: Verdict::Accepted { note },
files,
fuel_used: fuel,
},
Err(Refusal { reason }) => GuestOutcome {
verdict: Verdict::Refused {
code: codes::GUEST_REFUSED,
reason,
},
files,
fuel_used: fuel,
},
}
}
/// Name the reason a call ended in an error. A refusal the host recorded
/// itself (emit-file, a limit) wins over wasmtime's generic message.
fn classify(
&self,
e: &wasmtime::Error,
state: &State,
limits: &RunLimits,
) -> (&'static str, String) {
if let Some(r) = &state.refusal {
return (codes::EMIT_REFUSED, r.clone());
}
if let Some((code, h)) = &state.limiter.hit {
return (code, h.clone());
}
if let Some(t) = e.downcast_ref::<Trap>() {
return match t {
Trap::OutOfFuel => (
codes::OUT_OF_FUEL,
format!("out of fuel: limit {} units", limits.fuel),
),
Trap::Interrupt => (
codes::TIMEOUT,
format!("wall-clock timeout after {:?}", limits.timeout),
),
Trap::StackOverflow => (
codes::STACK_OVERFLOW,
format!("stack overflow: limit {} bytes", self.stack_bytes),
),
Trap::UnreachableCodeReached => (
codes::TRAPPED,
"unreachable instruction (the guest aborted)".into(),
),
other => (codes::TRAPPED, other.to_string()),
};
}
// Lifting/lowering errors (bounds, alignment, UTF-8, discriminants)
// and instantiation limits come as plain errors; keep the cause.
let text = one_line(&format!("{e:#}"));
if text.contains("resource limit exceeded") {
(codes::INSTANTIATION_LIMIT, text)
} else if text.contains("utf-8") {
(codes::TRAPPED, format!("not valid UTF-8: {text}"))
} else {
(codes::TRAPPED, text)
}
}
}
/// The record types of `wit/heimdall.wit`.
#[derive(ComponentType, Lower)]
#[component(record)]
struct Param {
key: String,
value: String,
}
#[derive(ComponentType, Lift)]
#[component(record)]
struct Output {
note: String,
}
#[derive(ComponentType, Lift)]
#[component(record)]
struct Refusal {
reason: String,
}
/// Per-run store data.
struct State {
limiter: Limiter,
limits: RunLimits,
files: Vec<EmittedFile>,
emitted_bytes: usize,
/// Set when the host itself refused the guest (emit-file); ends the run.
refusal: Option<String>,
}
impl State {
fn new(limits: RunLimits) -> State {
State {
limiter: Limiter::new(&limits),
limits,
files: Vec::new(),
emitted_bytes: 0,
refusal: None,
}
}
fn refuse(&mut self, why: String) -> wasmtime::Error {
let r = format!("emit-file refused: {why}");
self.refusal = Some(r.clone());
wasmtime::Error::msg(r)
}
}
/// The host's only import: hand one file out. Every check runs BEFORE the
/// bytes are copied; a refusal ends the run (and the run hands on nothing).
fn emit_file(
mut cx: StoreContextMut<'_, State>,
(name, bytes): (WasmStr, WasmList<u8>),
) -> wasmtime::Result<()> {
let limits = cx.data().limits.clone();
let count = cx.data().files.len();
if count >= limits.max_files {
return Err(cx
.data_mut()
.refuse(format!("too many files: limit {}", limits.max_files)));
}
let len = bytes.len();
if len > limits.max_file_bytes {
return Err(cx.data_mut().refuse(format!(
"file too large: {len} bytes, limit {}",
limits.max_file_bytes
)));
}
let total = cx.data().emitted_bytes.saturating_add(len);
if total > limits.max_emit_bytes {
return Err(cx.data_mut().refuse(format!(
"total output {total} bytes is over the limit {}",
limits.max_emit_bytes
)));
}
let name = match name.to_str(&cx) {
Ok(n) => n.into_owned(),
Err(_) => return Err(cx.data_mut().refuse("the name is not valid UTF-8".into())),
};
if let Err(why) = check_file_name(&name) {
let shown: String = name.chars().take(64).collect();
return Err(cx.data_mut().refuse(format!("bad name {shown:?}: {why}")));
}
if cx.data().files.iter().any(|f| f.name == name) {
return Err(cx
.data_mut()
.refuse(format!("{name:?} was already emitted")));
}
let data = bytes.as_le_slice(&cx).to_vec();
let sha256 = heimdall_core::Sha256Digest::of(&data);
let st = cx.data_mut();
st.emitted_bytes = total;
st.files.push(EmittedFile {
name,
bytes: data,
sha256,
});
Ok(())
}
/// Memory/table/instance limits that refuse BY NAME: a growth past the limit
/// is a trap that records which limit fired (never a quiet -1 the guest could
/// retry around). Memory and table sizes count over ALL of the guest's
/// memories and tables together (docs/POLICY.md), not per memory.
struct Limiter {
memory_bytes: usize,
table_elements: usize,
instances: usize,
tables: usize,
memories: usize,
memory_total: usize,
table_total: usize,
hit: Option<(&'static str, String)>,
}
impl Limiter {
fn new(l: &RunLimits) -> Limiter {
Limiter {
memory_bytes: l.memory_bytes,
table_elements: l.table_elements,
instances: l.instances,
tables: l.tables,
memories: l.memories,
memory_total: 0,
table_total: 0,
hit: None,
}
}
}
impl ResourceLimiter for Limiter {
fn memory_growing(
&mut self,
current: usize,
desired: usize,
_max: Option<usize>,
) -> wasmtime::Result<bool> {
let total = self
.memory_total
.saturating_sub(current)
.saturating_add(desired);
if total > self.memory_bytes {
let why = format!(
"memory limit: asked to grow from {current} to {desired} bytes ({total} in all), limit {}",
self.memory_bytes
);
self.hit = Some((codes::MEMORY_LIMIT, why.clone()));
return Err(wasmtime::Error::msg(why));
}
self.memory_total = total;
Ok(true)
}
fn table_growing(
&mut self,
current: usize,
desired: usize,
_max: Option<usize>,
) -> wasmtime::Result<bool> {
let total = self
.table_total
.saturating_sub(current)
.saturating_add(desired);
if total > self.table_elements {
let why = format!(
"table limit: asked to grow from {current} to {desired} elements ({total} in all), limit {}",
self.table_elements
);
self.hit = Some((codes::TABLE_LIMIT, why.clone()));
return Err(wasmtime::Error::msg(why));
}
self.table_total = total;
Ok(true)
}
fn instances(&self) -> usize {
self.instances
}
fn tables(&self) -> usize {
self.tables
}
fn memories(&self) -> usize {
self.memories
}
}
/// Bumps the engine epoch every tick; a store's deadline is counted in ticks.
/// Stops when the host and every guest from it are dropped.
struct Ticker {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
impl Ticker {
fn start(engine: Engine, tick: std::time::Duration) -> Ticker {
let stop = Arc::new(AtomicBool::new(false));
let s = stop.clone();
let handle = std::thread::Builder::new()
.name("heimdall-wasm-epoch".into())
.spawn(move || {
while !s.load(Ordering::Relaxed) {
std::thread::sleep(tick);
engine.increment_epoch();
}
})
.expect("the epoch thread could not be started");
Ticker {
stop,
handle: Some(handle),
}
}
}
impl Drop for Ticker {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
/// Whitespace collapsed and capped, so a refusal stays one readable line.
fn one_line(s: &str) -> String {
let flat = s.split_whitespace().collect::<Vec<_>>().join(" ");
if flat.len() <= 400 {
return flat;
}
let mut cut = 400;
while !flat.is_char_boundary(cut) {
cut -= 1;
}
format!("{}…", &flat[..cut])
}
fn sidecar_path(module: &Path, ext: &str) -> PathBuf {
let mut n = module.file_name().unwrap_or_default().to_os_string();
n.push(".");
n.push(ext);
module.with_file_name(n)
}
/// Read at most `max` bytes; a larger file is refused without reading it all.
fn read_capped(path: &Path, max: u64) -> Result<Vec<u8>, LoadRefusal> {
let unreadable = |e: std::io::Error| LoadRefusal::Unreadable {
path: path.to_path_buf(),
detail: e.to_string(),
};
let f = std::fs::File::open(path).map_err(unreadable)?;
let mut bytes = Vec::new();
f.take(max.saturating_add(1))
.read_to_end(&mut bytes)
.map_err(unreadable)?;
if bytes.len() as u64 > max {
let total = std::fs::metadata(path)
.map(|m| m.len())
.unwrap_or(bytes.len() as u64);
return Err(LoadRefusal::TooLarge { bytes: total, max });
}
Ok(bytes)
}
fn read_pin(sidecar: &Path) -> Result<String, LoadRefusal> {
let missing = || LoadRefusal::DigestMissing {
sidecar: sidecar.to_path_buf(),
};
let f = std::fs::File::open(sidecar).map_err(|_| missing())?;
let mut text = String::new();
f.take(256)
.read_to_string(&mut text)
.map_err(|_| missing())?;
Ok(text)
}
fn check_pin(pin: &'static str, expected: &str, actual: &str) -> Result<(), LoadRefusal> {
let e = expected.trim().to_ascii_lowercase();
let shown: String = e.chars().take(64).collect();
if e.len() == 64 && e.bytes().all(|b| b.is_ascii_hexdigit()) && e == actual {
Ok(())
} else {
Err(LoadRefusal::DigestMismatch {
pin,
expected: shown,
actual: actual.to_string(),
})
}
}
fn untrusted(e: TrustError) -> LoadRefusal {
let failure = match &e {
TrustError::Unsigned { .. } => TrustFailure::Unsigned,
TrustError::SignatureInvalid { .. } => TrustFailure::SignatureInvalid,
TrustError::RootsUnreadable { .. } => TrustFailure::RootsUnreadable,
TrustError::VerifierUnavailable { .. } => TrustFailure::VerifierUnavailable,
};
LoadRefusal::Untrusted {
failure,
detail: e.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn memory_is_counted_over_all_memories_together() {
let mut l = Limiter::new(&RunLimits {
memory_bytes: 15,
..RunLimits::default()
});
assert!(l.memory_growing(0, 10, None).unwrap());
assert!(
l.memory_growing(0, 10, None).is_err(),
"two memories of 10 passed a limit of 15"
);
let (code, why) = l.hit.clone().unwrap();
assert_eq!(code, codes::MEMORY_LIMIT);
assert!(why.contains("20 in all"));
}
#[test]
fn tables_are_counted_over_all_tables_together() {
let mut l = Limiter::new(&RunLimits {
table_elements: 100,
..RunLimits::default()
});
assert!(l.table_growing(0, 60, None).unwrap());
assert!(
l.table_growing(60, 70, None).unwrap(),
"growing the same table is not double counted"
);
assert!(l.table_growing(0, 40, None).is_err());
}
#[test]
fn a_pin_must_be_64_hex_digits() {
let d = blake3::hash(b"x").to_hex().to_string();
assert!(check_pin("p", &d, &d).is_ok());
assert!(check_pin("p", &format!(" {}\n", d.to_uppercase()), &d).is_ok());
assert!(check_pin("p", &d[..63], &d).is_err());
assert!(check_pin("p", "", &d).is_err());
}
#[test]
fn a_long_error_is_cut_to_one_line() {
let s = one_line(&format!("a\n b\t{}", "é".repeat(500)));
assert!(s.starts_with("a b "));
assert!(!s.contains('\n'));
assert!(s.len() <= 404);
}
}