The class-name check on every nippy thaw the engine runs over a file.
A frozen nippy value can name a class in three of its type ids, and reading one
resolves that name and builds an instance of it: a record frame (Class/forName, then
the static create), a deftype frame (Class/forName, then the first public
constructor over the fields that follow), and a Serializable frame (an
ObjectInputStream over the bytes that follow). nippy 3.8.1 gates the third behind
*thaw-serializable-allowlist* and the first two behind nothing at all.
Every file the engine reads is untrusted input — a store directory or a dump arrives from wherever an operator copied it — so all three are gated here, and the gate is the tightest one there is:
A vaelii frame never carries a class name.
That is already the export format's stated rule (vaelii.impl.io.export), and the
disk codec writes a record's fields positionally for a size reason
(vaelii.impl.disk.codec), so nothing the engine writes states a class either. A
frame that names one therefore came from somewhere else, and allowed-classes is
empty: the name is refused (:disallowed-class) before the class is resolved.
The Serializable allowlist is pinned rather than inherited. nippy's default is
a safe set, but it lives in a dynamic var an embedding application is invited to
widen — nippy documents allow-and-record-any-serializable-class-unsafe for exactly
that migration — and a host that widened it would widen the engine's file readers with
it. Binding it per read makes the entry point this namespace's rather than the host's.
How the first two are gated, since nippy exposes no hook for them: the readers
taoensso.nippy.io dispatches to are vars, and this namespace installs a checked
reader in place of each. A refusal is thrown from there, which is before nippy's own
try — so it travels rather than becoming the {:nippy/unthawable …} placeholder a
failed resolution otherwise reads as, and no class is loaded on the way. The
replacement calls straight through to the reader it replaced unless with-guard is in
force, so a host application thawing its own records in this JVM is unaffected.
What holds that wrap to the release it was written against is
pinned-nippy-version: three internals are reached into here, and a bump that keeps
their names while routing deserialization around them would narrow this entry point without
reddening anything. So the version is checked at load and the namespace refuses to
come up against another one.
The class-name check on every nippy thaw the engine runs over a file.
A frozen nippy value can **name a class** in three of its type ids, and reading one
resolves that name and builds an instance of it: a record frame (`Class/forName`, then
the static `create`), a deftype frame (`Class/forName`, then the first public
constructor over the fields that follow), and a `Serializable` frame (an
`ObjectInputStream` over the bytes that follow). nippy 3.8.1 gates the third behind
`*thaw-serializable-allowlist*` and the first two behind nothing at all.
Every file the engine reads is **untrusted input** — a store directory or a dump
arrives from wherever an operator copied it — so all three are gated here, and the
gate is the tightest one there is:
> **A vaelii frame never carries a class name.**
That is already the export format's stated rule (`vaelii.impl.io.export`), and the
disk codec writes a record's fields **positionally** for a size reason
(`vaelii.impl.disk.codec`), so nothing the engine writes states a class either. A
frame that names one therefore came from somewhere else, and `allowed-classes` is
empty: the name is refused (`:disallowed-class`) before the class is resolved.
**The `Serializable` allowlist is pinned rather than inherited.** nippy's default is
a safe set, but it lives in a dynamic var an embedding application is invited to
widen — nippy documents `allow-and-record-any-serializable-class-unsafe` for exactly
that migration — and a host that widened it would widen the engine's file readers with
it. Binding it per read makes the entry point this namespace's rather than the host's.
**How the first two are gated**, since nippy exposes no hook for them: the readers
`taoensso.nippy.io` dispatches to are vars, and this namespace installs a checked
reader in place of each. A refusal is thrown from there, which is *before* nippy's own
`try` — so it travels rather than becoming the `{:nippy/unthawable …}` placeholder a
failed resolution otherwise reads as, and no class is loaded on the way. The
replacement calls straight through to the reader it replaced unless `with-guard` is in
force, so a host application thawing its own records in this JVM is unaffected.
**What holds that wrap to the release it was written against** is
`pinned-nippy-version`: three internals are reached into here, and a bump that keeps
their names while routing deserialization around them would narrow this entry point without
reddening anything. So the version is checked at load and the namespace refuses to
come up against another one.Is a thaw inside with-guard? The checked readers below stand in for nippy's own
process-wide, so this is what keeps the gate the engine's own: false — a host
application's thaw — calls straight through.
Is a thaw inside `with-guard`? The checked readers below stand in for nippy's own process-wide, so this is what keeps the gate the engine's own: false — a host application's thaw — calls straight through.
The class names a file the engine wrote may state — none.
Measured rather than asserted: a store built on each of the four durable backends and
a dump written in each variant and codec drive nippy's three class-name readers zero
times. A dump frame is a field map, a log frame is a positional vector, and every
leaf a sentence may carry is a type nippy has an id for (vaelii.impl.checks'
check-encodable refuses the rest at the public entry point). So the allowlist is empty, and
a name is a name this engine did not write.
A set rather than false because the refusal names the class either way, and because
a format that one day carried one would add it here and nowhere else.
The class names a file the engine wrote may state — **none**. Measured rather than asserted: a store built on each of the four durable backends and a dump written in each variant and codec drive nippy's three class-name readers zero times. A dump frame is a field map, a log frame is a positional vector, and every leaf a sentence may carry is a type nippy has an id for (`vaelii.impl.checks`' `check-encodable` refuses the rest at the public entry point). So the allowlist is empty, and a name is a name this engine did not write. A set rather than `false` because the refusal names the class either way, and because a format that one day carried one would add it here and nowhere else.
(check-nippy-pin! pinned)Refuse to go on unless the nippy on the classpath is pinned; the version agreed on,
otherwise a throw, which fails this namespace and with it the build.
This throws for two distinct reasons. The descriptor is unreadable, so nothing is
known about which nippy the wrap below is wrapping (:nippy-version-unreadable); or it
is readable and names another release (:nippy-version-moved). Both refuse,
because a class-name check that cannot say which nippy release it is guarding checks
nothing, and both messages say what the reader has to do next.
Refuse to go on unless the nippy on the classpath is `pinned`; the version agreed on, otherwise a throw, which fails this namespace and with it the build. This throws for two distinct reasons. The descriptor is **unreadable**, so nothing is known about which nippy the wrap below is wrapping (`:nippy-version-unreadable`); or it is readable and names **another release** (`:nippy-version-moved`). Both refuse, because a class-name check that cannot say which nippy release it is guarding checks nothing, and both messages say what the reader has to do next.
(guarded f)Run f — a thunk that thaws — behind the entry point, raising a :disallowed-class refusal
as itself rather than as whatever wrapped it.
The entry point for a caller reading many frames off one stream
(vaelii.impl.io.frames): the entry point is opened once for the run instead of once per
frame, which is one binding frame for ten thousand of them.
Run `f` — a thunk that thaws — behind the entry point, raising a `:disallowed-class` refusal as itself rather than as whatever wrapped it. The entry point for a caller reading **many** frames off one stream (`vaelii.impl.io.frames`): the entry point is opened once for the run instead of once per frame, which is one binding frame for ten thousand of them.
The nippy release this entry point was written against, and the whole of what keeps it from narrowing in silence.
The gate reaches into three of nippy's internals: the vars
taoensso.nippy.io/read-record and taoensso.nippy.io/read-deftype, wrapped in place
below, and nippy's own call to taoensso.nippy.impl/serializable-allowed?, which is
what lets a Serializable frame be refused by name rather than arrive as a placeholder.
A bump that renames or moves any of them fails this namespace at load, and that is
visible enough. The failure this pin exists for is the quiet one: a bump that keeps the
vars and stops routing a class name through them — a new reader path, an inlined call —
wraps cleanly, evaluates green, and covers less than it did. On a deserialization
trust boundary "the guard silently narrowed" is the worst available outcome, so the
version is asserted rather than assumed.
Moving this string is the ceremony, and the ceremony is the point. Whoever bumps nippy re-reads those three attachment points against the new release, confirms that every path resolving a class name still passes through them, and only then writes the new version here. One line per upgrade buys a guard that cannot drift without a reader.
The nippy release this entry point was written against, and the whole of what keeps it from narrowing in silence. The gate reaches into three of nippy's **internals**: the vars `taoensso.nippy.io/read-record` and `taoensso.nippy.io/read-deftype`, wrapped in place below, and nippy's own call to `taoensso.nippy.impl/serializable-allowed?`, which is what lets a `Serializable` frame be refused by name rather than arrive as a placeholder. A bump that **renames or moves** any of them fails this namespace at load, and that is visible enough. The failure this pin exists for is the quiet one: a bump that keeps the vars and stops routing a class name through them — a new reader path, an inlined call — wraps cleanly, evaluates green, and covers less than it did. On a **deserialization trust boundary** "the guard silently narrowed" is the worst available outcome, so the version is asserted rather than assumed. **Moving this string is the ceremony, and the ceremony is the point.** Whoever bumps nippy re-reads those three attachment points against the new release, confirms that every path resolving a class name still passes through them, and only then writes the new version here. One line per upgrade buys a guard that cannot drift without a reader.
(resolved-nippy-version)The nippy release actually on the classpath, or nil when it cannot be read.
Off the artifact's own Maven descriptor, because nippy states its version nowhere a
caller can read it — it asserts encore's (enc/assert-min-encore-version) and exposes
none of its own. Every jar Maven builds carries
META-INF/maven/<group>/<artifact>/pom.properties, and leiningen's uberjar keeps it
(only signature files are excluded), so this reads the same string from a checkout and
from the standalone jar.
The nippy release actually on the classpath, or nil when it cannot be read. Off the artifact's own Maven descriptor, because nippy states its version nowhere a caller can read it — it asserts *encore*'s (`enc/assert-min-encore-version`) and exposes none of its own. Every jar Maven builds carries `META-INF/maven/<group>/<artifact>/pom.properties`, and leiningen's uberjar keeps it (only signature files are excluded), so this reads the same string from a checkout and from the standalone jar.
(thaw bs)nippy/thaw of bs behind the entry point.
`nippy/thaw` of `bs` behind the entry point.
(thaw-from-in! in)nippy/thaw-from-in! from in behind the entry point.
`nippy/thaw-from-in!` from `in` behind the entry point.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |