Skip to content

wasi-python 0.1.0a7

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 19 Oct 13:21
· 633 commits to main since this release
  • Fixed: an async gate that raises GeneratorExit refuses the call (spec 075; the 0.1.0a6
    security review's F1). An async gate (an async def, a function returning an awaitable, or an
    object with an async __call__) that raised GeneratorExit or a subclass used to allow the
    call, and the tool ran; an async tool that raised it was answered ok with None. Now the
    gate's call is denied and the tool's tool_error, each with the fault in the record, as a
    sync gate's and a sync tool's already were. The result of an awaitable counts only if it
    returned. Also (spec 060's build review, N1): a tool list over the 16 MiB answer cap is refused
    from its length on each list frame, without its answer being built. No public name, default
    or answer on the wire changes.

  • Changed: the snapshot pre-imports no package (decision 34, spec 069). It starts with the
    interpreter and the stdlib's bootstrap modules (encodings, io, os, sys, json, csv,
    re, _colorize), for every package now and later; the judge refuses a snapshot that holds
    any other top-level module. A bare call costs 15.6M guest instructions instead of 83.5M; a call
    that imports pandas pays it, 4.60G in all instead of 83.5M (a small numpy job about 0.7G more
    than before), so a fuel= cap set for a pandas job on the snapshot needs that much more. The
    snapshot is 30.4 MB smaller (44.6 MB) and the wheel about 11 MB smaller. numpy, pandas and the
    other packages now import with the call's environment. A session's shutdown costs about 43M
    instructions when nothing beyond the stdlib was imported (about 306M before). snapshot=True
    is unchanged as an argument; no public name, signature or default changes. MANIFEST's
    x-preimport line is now read from the recipe. Known and unchanged: run on the snapshot
    loses an unclosed file's buffered bytes when __main__ defines a function (a gap until spec
    073; close your files before a call ends).

  • Bounded sessions (spec 055). Session keeps one instance of the runtime across turns:
    session.run(code) and await session.run_async(code) each run one turn in __main__, and
    variables, functions, classes and imports carry over. A turn echoes its final expression,
    reports an exception with <turn N> in its traceback, and reports sys.exit() as its exit
    status while the session goes on, as a Jupyter kernel does. Each turn has its own fuel,
    deadline, output cap, cancel, file-call and tool-call allowance; the session has one memory
    cap, an idle bound (idle_s, default 15 minutes) and a lifetime (lifetime_s, default 1
    hour), which caps every turn's deadline; max_open_files counts across a session's turns and
    its shutdown, since its store keeps what they open. A trap, os._exit, a bound, close() or a turn's
    exit() or quit() ends it; session.ended says which, and later turns raise
    SessionEnded. A session's close and expiry, and exit() or quit() in a turn, run
    Python's shutdown (atexit, flushes) within shutdown_s seconds and the session's fuel, as a
    Jupyter kernel's do; past that budget the session is stopped; sys.exit() keeps the session;
    session.shutdown_outcome says whether the shutdown finished, was stopped or did not run. Added:
    Session, SessionEnded, three Defaults fields (session_idle_s, session_lifetime_s,
    session_shutdown_s), stats()["sessions_open"], and two exports in both binaries, turn and
    shutdown, whose contract the README gives for any embedder. run is unchanged.

  • Deep recursion raises RecursionError (spec 071). A __repr__ that calls repr(self),
    lru_cache recursion, a recursive sort key or a numpy, pandas or lxml callback used to end the
    guest in a wasm trap; it now raises, on both binaries, at wasmtime's default 512 KiB stack.
    The guest narrows CPython's C-stack guard to 128 KiB with CPython's own
    PyUnstable_ThreadState_SetStackProtection, called from the reactor's recipe: no patch.
    Changed: such recursion stops at about half the depth it reached before trapping (214
    levels for the __repr__ case, 40 to 250 by shape), so a program that recursed that deep and
    finished now raises. Callbacks out of expat, recursion in C over very deep data, and freeing
    very deep structures still trap, with the limits the README lists. The runner now states its
    wasm stack, 512 KiB, instead of taking wasmtime's default, and the judge's guest driver makes
    the runner's engine (fuel, epoch interruption, that stack). python.wasm, the snapshot and
    MANIFEST change; the zip does not.

  • On Linux, a host's faulthandler can no longer turn a guest trap into the host's death
    (spec 071). If faulthandler was enabled or disabled after the runner's first call, a guest
    trap used to kill the host with SIGILL (and after a late signal.signal for SIGILL, hang
    it); run now refuses such a call with RuntimeError before the guest runs, says why, and
    names the remedy. Install faulthandler and crash reporters before the first call, or not at
    all.

  • The build runs only the pinned host Python (spec 079; the 0.1.0a6 security review's
    F2). FreeType's configuration headers, HarfBuzz's generated sources and matplotlib's
    matplotlibrc were written by whatever python3 the build host had first on PATH, and
    CPython's configure ran it once. Now step 10 puts a shim for the pinned CPython 3.15.0 first
    on every step's PATH. Behind it, scripts/build.sh puts a stand-in for every other Python
    name the host has, which fails the step if it is run. A build host needs no Python of its
    own. The artifacts are the same bytes; MANIFEST's recipe lines change.

  • The release judges in parallel shards (spec 063). The release runs numpy's and pandas' own
    test suites (eleven groups, 63,718 passing tests) on Linux, one group of them on macOS and on
    Windows, every product check and the runner's own tests on all three systems, and the rules and
    the judge's own tooling on macOS and Linux, in eighteen parallel jobs after the build. A
    gather job reads every shard's record by id and SHA-256 and publishes nothing unless each
    shard ran exactly its share on the release's bytes, nothing failed, the probe passed on three
    systems, and the lane's newest acceptance is on the same python.wasm. uv run pytest --shard=<name> runs one shard. Every package now names its own suite, or why not yet, in
    tests/upstream/packages.toml.

  • lxml, with libxml2 and libxslt built in (spec 046). lxml 6.1.3's lxml.etree and
    lxml.objectify are builtins, over libxml2 2.15.4 and libxslt 1.1.45 with libexslt: XPath,
    XSLT, XInclude, C14N, DTD, XML Schema, RELAX NG and Schematron validation, the HTML parser,
    iterparse, and files read and written by path inside a grant. lxml's Python package
    (lxml.html, lxml.builder, lxml.sax, ElementInclude, lxml.html.diff) is in the zip with
    its METADATA. pandas' read_html with no flavor now works (lxml is its first flavor), and so
    do read_xml and to_xml with their default parser. The judge's golden job lxml_xml equals
    native lxml at the same pins. What differs from native, as the README says:

    • Character encodings are wasi-libc's iconv, not GNU libiconv: windows-1252, ISO-8859-2
      and -15, cp1251, KOI8-R, TIS-620 and UTF-16 round-trip every character; Shift_JIS,
      ISO-2022-JP and GB2312 read a few characters as other characters; cp932, EUC-JP, Big5 and
      cp950 documents fail to parse (in HTML and recover=True their text ends at the first
      unreadable character, with no exception); most CJK writes raise LookupError. GB18030
      fails at its first non-ASCII byte, on purpose
      , because wasi-libc decodes 1,617 of its
      characters wrongly without an error: recipes/iconv_refusals.c, linked into the reactor
      with -Wl,--wrap=iconv_open, iconv and iconv_close, until a wasi-sdk carries musl's fix
      (context/upstream/15-wasi-libc-gb18030-decoder/). Strict XML raises, recover=True keeps
      no root, the HTML parser keeps only the ASCII text before it, read_html of a Chinese
      table raises; no character is read wrongly in any mode. But a table that reaches the first
      non-ASCII byte comes back cut there with no exception: its last row may be all NaN, a
      column may turn float, and the rows and tables after it are missing (check error_log).
      The README gives the working route: decode with Python's gb18030 codec and give lxml the
      text.
    • XSLT reads and writes files by default. A stylesheet without an access_control, or a
      document it loads with document(), can read any file in a grant (external entities
      included) and write into any writable grant; use etree.XSLTAccessControl.DENY_ALL for
      untrusted stylesheets. lxml's parser defaults stay the safe ones.
    • Left out: lxml.isoschematron (it reads its stylesheets by file path, and their terms
      are not approved; a stand-in raises ImportError; etree.Schematron works), zlib in
      libxml2 (XMLParser(decompress=True) raises; lxml compresses output through Python's
      gzip), cssselect and lxml_html_clean. A catalog is read only from a grant at /etc/xml.
    • One more carried patch:
      recipes/patches/libxml2-2.15.4/0001-xmlio-without-dup-on-wasi.patch lets libxml2's file
      input and output work without dup(), which WASI preview 1 does not have (without it
      libxml2 does not compile for wasm32-wasip1). Its facts sheet for the owner:
      context/upstream/patches/libxml2-2.15.4/0001-xmlio-without-dup-on-wasi.md.
    • The build: two new steps, 36 (libxml2 and libxslt, by their own configure, under an empty
      environment, with the pinned pkgconf and an empty search path) and 54 (lxml's two modules,
      from the Cython 3.2.9 C its sdist ships); three new pins. libxml2's default catalog is
      native Linux's /etc/xml/catalog, not a build folder, and a new judge check holds every
      later package to the same rule: neither binary names the checkout or the builder's home.
    • Licences: LICENSES/libxml2.txt (with the notices of dict.c, list.c and
      timsort.h), LICENSES/libxslt.txt and LICENSES/lxml.txt.
  • regex is built in (spec 047). regex 2026.9.29's regex._regex is a builtin module and its
    Python package is in the zip with its METADATA: fuzzy matching, full Unicode properties from
    regex's own tables, graphemes, variable-length lookbehind and captures() give native regex's
    answers (the golden job regex_features). One carried patch,
    recipes/patches/regex-2026.9.29/0001-callback-signatures.patch, gives five callbacks the
    parameter CPython passes (Pattern.groupindex, pickling a pattern, Match.capturesdict(),
    allcaptures() and allspans() each trapped the guest without it); its upstream facts sheet
    is context/upstream/patches/regex-2026.9.29/0001-callback-signatures.md, for the owner to
    send. The init function of each of the 71 cross-built builtins now runs inside CPython's
    package context, set to its dotted name, as a native import from a shared library does, so a
    single-phase module keeps its import name (regex._regex, and a compiled pattern pickles);
    numpy's, pandas' and matplotlib's modules are multi-phase, or already named in full
    (pandas._libs.json), and do not change. The judge checks every dotted
    builtin's __name__ on both binaries. Licence: Apache-2.0 and CNRI-Python, in
    LICENSES/regex.txt with a line naming the patched file. timeout= counts the guest's clock;
    fuel and the deadline are the limits that hold.

  • The judge's small follow-ups (spec 070). The README's Limits now says that a refused
    discovery frame (max_tool_calls=0, or a request budget too small for it) makes from host import tools raise ImportError, and a check holds it. The release scan reads a download's
    path: as a path, so ./release-assets is release-assets; the progress history's volume
    budget is tested at its exact byte, and a volume holding a carriage return is reported. Nothing
    in the artifact set, the wheel's code or the guest changes.

  • Security: a cap on files open at once (spec 068; the 0.1.0a2 security review's F11 and spec
    055's pre-build review's F2; spec 049, Decision 8, exception 2). max_open_files= on run and
    run_async, Defaults.open_files and wasi-python run --max-open-files, default 1,024: the
    files and folders a guest holds open at once, in grants, the root and mounts alike. Past it an
    open answers EMFILE before anything is opened, and the guest can close files and go on. Until
    now a guest could open host descriptors until the process's own limit (122,870 on the
    research's Mac), and saw EIO there. The cap protects the embedder's process only within that
    process's soft descriptor limit: raise it, or set the cap below it (the README's Safety);
    wasi-python run raises its own. Descriptors wasmtime holds while it resolves a deep path are
    not counted (Not protected). Calls without mounts now pass path_open, fd_close and
    fd_renumber through the runner, which forwards them to wasmtime; fuel does not change.

  • Changed: a new, tighter default (spec 068; spec 049, Decision 8, exception 1): a program
    that holds more than 1,024 files open at once passes a larger max_open_files, or None.

  • Windows judges a grant's path as Linux and macOS do (spec 076). A grant or root with
    .. after a missing folder or a file (new\..\x) is now refused on Windows too, before any
    work; before, Windows dropped the .. by its text, made x and ran the guest. On Windows a
    host path with a name that holds :, ends in a space or a period, or is a reserved device
    name (NUL, COM1 and the rest, and COM¹ to COM³ and LPT¹ to LPT³; spec 077) is
    refused, since Windows rewrites such names and the runner's checks could then judge other
    folders than the ones granted (a spelling the embedder gives, never the guest). So is a name
    that holds <, >, ", |, ?, * or a control character, and, out of caution, CONIN$
    or CONOUT$: the refused names are exactly those Python's ntpath.isreserved refuses (spec
    077), and a root still to be made through one is refused before any folder is made. A path rooted with no drive takes the current folder's drive. The
    README states the one errno that differs by system on a grant: a write through a descriptor
    opened only to read (or the reverse) is EBADF on Linux and macOS and EACCES on Windows,
    wasmtime's own mapping of the host's error.

Verify a download (gh logged in; see the README's Verifying a download):

gh attestation verify <asset> --repo ClayGendron/wasi_python \
  --signer-workflow ClayGendron/wasi_python/.github/workflows/release.yml@refs/tags/v0.1.0a7 \
  --source-ref refs/tags/v0.1.0a7 --deny-self-hosted-runners