Skip to content

leptos-wasi-runtime 0.4.2

Latest

Choose a tag to compare

@codeitlikemiley codeitlikemiley released this 05 Aug 02:58
61c1d54

Fixed

  • Bounded static asset path validation to a fixed number of residual
    percent-decoding passes. A path such as /static/%2525…2541 previously drove
    a decode loop that shrank by two bytes per pass, costing time quadratic in
    the request path length; a 16 KiB path burned tens of milliseconds of CPU per
    unauthenticated GET and still resolved successfully. Chains longer than the
    budget are now rejected as residual percent-encoding.
  • Removed the generated route-list cache. It was keyed by TypeId, which
    identifies behavior only for types with a single inhabitant, so two different
    applications coerced to the same fn() -> _ pointer type shared one cached
    list and the second served the first's routes. That unsoundness was first
    narrowed to zero-sized closures, but the cache could never pay for itself:
    both supported hosts instantiate a fresh component per request, so a
    thread-local map starts empty on every lookup and is discarded with the
    instance. It only added a map allocation and a full deep clone of the route
    list to each request. Route discovery is now unconditional and correct by
    construction.
  • Accepted a v-prefixed version when resolving a pinned tool. The whole-version
    rule required a non-version character immediately before the digits, and
    cosign version reports GitVersion: v3.1.1 and nothing else, so no cosign
    build that has ever shipped could satisfy it. Every gate that verifies a
    signature was unreachable as a result: both component sync scripts and, through
    them, the authorization browser lane failed at tool resolution with cosign 3.1.1 is required while the correct cosign was on PATH. The rule is still a
    whole-version test — 4.0.2 continues to reject a reported 4.0.21, and
    3.1.1 rejects v13.1.1.

Changed

  • Split handler.rs (3,313 lines) into src/handler/ and executor.rs into
    src/executor/. The handler file held seven unrelated concerns plus both
    host adapters; it is now a 37-line module root over eleven submodules, and
    the largest file in the crate is 1,026 lines. This is code movement only:
    the public API is unchanged (cargo-semver-checks: 223 pass, no update
    required), lib.rs and request.rs are untouched, and the lib test set is
    identical test-for-test. Items shared between submodules are pub(super),
    which is the access they already had while everything lived in one file.

  • Enabled clippy::pedantic and moved the lint configuration into
    Cargo.toml's [lints] tables. forbid(unsafe_code) and
    deny(missing_docs) now cover tests, benches, and examples rather than the
    library target alone, and a plain cargo clippy reports exactly what CI
    enforces instead of the weaker default set. clippy::unwrap_used and
    clippy::panic are denied: both were already absent from production code,
    and a panic in a WASI component traps the instance rather than unwinding.

  • Sixteen public fallible APIs now document how they fail. Both Handler
    constructors, both handle_with_context, the five route generators,
    static_files_handler, from_wasi_request, the two WASI-to-HTTP
    converters, and Response::headers gained # Errors sections naming the
    variants they return.

  • render_mode takes &SsrMode rather than an owned value, dropping a clone
    on every SSR render, and the Preview 2 initializer no longer clones an
    executor it drops immediately afterwards.

  • Route discovery no longer runs on requests that cannot use the SSR router. A
    server function, a static asset, and an already-selected response all resolve
    without it, but discovery renders the whole application to extract its
    <Routes/> — measured at 183 µs of a 1054 µs request, and paid on every
    request because both supported hosts instantiate a fresh component each time.
    0.3.2 skipped this and 0.4.0 stopped doing so; the results were generated and
    then discarded. Restoring the skip recovers 7.35pp on server-function and
    static-asset requests (13.13% → 5.78% slower than 0.3.2, paired, n=5), and
    leaves requests that do consult the router unchanged, as expected.

    This trades away a validation step: an application reached only through
    server functions and static assets no longer rejects a malformed route table
    at request time. leptos_wasi::validate_route_table is the replacement — call
    it once from a test to apply exactly the rules the request path applies.

  • Every soak lane now compares against a baseline instead of only an absolute
    ceiling. The WASIp3 lane had no baseline at all — WASIp3 postdates 0.3.2, so
    there was nothing historical to compare it to — and its 150 ms ceiling left
    60 ms of headroom above the 88 ms observed, meaning a gradual regression
    could not have been detected there. Each lane now baselines at the oldest
    commit that supports its preview: WASIp2 at 0.3.2, WASIp3 at the final WASI
    0.3 migration. The absolute ceiling additionally runs on every lane, because
    a relative gate cannot see a slowdown that was already present when its own
    baseline was recorded.

  • Soak latency and throughput budgets are set per lane rather than globally.
    The WASIp2 lanes span the 0.4 adapter rewrite and carry a measured, documented
    per-request cost, so their budgets clear it and catch anything worse; the
    WASIp3 baseline already contains that cost and keeps the tight defaults. See
    PERFORMANCE.md for the measurements behind each number.

  • Every response the crate emits now carries X-Content-Type-Options: nosniff,
    not only static assets. The crate's own error bodies — 404, the static 405,
    and the 413/400 request-policy rejections — additionally declare
    text/plain; charset=utf-8; they previously carried no content type at all,
    which is the case content sniffing exploits. The default is applied once, at
    the end of the shared render path, and only when the header is absent, so an
    application that sets its own value through ResponseOptions still wins. The
    WASIp2 internal-error response is constructed outside that path and sets both
    headers itself. This also makes the 0.4.0 note about static routes adding
    X-Content-Type-Options true of the 405 response, which previously carried
    only Allow.

  • Advanced the companion pin from wasi-auth 0.1.0-alpha.4 to 0.1.0-rc.3,
    pinned at the commit its release tag points at rather than at the branch tip.
    Every symbol the fixtures import is unchanged across the range — upstream
    reports no .rs file touched in any consumer-visible crate — so the move is
    version metadata, regenerated artifact records, and lockfiles, with no code
    change on either side. wasi-http-authn stays at 0.2.0-alpha.3, because
    the legacy middleware subtree is byte-identical across the same range.

    The authorization artifact set is now recorded from the assets published on
    the upstream release for that tag, not from a local pipeline run. Upstream
    signs with a key generated per run and discarded, so the signature and key
    digests a local rerun produces can never match a previously recorded set;
    one immutable published bundle is the only thing those fields can pin. This
    also drops two revisions the lock could not resolve: an artifact_revision
    absent from wasi-auth history, and a [middleware].baseline_revision that
    belongs to the standalone wasi-http-middleware history and is now recorded
    with that provenance instead of presented as a wasi-auth commit.

  • Pinned the companion surface by digest. The lock named two crates while the
    fixtures compile six, and the four beyond those must still exist in the
    checkout for a --locked build, so a rename or a version bump upstream
    reached this repository as a resolution failure inside a fixture build.
    [authorization] now records the companion's own companion.toml — which
    upstream generates from cargo metadata across both its workspaces and
    drift-checks in its CI — and check-authz-companion.sh verifies that digest
    before it builds anything.

    The digest alone would only report that something moved, so the same check
    then names what moved: every directly usable crate must sit on its own
    workspace's locked version line, the legacy workspace included on its
    independent one, and the recorded artifact identity and component list must
    agree with the lock. A bumped crate now fails as companion crate wasi-authz-cedar is 0.1.0-rc.9, but wasi-auth is locked at 0.1.0-rc.3.

  • Recorded the wasi-http-middleware artifact set from a published release
    instead of from a local build. Its digests previously came from whoever last
    ran the upstream supply-chain script on their own machine, naming files that
    exist in no checkout — artifacts/ and reports/supply-chain/ are
    git-ignored upstream — so the signed-artifact gate for this bundle could not
    run at all. Upstream now cuts it from a tag, and the digests here come from
    those assets. The component digests moved, because they embed absolute source
    paths and so depend on where the build ran; the SBOM and WIT digests did not
    move at all, which is what a reproducible record should look like.

    This also corrects a claim made when the authorization pin advanced.
    [middleware].baseline_revision was described as resolvable only in the
    standalone wasi-http-middleware history and never in wasi-auth's. It is
    reachable from wasi-auth's main: the subtree import preserved that
    history rather than squashing it, and the same is true of the middleware
    source_revision and artifact_revision. The one revision that genuinely
    does not sit on mainline is the authorization artifact_revision this cycle
    already replaced, which is on a diverged branch.

  • scripts/verify-artifact-set.py compares the authorization bundle's identity
    against the lock's artifact_name/artifact_version rather than its
    name/version. Those pairs deliberately differ — the wasi-auth
    repository ships a wasi-authz bundle — and the static manifest audit
    already required the artifact_* keys, so the signed-artifact gate could not
    pass at the same time as the audit. That was true of the records shipped
    before this change, not a consequence of it; the gate is satisfiable for the
    first time here. A lock section without artifact_* keys, such as the
    middleware one, still matches on name/version.

Added

  • cargo make bench runs a host-native route-discovery benchmark over 3, 8,
    and 32 routes. Route discovery is the largest in-guest cost of a request
    that renders — roughly 183 µs of 1054 µs — and most of it happens inside
    leptos_router::RouteList::generate, which this crate does not own. A
    leptos bump that regressed discovery by 40% would move an end-to-end request
    by about 7%, inside the soak job's 8–12% budget, so nothing in CI would see
    it. Deliberately not wired into push/PR CI: the soak job's own notes record
    a ±0.7pp measurement problem on shared runners over 600 seconds, which a
    microsecond-scale sample cannot survive.

  • The public-API fixtures in tests/api-fixtures/ exercise every builder
    method. All three previously called only build/build_with_config, and
    four of the seven registration methods had no call site anywhere in the
    repository, so the one CI job whose purpose is compiling external-consumer
    imports could not have caught a change to them.

  • HandlerConfig::with_request_body_timeout_ns bounds how long a whole request
    body may take to arrive, and request_body_timeout_ns reads it back. It is
    None by default, so no existing deployment changes behaviour: request
    deadlines remain a deployment responsibility, and this is defense in depth
    for an ingress that cannot supply one. Exceeding the budget produces
    408 Request Timeout. Preview 2 polls the input stream against a monotonic
    timer instead of blocking on it, and Preview 3 races the length-limited
    collect against the same duration, so both previews apply one whole-body
    budget rather than a per-chunk one.

  • validate_route_table, which checks an application's route table without
    serving a request. Route discovery is skipped on requests that cannot use the
    SSR router, so a route table reached only through server functions and static
    assets is never validated in production; calling this once from a test closes
    that gap. It shares its implementation with the request path, so the rules a
    deployment enforces and the rules a test suite checks cannot drift apart.

  • Unit coverage for the previously untested request, response, redirect, and
    executor surfaces: WASI Preview 2 method and scheme conversion, Accept
    negotiation, referrer and Location sanitization, utils::redirect,
    axum_core and boxed-error body streaming, ResponseOptions application
    semantics, pollable dispatch through an injected poller, and both executor
    scheduling modes.

  • An end-to-end regression asserting that a nested percent-encoding chain is
    rejected without a latency blow-up relative to a plain 404.

  • Coverage for Location handling through the whole render path, rather than
    only through apply_server_fn_redirect in isolation: a Location written
    through ResponseOptions reaches the client unchanged, including an
    off-origin scheme and authority, while a Location set on a server
    function's own response is reduced to a same-origin path. The end-to-end
    open-redirect assertions now require an exact same-origin path instead of a
    matching suffix, which an absolute URL could satisfy.

  • A companion-authz CI job, so the authorization companion gates run on every
    push instead of only from a maintainer's checkout. It reads the pinned
    revision out of tests/middleware/components.lock.toml, checks wasi-auth
    out beside this repository at that revision — the layout both the fixtures'
    relative path dependencies and authz_repository require — installs the
    bundle published for the matching tag, and runs
    sync-authz-components.sh: companion revision and version, baseline
    ancestry, a clean companion tree, locked fixture clippy, every digest in the
    artifact set, both cosign verify-blob calls, and per-component
    wasm-tools validate with the WIT stack-order assertions. It then builds the
    WASIp2 lifecycle fixture --locked against the same companion.

    Advancing the pin stays a lock edit: the workflow names no revision, version,
    or tag of its own. The bundle is fetched from the release rather than rebuilt
    because the upstream signing key is ephemeral per run, so a rebuild could
    never reproduce the recorded signature digests.

    The same job also verifies the wasi-http-middleware bundle, on its own
    version line and its own release tag, so both signed artifact sets are now
    enforced on every push and no gate is left claiming it cannot run.

Documentation

  • Documented validate_route_table and with_request_body_timeout_ns in the
    README and the production contract. Both shipped in this cycle without
    reaching either document, and validate_route_table in particular is the
    mitigation for route-table validation no longer running on claimed requests,
    so its absence from the operational contract was the more consequential gap.
  • Rewrote the companion-pin sections of the README and MIDDLEWARE.md. They
    described the pin as deliberately held behind upstream and the authorization
    bundle as an unregenerated pre-consolidation set, which the pin move in this
    same cycle made false. They now state where the bundle comes from and why it
    cannot be reproduced locally, which is the part a reader needs in order to
    re-verify it.
  • Reorganised the README's runtime section, which had grown into a single
    paragraph covering composition, Spin canaries, companion components, and
    post-0.5 scope. It is now split by subject, with the per-runtime composition
    status as a table.
  • Documented component instance reuse, which is the largest throughput lever
    available to a WASIp2 deployment and is not controlled by this crate.
    wasmtime serve --max-instance-reuse-count defaults to 1 for WASIp2 and 128
    for WASIp3, so a Preview 2 deployment rebuilds the component for every
    request unless it opts out: measured at roughly 107 µs per request, about 14%
    of throughput at low concurrency, which exceeds any single change this crate
    has made to its own request path. The pooling allocator was measured
    alongside it and is indistinguishable from noise, so it is not recommended.
    Reuse makes guest statics outlive a request, so PRODUCTION.md records both
    the win and the obligation to test for it; LEPTOS_WASI_MAX_INSTANCE_REUSE
    runs this repository's e2e suite under reuse, where both previews pass.
  • Corrected PERFORMANCE.md, which presented 779 µs of a 1054 µs request as
    though instantiation dominated it. Instantiation is roughly 107 µs; the rest
    is the host HTTP path, the socket, and the probe's own client overhead. An
    earlier revision used the larger figure to argue that module size drove the
    regression, which the arithmetic never supported.
  • Documented the per-request Leptos nonce the handler already provides and a
    recipe for emitting a Content-Security-Policy from the application through
    ResponseOptions, including the 'wasm-unsafe-eval' requirement for
    hydration and the fact that leptos_meta's Stylesheet and Link
    components accept no nonce attribute.
  • Recorded that guest-served static responses carry no Cache-Control,
    ETag, or Last-Modified and answer no conditional request with 304, and
    that the Spin manifests obtain Cache-Control from spin-fileserver rather
    than from this crate.
  • Separated body size, which is bounded on both previews, from body read time,
    which is bounded on neither. The host supplies the request-read/idle deadline
    and the concurrent-instance cap in addition to a size limit.
  • Documented that a server-function response has its Location reduced to a
    same-origin path, that a Location written through ResponseOptions
    bypasses that reduction, and that both differ from leptos_axum and
    leptos_actix.