Repository navigation
·
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.pyheld 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.pykeeps 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 relativecache_diris 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, whosepath_renameandfd_readdircarry 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 (EBADForEPERMbeforeENOTDIR). 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 runof 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'spinned()and its newpin_of()find a pin by its name field compared as text, once, where agreplet.match any character; every meson configure passes--wrap-mode=nodownload(HarfBuzz keeps its ownnofallback; contourpy, which has no wrap file to fetch and records its configure options in the shipped zip, is the one exemption); the download usescurl --proto '=https' --proto-redir '=https' --tlsv1.2and names a present file that does not match with the fix;scripts/assemble_artifacts.pyreads the wheels, the sympy wheel, the wasi-libc and LLVM notices and the Unicode terms throughpinned_bytes(); the release's same-bytes job checks every file of the unpacked.tar.xzagainst itsSHA256SUMS(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 notestakes--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 asubprojects/folder its tarball did not ship, and.gitignorenames.env,.env.*,*.pem,*.keyand.claude/settings.local.json. The three artifacts andLICENSES/do not change;MANIFEST'srecipelines for the edited scripts do, and so doesSHA256SUMS's line forMANIFEST. - Pull requests into
devrun what their paths need (spec 138, step 2): the release workflow runs on every pull request intodevandmain, on every merge intodevand 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 withif::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-statusis the one required check. A merge intodevruns every normal-tier shard on three systems, the build shard andverifyon three. The judge jobs' matrices are generated fromtests/release_shards.toml, every reader of the build's files runs one local action, the build shard and the arm64 rebuild share one compile, andrelease.ymlis 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-datatree, 213 entries, with its record before granting it; the comparison walked the tree withos.walkand 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 withos.scandir, stamps each entry with oneos.lstatas 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,
truncatednames 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
devis 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-recordrunsuv run pytest --accept-from <record.json> --claims context/changes/with no secret, andaccept-push, in theacceptenvironment, commits the two progress files alone and pushes them ontodevwith a deploy key, only whiledevis still the commit judged. A change's CHANGELOG line, package rows and claims are its news file undercontext/changes/, whichrelease/version.py bumpassembles at a release;ci-statusrefuses a pull request intodevthat 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
devtomain(spec 138, step 4): its run is the whole workflow on the commitmainwill receive (a set built on macOS and never reused, both rebuilds and same-bytes, every shard of both tiers on three systems,verifyon three andquick), and its gather has every goal: noPLAN, 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 ownaccept-recordaccepts the record of both tiers, and.github/workflows/accept-release.yml, started by that run's completion ondev's own context, pushes the acceptance ontodevwhiledev'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 andmain's tip an ancestor.release/version.py datedates the section ondevbefore the release pull request, joins any news file merged afterbumpand moves the ledger's blocks under the version;release/version.py releasecommits nothing, tagsmain's tip and pushes the tag alone, so the commit tagged is the one judged plus its acceptance.bump,dateand the release's two reviews land in one pull request intodev, 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 exactlydate'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 thequickjob runs as itslintstep (uv run pytest tests/lint), gatingci-statuswithout 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 intests/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, andlintholds the accepted fuel too; a goal check past 60 seconds inquickis reported as a warning, never a failure.--accept-judgeis 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 andexpected/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 releaseparses:--tiercomes from thepytest11entry point ofwasi-python-judge-options, a dev-only member of the uv workspace undertests/judge_options/, and the wheel declares none. Thecoveragejob 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 withValueError(spec 132): CPython'ssysconfigreads these private build settings in isolated mode, and_PYTHON_SYSCONFIGDATA_PATHruns the module it names, so on a reused root a file an earlier guest wrote ran onimport pandasin a later call.run,run_asyncandSessionrefuse the key before any work; the security model now namesSCIKIT_LEARN_DATAbesideHOMEandPYTHONTZPATHas anenv=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.partialorpartialmethodnow 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 ananyOforoneOfbranch is now refused at registration, as one naming it inpropertiesalready was.tool()reads the class's__new__and__init__through its MRO, throughstaticmethod,classmethod,functools.wrapsand 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.mdsays 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_oneoffanswers misaligned pointers, unknown clock flags, and descriptors the guest does not hold or that are folders as wasmtime does (spec 140): a misalignedin_ptr,out_ptrornevents_ptrends the run, as wasmtime ends it (out_ptrandnevents_ptronce 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 isEINVAL; a number the guest never held, closed or renumbered away, a preopen and a mount's folder areEBADF, 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 isEFAULTwhere wasmtime ends the run or answers a refused subscription's errno, the events' room is 32 bytes per subscription, more than 4096 subscriptions areENOMEM, 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 isEBADFthere, 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