Repository navigation
Semlith 0.46.0
The two security audits of 0.45.1 (2026-10-23) found nothing Critical or
High. This release closes all eighteen of their findings, or records why one
is accepted. Each fix comes with a test that fails on 0.45.1.
Security
- Only a commit on main can become a signed release (SEC-001). Before,
any pushedv*tag was built, signed with the project key, attested and
published as Latest, so anyone who could push a branch and a tag could ship
a release every installer accepted. The release workflow now refuses a tag
whose commitmaindoes not contain, before anything is built. Only an
organisation admin can create av*tag. The signing key lives in a
releaseenvironment that onlyv*tags can deploy to. - Each release job holds one permission (SEC-007).
signholds the key and no write token.attestholds the OIDC token.
releasewrites the release and runs the one third-party action.- Tag builds use
--lockedwith no build cache. - Checkouts drop their credentials.
- Latest moves only to the highest version released.
- The shared-library and glibc gates fail on empty
readelforobjdump
output instead of passing. - The packs workflow builds with a read-only token and publishes in a
separate job that runs no third-party code.
- The installers check who built a release, and refuse an old one
(SEC-007, SEC-015).install.shandinstall.ps1askghfor an
attestation signed byrelease.ymlfor the tag being installed. They refuse
a resolvedlatestolder than v0.46.0, and any tag that is not a whole
vMAJOR.MINOR.PATCH.install.shrefuses busybox wget.install.ps1
runs in one script block, soirm | iexleaves nothing in the caller's
session, and it restores the process-wide TLS setting afterwards. semlith addnever fetches through a proxy (SEC-003). ureq read
ALL_PROXY,HTTPS_PROXYandHTTP_PROXY. Through a proxy, the proxy
resolves the name, so the private-address guard checked only the proxy's
address. A failed fetch now names any proxy variable that is set.
198.18.0.0/15, 192.0.0.0/24, 192.88.99.0/24 and fec0::/10 are private
(SEC-009).semlith upgradeunpacks the bytes it checked (SEC-004, SEC-014). It
used to write the checked archive out and open it again, in a work
directory that might already exist. It now unpacks from memory into a
directory it has just created, owner-only. Both archive readers refuse an
entry with a colon (a Windows drive-relative path or alternate data stream)
or a Windows device name.- Packs and the embedding model are used as they were checked (SEC-004).
A pack is extracted from the bytes that matched its digest. A bare-version
stamp from before 0.41.0 no longer counts as installed. The embedding
model's files are checked against their pinned digests once per process,
as they are loaded. - On macOS, a native parser cannot grow past its worker's memory limit
(the second audit's Medium). Darwin refuses a lowerRLIMIT_AS, so the
limit used to cover only Rust's allocations, and the deadline bounded the
rest. The daemon now reads each worker's memory footprint every 25 ms and
ends one that passes its idle footprint plus the limit. The file is refused
with the peak and the cap, and the next file parses. A poll can overshoot by
what a parse allocates in 25 ms, about 13 MiB on an M1. A worker that has
already read other files gets the file once more on a fresh worker before
it is refused, because malloc keeps what earlier files freed. - Isolation headers (SEC-015). Every portal response sends
Permissions-Policy,Cross-Origin-Opener-Policy: same-originand
Cross-Origin-Resource-Policy: same-origin. - Every lockfile is audited (SEC-010, SEC-011, SEC-013, SEC-016).
cargo denychecks every feature's dependency graph (the Linux release
builds withdynamic-ort), and denies yanked crates and wildcard
requirements.- The fuzz harness's lockfile moves to rustls-webpki 0.103.16,
webpki-roots 1.0.11 and zip 9.0.5. - The Core ML pack installs a lock with every package pinned by hash, and
CI audits it beside both Cargo lockfiles. ttf-parserandpastestay unmaintained with no replacement upstream,
so their ignores carry today's reasons. ttf-parser reads only fonts
semlith picks for its own PDF reports. lopdf calls it only to write a PDF,
never when an indexed PDF is read. The font readers that do see indexed
PDFs run in the parse worker.tests/parse_pdf.rsindexes a PDF whose
embedded Type 1 font overflows its reader's stack, and one whose CFF font
panics. The first is refused as a crash, the second as having no text, and
the daemon stays up.
- A secret scan gates every push (SEC-002). gitleaks runs over the tree,
with a rule for crates.io tokens. Publishing has gone through Trusted
Publishing since 0.41.0, and the stale local token is gone.
Changed
- CI decides more carefully what is prose (SEC-005, SEC-006).
changes.shused to treat the agent rules, skill and clients guide that
semlith setupwrites into users' clients as prose, because they live
underdocs/. It now treats them as code. A rename counts as both of its
paths. A previous head is trusted only if its run, on the same branch, ran
the heavy lanes green. Self-tests cover it and the new tag check. - The bug hunt in
native-smoke.ymlreads its own budget-capped
ANTHROPIC_BUGHUNT_KEYand skips without it. Its tools are installed at
pinned versions.
Accepted
- A process running as you that writes the model cache while the daemon runs
could change an unpacked pack's files, or the model graph ONNX Runtime opens
by path, after the check. Such a process can already replace the binary.
docs/security.mdsays so (SEC-004). - Two expired confidential-VM attestation tokens remain in the history behind
the v0.37.0-rc.5 and rc.6 tags, which the tag ruleset keeps immutable. They
grant nothing (SEC-012).
Install
macOS and Linux
curl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/semlith/semlith/main/install.sh | shWindows
irm https://raw.githubusercontent.com/semlith/semlith/main/install.ps1 | iexOr download the archive for your platform below, unpack it, and put
semlith on your PATH. Intel macOS has no prebuilt binary: ONNX
Runtime no longer publishes x86_64 macOS builds.
Verify a download against SHA256SUMS before running it, SHA256SUMS
against its signature (release key
SHA256:a5oJmrakIwDHEWTDWqWe51Ttt6m2tQ0dsGzIqyxDuxs) with
echo "semlith-release ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMTHiHMzeSHzzc+8UDZHUzJcvb6XGUBb+TJdFes91xm" > allowed_signers
and ssh-keygen -Y verify -f allowed_signers -I semlith-release -n file -s SHA256SUMS.sig < SHA256SUMS,
and its provenance with gh attestation verify <file> --repo semlith/semlith --signer-workflow semlith/semlith/.github/workflows/release.yml.