0.7.0 - SBOM and Dependency Auditing
Cown-lifecycle correctness fixes — three use-after-free paths in the
CownCapsule pickle / acquire / noticeboard machinery now hold the
inner BOCCown alive across the writer's wrapper drop — plus
supply-chain hardening: pinned and hash-verified Python dependencies,
SHA-pinned GitHub Actions, dependabot coverage, vulnerability scanning,
and PEP 770 SBOMs embedded in every wheel.
New Features
- PEP 770 SBOMs in every wheel — every wheel built by
.github/workflows/build_wheels.ymlnow embeds a
CycloneDX 1.6 <https://cyclonedx.org/specification/overview/>_
JSON SBOM under<dist>-<version>.dist-info/sboms/bocpy.cdx.json.
Generation runs inside cibuildwheel's repair step on every platform
(Linuxauditwheel, macOSdelocate, Windows direct injection)
via the new stdlib-onlyscripts/build_sbom.py. The
injectsubcommand rewrites the wheel'sRECORDatomically
(temp file + rename). - SBOM verification in CI — the new
verify_sbomsjob in
build_wheels.ymlre-downloads the extracted SBOM artifact and
runs two checks:scripts/validate_sbom.py(stdlib-only
structural validator pinning bocpy's wire format) and
grype <https://github.com/anchore/grype>_ (third-party SBOM
scanner) with--fail-on high. A separatesbomsartifact is
also uploaded by themergejob for downstream consumers. bocpy.__version__— a runtime version attribute derived
fromimportlib.metadata.version("bocpy"), with a
PackageNotFoundErrorfallback. Exported frombocpy.__all__
and documented in__init__.pyi.pyproject.tomlremains the
single source of truth for the version.- New documentation — :doc:
sbomwalk-through covering the
embedded SBOM format, extraction recipes, and verification commands. wait(noticeboard=True)final-state capture — :func:wait
now accepts anoticeboardkeyword that returns the final
noticeboard contents as a plaindictat shutdown (after the
noticeboard thread exits, before the entries are freed). Useful
for surfacing an early-stopping result, last error, or aggregated
counter that a behavior deposited just before the runtime
quiesced, replacing the oldersend/receivehandshake
that earlier examples used. Combined withstats=Trueit
returns a new :class:WaitResultNamedTuple(also exported
frombocpy.__all__) carrying both snapshots. The
examples/prime_factor.pyexample was migrated to the new
pattern.
Bug Fixes
- Cown-in-cown use-after-free — a
Cownembedded inside
another cown's value, a message-queue payload, or a noticeboard
snapshot was previously freed when the writer's local wrapper
dropped, because pickle bytes carry no refcount on their own.
CownCapsule_reducenow takes an inheritingCOWN_INCREFthat
_cown_capsule_from_pointer_inheritingconsumes on unpickle, so
the innerBOCCownsurvives until the consumer drops its
decoded wrapper. Affects every cross-cown reference shape — see
the newTestCownInCownclass for the full container-shape fuzz. - Acquire-failure poisoned-state — when
pickle.loadsfailed
partway throughcown_acquire, the cown was left in a
half-acquired state with the encoded bytes still in place. A retry
would re-run pickle against bytes whose embedded inherited refs
had already been partially consumed by pickle's error path,
risking dereferences of freedBOCCown*pointers. The cown's
xidatais now recycled on the failure path and a guard at the
top ofcown_acquirerejects any future acquire with a
deterministicRuntimeError; the worker recovery arm surfaces
it on the failing behavior's result cown. - Noticeboard hidden-cown audit — when a noticeboard value
reached aCownvia a route the pin walker cannot see — custom
__reduce__/__getstate__,copyreg.dispatch_table,
closure capture, module-level cache — the borrowing reconstructor
produced a token whose innerBOCCownwas not held alive by
the entry's pin set, leaving the next reader to UAF after the
writer's wrapper dropped. A per-thread borrowing context
(BOC_NB_CTX) now audits everyCownCapsule_reduceagainst
the caller's pin set during the noticeboard write pickle and
fails the wholenotice_write/notice_updateclosed if
any cown is unaccounted for. UnicodeDecodeErroron non-UTF-8 Windows locales —
Behaviors.startreadworker.pywithopen(path), which
picks uplocale.getpreferredencoding(False). On cp1252
(English Windows) the UTF-8 em-dashes in the worker source were
silently mojibake-d; on cp949 (Korean Windows) the read failed
withUnicodeDecodeError: 'cp949' codec can't decode byte 0xe2
andbocpycould not start at all (reported in
#14 <https://github.com/microsoft/bocpy/issues/14>_ by
@Forthoney <https://github.com/Forthoney>_). Fixed by passing
encoding="utf-8"explicitly inBehaviors.start, and the
same fix was applied to every otheropen()site in the repo
that reads or writes text known to contain non-ASCII bytes
(sphinx/source/conf.py,examples/sketches.pyx2,
export_module.py).- Silent worker-startup failures —
Behaviors.start_workers
raninterpreters.create()andinterpreters.run_string()
on the worker thread without a try/except, so a failure in either
killed the thread without ever replying onboc_behavior. The
parent's boundedreceive()then timed out with no diagnostic.
Both calls are now wrapped, and every failure path sends a
formatted traceback overboc_behaviorso the parent sees a
structured error instead of a timeout. - Silent worker bootstrap import failures — the generated
bootstrap script that loads the user module into each worker
sub-interpreter is now wrapped in a top-level try/except. Any
BaseExceptionis formatted with the user module name and sent
overboc_behavior(falls back tosys.stderrif the
message-queuesenditself raises), then re-raised so
run_stringreports it as well. Module-import failures that
previously surfaced only as a worker-startup timeout now arrive
as a proper traceback. boc_sched_worker_pop_slowskippedpopped_local— the
slow-path pending-fallback and WSQ-dequeue branches returned
work without bumpingpopped_local(the fast path always
did), so the documented producer/consumer identity in
:c:type:boc_sched_stats_twas violated whenever the fairness
arm fired or a worker entered the slow path directly. Both
branches now incrementpopped_localand reset the batch
budget, matching the fast path. The header's reconciliation
paragraph was also tightened to a "near-identity" that explicitly
accounts for fairness-token pops (which are re-enqueued via raw
boc_wsq_enqueuerather thanboc_sched_dispatch, leaving
consumer-side counters without a matching producer-side bump).
Supply Chain
- Hashed and pinned Python dependencies — every CI dependency is
resolved into aci/constraints-<extra>.txtfile via
uv pip compile --universal --generate-hashesand installed with
pip install --require-hashes. Covers thetest,linting,
docs, and newauditextras.bocpyitself is then
installed viapip install -e . --no-depsso an editable build
cannot smuggle in an unpinned transitive dependency. - Vulnerability scanning — new
auditjob inpr_gate.yml
runspip-audit --strictagainst every constraints file on every
PR.pip-audititself is pinned viaci/constraints-audit.txt
and self-checked. A new.github/workflows/nightly_audit.yml
re-runs the audit nightly againstmain. - SHA-pinned GitHub Actions — every
uses:line in
.github/workflows/is now pinned to a full 40-char commit SHA
with a trailing# vX.Y.Zcomment. - Dependabot coverage — new
.github/dependabot.ymlcovers
three ecosystems (piprooted at/ci,github-actions
rooted at/,piprooted at
/templates/c_abi_consumer), grouped weekly per ecosystem. - Downstream template pinned —
templates/c_abi_consumer
pinsbocpy~=MAJOR.MINORas both a build requirement and a
runtime dependency. Thefinalize-prskill bumps it in
lock-step with the root version. - New
SUPPLY_CHAIN.md— top-level policy doc describing
everything above with the exact regeneration commands.
Documentation
- Cown pickle-leak note — :class:
Cownnow documents that
pickle.dumpson a cown produces bytes that carry one strong
reference per embedded cown; orphan bytes (never unpickled in the
producing process) leak one strong ref per byte string. The bocpy
runtime never produces orphan bytes; the leak surface only
applies to third-party code that callspickle.dumps(cown)
directly. - Noticeboard cown-lifetime guarantee — :func:
notice_writeand
:func:notice_updatenow document that values may embed
:class:Cownreferences and that the noticeboard keeps each
embedded cown alive for as long as the entry remains. The new
paragraph in :doc:noticeboardmirrors this guarantee for
readers. - Noticeboard final-state capture guide — :doc:
noticeboard
gained a "Reading the Final State at Shutdown" section covering
thewait(noticeboard=True)contract, the combined
wait(stats=True, noticeboard=True)form returning
:class:WaitResult, the empty-dict fallbacks for the
never-started and never-written cases, and the recommendation
to usesnap.get(key)since :func:waitquiesces as soon as
every behavior completes with no guarantee any particular write
has landed. The early-stopping worked example in the same file
was rewritten around the new API.
Tests
TestCownInCownintest/test_boc.py— pins the
cown-in-cown UAF fix with three cases: an inner cown allocated
inside a behavior and observed by a downstream behavior, a cown
sent through the message queue and consumed by the receiver, and
a 50-trial deterministic fuzz over seven container shapes
(list/tuple/dict/@dataclass(slots=True)/
__dict__-only /__slots__-only / 2-levelCown[Cown[T]]).TestAcquireFailureTerminalintest/test_boc.py— pins
the poisoned-state contract: after a deserialisation failure the
cown stays permanently unavailable and every subsequent waiter
receives the deterministicRuntimeErroron its result cown.- Noticeboard hidden-cown regressions in
test/test_noticeboard.py— exercises__reduce__and
copyreg.dispatch_tablereductions that hide a cown from the
pin walker, and verifies the audit rejects the write closed
rather than leaving an unpinned borrowing token in the entry.
A complementary_VisibleCownPairtest guards against the
over-eager-rejection regression. test/test_version.py— coversbocpy.__version__:
pyproject parity, PEP 440 shape,__all__export, and the
importlib.metadatafallback path (subprocess test that
verifies the WARNING is emitted when the metadata lookup raises).test/test_build_sbom.pyandtest/test_validate_sbom.py
— full coverage of the SBOM generator and validator: CycloneDX
1.6 shape, deterministic UUIDv5 serialNumber,
SOURCE_DATE_EPOCHtimestamp, per-entry ZIP-attribute
preservation (external_attr/create_system/
compress_type/date_time) across symlink and
ZIP_STOREDentries, atomicRECORDrewrite, and the CLI
generate/inject/validatemodes.TestWaitNoticeboardCaptureintest/test_noticeboard.py
— pins thewait(noticeboard=True)contract: returned dict is a
plain mutabledict, empty-runtime / empty-noticeboard fallbacks
to{}, single-flag back-compat (wait()staysNone,
wait(stats=True)stayslist), combined-flag
:class:WaitResultshape, last-write-wins, delete propagation
through a chained behavior, fresh-session isolation, and the
single-shot guarantee that an explicitstop()followed by
wait(noticeboard=True)preserves the snapshot rather than
re-snapshotting the now-empty noticeboard. The existing
scheduler-stats tests intest/test_scheduler_stats.pywere
simplified to use the cown-chain barrier directly rather than a
send/receivehandshake, now that the same change is
exercised end-to-end by the newwait(noticeboard=True)tests.
Internal
flake8now lints.pyistubs (the default--filename
glob silently skipped them). Pre-existing defects in
__init__.pyi,_core.pyi, andtest_boc.pycleaned up in
the same pass. The workflow also lints the newscripts/
directory.flake8-encodingsadded to the[linting]extra — pins the
Windows-locale class of bug above as a permanent regression gate.
Any futureopen()call without an explicitencoding=
(or withencoding=None) now fails the PR-gate lint job. The
plugin and its transitive dependencies (flake8-helper,
astatine,domdf-python-tools,natsort) are pinned and
hash-verified inci/constraints-linting.txtlike every other
CI dependency.- Defensive
receive()timeouts on every lifecycle path —
Behaviors.start_workers,stop_workers,_abort_workers,
and the noticeboard mutator loop now pass a bounded timeout to
every_core.receive()they own. A wedged worker therefore
fails fast with a deterministicRuntimeErrorinstead of
hanging the parent forever. Defence in depth against the
sub-interpreter wedge observed on macOS arm64 + Python 3.12/3.13. - No
unittest.mockin test files that schedule@when—
the transpiler exports the whole test module for import in every
worker sub-interpreter, so a top-levelfrom unittest import mocktriggers animport asyncioin every worker. On macOS
arm64 + Python 3.12/3.13 this can deadlock during PEP 684
per-interpreter init. Replaced by a small in-house
test/mockreplacement.py(patch_attrcontext manager +
Recorder/RecorderMethodstubs) imported lazily inside
the few tests that need it. The pitfall is documented in the
testing-with-bocskill.