Skip to content

wasi-python 0.1.1

Latest

Choose a tag to compare

@github-actions github-actions released this 27 Oct 13:30
· 11 commits to dev since this release
Immutable release. Only release title and notes can be modified.

Changed

  • The platform module is split in three (spec 131). _platform.py held every platform difference at 1,464 of its 1,500 lines. The Windows class, its handle structure, its constants and its three helpers are now _platform_windows.py; what the Windows and POSIX classes both run (the walk's result and its refusal, the steps taken by path, the remover's budget and its walk by path, the cache's kind checks) is _platform_common.py, below both, so the import graph stays acyclic; _platform.py keeps the interface, the POSIX hosts and the one choice, at 828 lines. Nothing an embedder sees changes.
  • The module cache is read through the descriptor it was checked by (spec 135). A compiled module is loaded through the descriptor it was checked by, so the file read is the file checked; a cache whose path passes through a folder another user can write is not used, with a warning; a set given through artifacts= is hashed and compiled from one read; a cache folder or compiled module with an access list (macOS, Linux) is not used; on Windows the file is held open from its check to its load; cache_dir='' is refused, and a relative cache_dir is made absolute at the call. Running the runner with another user's home is not supported.
  • The pin moves to wasmtime 50 (spec 127). The wheel requires wasmtime>=50,<51; wasmtime-py 50.0.1 bundles wasmtime 50.0.0, whose path_rename and fd_readdir carry the fixes of the two advisories the runner had applied itself since 0.1.0a4 (a rename of a symbolic link named with a trailing slash could move a host folder into the sandbox on macOS; directory listings carried up to three bytes of host memory per entry), and whose fuel counts the work of a C++ call that ends in a caught exception, so the judge's row for it now passes. The runner's two shadows are gone, and a call without mounts passes only the three calls the cap on open files counts through the runner; the two sandbox checks now run against wasmtime's own calls. A slash-named rename on a bad descriptor or a read-only pair answers in wasmtime's own order (EBADF or EPERM before ENOTDIR). Fuel is equal to the instruction on the snapshot and in the judge's plain-reactor checks; a guest that catches a C++ exception is now charged for the work before the catch, a few thousand instructions in the kiwisolver check. On macOS, ending a process that compiled or loaded the guest no longer costs about 5 s: wasi-python run of a one-line program takes about 0.4 s where it took 5.4 s; wasmtime 50's release notes attribute this to registering unwind information with the system in bulk. A guest that passes a file call an argument out of its type's range ends with wasmtime 50's words, which name the direction.
  • The build reads its pins by exact name, and the release checks every inner file (spec 136). The build selects pins by exact name, meson builds never download, every licence input is read through its pin, the release checks every file inside its tarball, and the verify command in the release notes follows the repository's name. scripts/steps/common.sh's pinned() and its new pin_of() find a pin by its name field compared as text, once, where a grep let . match any character; every meson configure passes --wrap-mode=nodownload (HarfBuzz keeps its own nofallback; contourpy, which has no wrap file to fetch and records its configure options in the shipped zip, is the one exemption); the download uses curl --proto '=https' --proto-redir '=https' --tlsv1.2 and names a present file that does not match with the fix; scripts/assemble_artifacts.py reads the wheels, the sympy wheel, the wasi-libc and LLVM notices and the Unicode terms through pinned_bytes(); the release's same-bytes job checks every file of the unpacked .tar.xz against its SHA256SUMS (no other file, no symlink, every published file listed), and the build job verifies the set again just before it packs it; release/version.py notes takes --repository, which the publish job sets to $GITHUB_REPOSITORY, and a unit check holds the documented command equal to the one the attest job runs. The zero-patch check now names a subprojects/ folder its tarball did not ship, and .gitignore names .env, .env.*, *.pem, *.key and .claude/settings.local.json. The three artifacts and LICENSES/ do not change; MANIFEST's recipe lines for the edited scripts do, and so does SHA256SUMS's line for MANIFEST.
  • Pull requests into dev run what their paths need (spec 138, step 2): the release workflow runs on every pull request into dev and main, on every merge into dev and weekly, besides the tag and a dispatch. One decision job, release/ci_plan.py, reads the changed paths with git and gates every later job with if:: quick (ruff, mypy, the unit tests, the rules) runs always; the judge's shards run from the cached set as the paths need (Linux and Windows for any code, macOS when the runner, the set or the harness changed, the upstream groups on Linux when the set, the suites or the harness changed); the gather is narrowed to the pairs that ran, lists a growth instead of failing on it, and leaves G7 to the release; ci-status is the one required check. A merge into dev runs every normal-tier shard on three systems, the build shard and verify on three. The judge jobs' matrices are generated from tests/release_shards.toml, every reader of the build's files runs one local action, the build shard and the arm64 rebuild share one compile, and release.yml is 1,316 lines, from 2,602.
  • The per-call comparison of matplotlib's data takes half the interpreter's time (spec 128). Every call compares the whole mpl-data tree, 213 entries, with its record before granting it; the comparison walked the tree with os.walk and built a path object per entry, 2.3 to 2.9 ms of interpreter time per call under the zip folder's lock. It now lists with os.scandir, stamps each entry with one os.lstat as the record does, and keeps plain strings: the same comparison, the same one stat per entry on every system, about 1 ms. On the research machine the eight-thread wall of eight hellos shortened by 14 to 33 % in three windows with a result file (their ratio to the sequence 1.5 to 1.9 before, 1.9 to 2.0 after; on a shared machine the wall is the steadier number), and a hello in sequence is one to two milliseconds shorter (benchmark facts, not checks). The memo behind it found no lock of the runner's held across a guest and none waited on: concurrent runs wait on the interpreter's lock over the runner's per-call checks, and that ceiling is named, not hidden. A check now pins that the home thread holds no process lock while its guest runs.
  • A guest's output is read from a pipe on macOS and Linux (spec 129). wasmtime hands a custom output its bytes 4 KiB at a time on a thread of its own, and the runner's callback there cost about 14 µs a piece, most of it the hand-off and a Python thread state made and destroyed per piece: 32 MiB of output took 160 to 240 ms, ten thousand printed lines 300 ms. wasmtime now writes each stream into a pipe the runner reads on one thread of its own per call: about 40 ms and 35 ms. The output cap holds to the byte, truncated names the same streams, stdout and stderr stay apart, and nothing in the API or its defaults changes. Windows keeps the callback for now.
  • Every merge into dev is accepted by CI (spec 138, step 3): the run after a merge judges the normal tier on three systems, and two jobs follow a green gather. accept-record runs uv run pytest --accept-from <record.json> --claims context/changes/ with no secret, and accept-push, in the accept environment, commits the two progress files alone and pushes them onto dev with a deploy key, only while dev is still the commit judged. A change's CHANGELOG line, package rows and claims are its news file under context/changes/, which release/version.py bump assembles at a release; ci-status refuses a pull request into dev that is not documentation only and has no news file, or no review file whose verdict begins "Lands", committed after its last code, by a reviewer other than its author. A documentation-only push onto a green head re-uses that head's run, and the weekly run opens an issue when it is red.
  • A release is a pull request from dev to main (spec 138, step 4): its run is the whole workflow on the commit main will receive (a set built on macOS and never reused, both rebuilds and same-bytes, every shard of both tiers on three systems, verify on three and quick), and its gather has every goal: no PLAN, G7 holding the newest acceptance to the run's own set, clean and whole, and each release-tier row that moved since the last release listed with the reason of a news file or of the release-tier ledger. Its own accept-record accepts the record of both tiers, and .github/workflows/accept-release.yml, started by that run's completion on dev's own context, pushes the acceptance onto dev while dev's tip is the head judged, red when it is not. When the acceptance lands, the release pull request's next run re-uses the green one. The release pull request is refused any commit that changes the accepted state and is not an acceptance, and its build job checks that the version's section is dated, its tag free and main's tip an ancestor. release/version.py date dates the section on dev before the release pull request, joins any news file merged after bump and moves the ledger's blocks under the version; release/version.py release commits nothing, tags main's tip and pushes the tag alone, so the commit tagged is the one judged plus its acceptance. bump, date and the release's two reviews land in one pull request into dev, the release's preparation, which the decision job judges as documentation, since its one change outside documentation is the version line and its change to the release-tier ledger is exactly date's move.
  • The judge's diet (spec 138, step 5): the eight writing conventions (the specifications' shape and that they leave no choice undecided, one-line Markdown paragraphs, the labels of an executed write-up, the README's coverage badge, and the three rules that code and strings name no document of the knowledge tree) leave the goal count for tests/lint/, which the fast tier does not collect and the quick job runs as its lint step (uv run pytest tests/lint), gating ci-status without a ratchet; the four rules that hold more than writing stay goal checks. The fast tier has a budget: a normal-tier goal check whose fuel is past its family's cap in tests/lint/budget.toml (twice today's largest) is red on the pull request that grew it (the gather) and refused at the acceptance, until the file names the specification that will move it, and lint holds the accepted fuel too; a goal check past 60 seconds in quick is reported as a warning, never a failure. --accept-judge is needed only when a verdict-bearing judge file changed since the last acceptance (tests/harness/paths.py, VERDICT_BEARING: the known gaps, the upstream suites' TOML files and expected/ lists, CPython's plan, the shard plan, the public API's list, the release tier's claims, which join the judge's files) and is refused as unnecessary otherwise, the judge's hash recording the rest. uv run pytest --tier release parses: --tier comes from the pytest11 entry point of wasi-python-judge-options, a dev-only member of the uv workspace under tests/judge_options/, and the wheel declares none. The coverage job is gone: on a run with the merge-grade set each Linux leg of the product's own shards runs under coverage.py and sends its report to Codecov, which merges them into the badge.

Security

  • An env= key that begins with _PYTHON_ is refused with ValueError (spec 132): CPython's sysconfig reads these private build settings in isolated mode, and _PYTHON_SYSCONFIGDATA_PATH runs the module it names, so on a reused root a file an earlier guest wrote ran on import pandas in a later call. run, run_async and Session refuse the key before any work; the security model now names SCIKIT_LEARN_DATA beside HOME and PYTHONTZPATH as an env= that can point a package at a reused root.
  • The door hands the guest exactly the bytes it checked (spec 137), a host that raises a wasmtime exception is a host fault like any other, and a nested door call is a fault; the security model says a writable grant over the module cache is refused, names XML parsing beside dict keys under the snapshot's fixed key, and says a process that used wasmtime-py directly must not fork and run a guest.
  • A class tool's constructor keywords bound with a partial are fixed (spec 133): A class registered as a tool whose constructor binds a keyword with a functools.partial or partialmethod now has that keyword fixed, as a function's is: a frame naming it is refused and a given schema naming it is refused at registration. A given schema that names a bound keyword in an anyOf or oneOf branch is now refused at registration, as one naming it in properties already was. tool() reads the class's __new__ and __init__ through its MRO, through staticmethod, classmethod, functools.wraps and a delegating metaclass; before, a guest frame could set such a keyword and the constructor ran with the guest's value.
  • Fuel on wasmtime 49.0.0 under-counts work that ends in a caught C++ exception (spec 134; an upstream advisory, GHSA-m63x-6p34-q65x): SECURITY.md says so, the security model and the fuel page say to keep a finite deadline beside fuel there, and the judge holds the gap until the pin moves to wasmtime 50. The judge also holds both binaries' exception tags at an even count, which keeps a second advisory's precondition away.
  • poll_oneoff answers misaligned pointers, unknown clock flags, and descriptors the guest does not hold or that are folders as wasmtime does (spec 140): a misaligned in_ptr, out_ptr or nevents_ptr ends the run, as wasmtime ends it (out_ptr and nevents_ptr once the subscriptions are answered, so a refused one answers its errno first), and a metered run it ends reports its fuel; a clock with an unknown flag bit is EINVAL; a number the guest never held, closed or renumbered away, a preopen and a mount's folder are EBADF, and a file the guest holds is ready on whatever number it was renumbered onto. The places the runner's poll still answers otherwise than wasmtime's are named in its docstrings, each with a check: a pointer past memory is EFAULT where wasmtime ends the run or answers a refused subscription's errno, the events' room is 32 bytes per subscription, more than 4096 subscriptions are ENOMEM, the events' padding is written as zeros, a folder the guest opened, a file on a read-only place polled for writing and a closed stdio number are ready, stdio renumbered onto another number is EBADF there, a file at its end has no hangup flag, and every ready descriptor is reported at once.
  • A missing root's folders are made at 0700 and refused on Linux and macOS if another user made them (spec 139), the sweep of dead private folders removes only this user's, and the matplotlib data guard refuses the names Windows reserves or rewrites, names holding a format character HFS+ drops (U+200C, U+FEFF and the rest of Unicode's category Cf) and names equal under case folding. A made root that is renamed away and replaced before it is walked, and a link at a missing name on the walk by path, are refused. One owner test, by the effective uid, serves the made folders and the sweep. On Windows, from Python 3.13, a made folder gets CPython's access list for the user, SYSTEM and the Administrators; no owner is compared there.

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.1 \
  --source-ref refs/tags/v0.1.1 --deny-self-hosted-runners