Skip to content

Commit 121b68c

Browse files
committed
Version 0.6.0
1 parent 3e396b1 commit 121b68c

3 files changed

Lines changed: 98 additions & 95 deletions

File tree

CHANGELOG.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,103 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
77

88
<!-- %% CHANGELOG_ENTRIES %% -->
99

10+
## 0.6.0 - 2026-05-31
11+
12+
This release is a security-hardening pass over the native (NIF) boundary
13+
and the build/release pipeline: direct `Emily.Native` calls now validate
14+
their arguments instead of trusting Elixir-side normalization,
15+
precompiled-NIF downloads verify against a checksum pinned in the hex
16+
package (a trust root independent of the GitHub release), and the
17+
per-stream worker is bounded and tears down without blocking a BEAM
18+
scheduler. It is backward compatible, but two behaviour changes matter
19+
for high-concurrency callers: the per-worker async queue is now bounded
20+
(`worker_queue_limit`, default 8192) and rejects when full, and a stopped
21+
or dropped worker replies `{:error, :stopped}` to queued callers instead
22+
of running their work.
23+
24+
### Added
25+
26+
- `Emily.Stream.close/1` stops a stream's worker thread deterministically
27+
instead of waiting for garbage collection: queued operations are
28+
cancelled (their callers get a `RuntimeError`), the in-flight op
29+
finishes, and the OS thread is joined off the BEAM schedulers.
30+
- `config :emily, worker_queue_limit: N` (default `8192`) bounds the
31+
per-worker async queue, and `config :emily, await_timeout: ms` (default
32+
`:infinity`) sets an optional timeout for awaiting native results.
33+
34+
### Security
35+
36+
- Worker-thread teardown no longer blocks a BEAM scheduler. The resource
37+
destructor previously drained the worker's entire queue and joined the
38+
OS thread inline, so collecting a busy stream during GC could stall a
39+
scheduler. Workers are now joined off-scheduler by a dedicated reaper
40+
(itself joined at NIF unload), and on stop the worker cancels its
41+
queued tasks — replying `{:error, :stopped}` — instead of running them.
42+
43+
- The async NIF worker queue is now bounded (`worker_queue_limit`, reject
44+
when full) so a flood of operations can't grow it without limit and pin
45+
host/GPU memory, and a stopped or dropped worker now replies
46+
`{:error, :stopped}` to every queued caller instead of leaving it
47+
blocked forever. `Emily.Native.worker_queue_depth/1` exposes the depth
48+
for observability.
49+
50+
- The dev/CI source-build path now refuses to trust an MLX install
51+
directory it doesn't own and keeps the build cache `0700`, so a shared
52+
or attacker-controlled `EMILY_CACHE` can't plant a `libmlx.a` that is
53+
then statically linked into the NIF. Fixed system tools (`getconf`,
54+
`id`, `sw_vers`, plus `xcrun`/`sysctl`/`ps` in `build-mlx.sh`) resolve
55+
from absolute/system paths rather than `$PATH`, and the MLX-build lock
56+
records the holder's process start time so a recycled PID can't be
57+
mistaken for the original holder. Build-time only; no runtime change.
58+
59+
- Precompiled NIF downloads are now verified against checksums pinned
60+
inside the hex package (`native_checksums.txt`) rather than a `.sha256`
61+
sidecar fetched from the same GitHub release as the tarball. Because
62+
the package contents are covered by Hex's package hash in the
63+
consumer's `mix.lock`, the trust root no longer lives in the mutable
64+
release. The tarball is also extracted with `:erl_tar` against a strict
65+
entry allowlist (`libemily.{so,dylib}` + `mlx.metallib`), rejecting
66+
symlinks, hardlinks, `..` traversal, absolute paths, and unexpected
67+
entries — closing a path-traversal/arbitrary-write vector in the old
68+
`tar -xzf` extraction. New `mix emily.checksums` task regenerates the
69+
pinned file per release.
70+
71+
- Integer arguments crossing the NIF boundary are now range-checked
72+
before being narrowed from Elixir's `int64` to C++ `int`. Previously an
73+
out-of-range axis, count, or shape entry wrapped silently (e.g. an axis
74+
of `2^32 + 3` became `3`), dispatching the wrong MLX operation; and
75+
unbounded sample counts in `random_split`/`random_categorical` could
76+
drive huge allocations. Out-of-range values, and negative counts, now
77+
raise `ArgumentError`. Centralized as `checked_int` / `require_count`
78+
helpers applied across the reduce, shape, sort, random, index, linalg,
79+
conv, and fast NIFs.
80+
81+
- Native indexing and window NIFs now validate their vector arguments
82+
against the tensor rank before indexing, and reject non-positive
83+
strides, dilations, and window dimensions. Previously a direct
84+
`Emily.Native` call with a malformed `slice_update` start, a short
85+
pad/window vector, or a zero window stride could read a C++ vector out
86+
of bounds or trigger an integer divide-by-zero (SIGFPE) — both of which
87+
crash the whole BEAM VM rather than raising in the caller. They now
88+
raise `ArgumentError`.
89+
90+
- `Emily.Native.from_binary/3` now validates tensor shapes at the NIF
91+
boundary. Dimensions above `INT32_MAX` are rejected (previously they
92+
silently truncated through MLX's `int32` `ShapeElem`), and the element
93+
and byte counts are computed with overflow checking. Without this an
94+
attacker-chosen shape whose element product wrapped (e.g.
95+
`[2^21, 2^21, 2^22]``0`) could pass the binary-size check against an
96+
undersized — even empty — binary and build an array whose shape outran
97+
its allocation, an out-of-bounds read on the next `eval`/`to_binary`.
98+
99+
- `Emily.Native.conv_general/8` now rejects a non-positive `groups`
100+
argument with `ArgumentError` instead of crashing the BEAM VM. MLX's
101+
convolution checks compute `in_channels % groups`, so `groups <= 0`
102+
(or a large value that narrows to zero through the `int64 → int`
103+
conversion) was an integer modulo-by-zero — a SIGFPE that bypassed the
104+
NIF's exception path and terminated the entire node. The guard
105+
validates the un-narrowed value at the NIF boundary.
106+
10107
## 0.5.1 - 2026-05-23
11108

12109
### Fixed

RELEASE.md

Lines changed: 0 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +0,0 @@
1-
This release is a security-hardening pass over the native (NIF) boundary
2-
and the build/release pipeline: direct `Emily.Native` calls now validate
3-
their arguments instead of trusting Elixir-side normalization,
4-
precompiled-NIF downloads verify against a checksum pinned in the hex
5-
package (a trust root independent of the GitHub release), and the
6-
per-stream worker is bounded and tears down without blocking a BEAM
7-
scheduler. It is backward compatible, but two behaviour changes matter
8-
for high-concurrency callers: the per-worker async queue is now bounded
9-
(`worker_queue_limit`, default 8192) and rejects when full, and a stopped
10-
or dropped worker replies `{:error, :stopped}` to queued callers instead
11-
of running their work.
12-
13-
### Added
14-
15-
- `Emily.Stream.close/1` stops a stream's worker thread deterministically
16-
instead of waiting for garbage collection: queued operations are
17-
cancelled (their callers get a `RuntimeError`), the in-flight op
18-
finishes, and the OS thread is joined off the BEAM schedulers.
19-
- `config :emily, worker_queue_limit: N` (default `8192`) bounds the
20-
per-worker async queue, and `config :emily, await_timeout: ms` (default
21-
`:infinity`) sets an optional timeout for awaiting native results.
22-
23-
### Security
24-
25-
- Worker-thread teardown no longer blocks a BEAM scheduler. The resource
26-
destructor previously drained the worker's entire queue and joined the
27-
OS thread inline, so collecting a busy stream during GC could stall a
28-
scheduler. Workers are now joined off-scheduler by a dedicated reaper
29-
(itself joined at NIF unload), and on stop the worker cancels its
30-
queued tasks — replying `{:error, :stopped}` — instead of running them.
31-
32-
- The async NIF worker queue is now bounded (`worker_queue_limit`, reject
33-
when full) so a flood of operations can't grow it without limit and pin
34-
host/GPU memory, and a stopped or dropped worker now replies
35-
`{:error, :stopped}` to every queued caller instead of leaving it
36-
blocked forever. `Emily.Native.worker_queue_depth/1` exposes the depth
37-
for observability.
38-
39-
- The dev/CI source-build path now refuses to trust an MLX install
40-
directory it doesn't own and keeps the build cache `0700`, so a shared
41-
or attacker-controlled `EMILY_CACHE` can't plant a `libmlx.a` that is
42-
then statically linked into the NIF. Fixed system tools (`getconf`,
43-
`id`, `sw_vers`, plus `xcrun`/`sysctl`/`ps` in `build-mlx.sh`) resolve
44-
from absolute/system paths rather than `$PATH`, and the MLX-build lock
45-
records the holder's process start time so a recycled PID can't be
46-
mistaken for the original holder. Build-time only; no runtime change.
47-
48-
- Precompiled NIF downloads are now verified against checksums pinned
49-
inside the hex package (`native_checksums.txt`) rather than a `.sha256`
50-
sidecar fetched from the same GitHub release as the tarball. Because
51-
the package contents are covered by Hex's package hash in the
52-
consumer's `mix.lock`, the trust root no longer lives in the mutable
53-
release. The tarball is also extracted with `:erl_tar` against a strict
54-
entry allowlist (`libemily.{so,dylib}` + `mlx.metallib`), rejecting
55-
symlinks, hardlinks, `..` traversal, absolute paths, and unexpected
56-
entries — closing a path-traversal/arbitrary-write vector in the old
57-
`tar -xzf` extraction. New `mix emily.checksums` task regenerates the
58-
pinned file per release.
59-
60-
- Integer arguments crossing the NIF boundary are now range-checked
61-
before being narrowed from Elixir's `int64` to C++ `int`. Previously an
62-
out-of-range axis, count, or shape entry wrapped silently (e.g. an axis
63-
of `2^32 + 3` became `3`), dispatching the wrong MLX operation; and
64-
unbounded sample counts in `random_split`/`random_categorical` could
65-
drive huge allocations. Out-of-range values, and negative counts, now
66-
raise `ArgumentError`. Centralized as `checked_int` / `require_count`
67-
helpers applied across the reduce, shape, sort, random, index, linalg,
68-
conv, and fast NIFs.
69-
70-
- Native indexing and window NIFs now validate their vector arguments
71-
against the tensor rank before indexing, and reject non-positive
72-
strides, dilations, and window dimensions. Previously a direct
73-
`Emily.Native` call with a malformed `slice_update` start, a short
74-
pad/window vector, or a zero window stride could read a C++ vector out
75-
of bounds or trigger an integer divide-by-zero (SIGFPE) — both of which
76-
crash the whole BEAM VM rather than raising in the caller. They now
77-
raise `ArgumentError`.
78-
79-
- `Emily.Native.from_binary/3` now validates tensor shapes at the NIF
80-
boundary. Dimensions above `INT32_MAX` are rejected (previously they
81-
silently truncated through MLX's `int32` `ShapeElem`), and the element
82-
and byte counts are computed with overflow checking. Without this an
83-
attacker-chosen shape whose element product wrapped (e.g.
84-
`[2^21, 2^21, 2^22]``0`) could pass the binary-size check against an
85-
undersized — even empty — binary and build an array whose shape outran
86-
its allocation, an out-of-bounds read on the next `eval`/`to_binary`.
87-
88-
- `Emily.Native.conv_general/8` now rejects a non-positive `groups`
89-
argument with `ArgumentError` instead of crashing the BEAM VM. MLX's
90-
convolution checks compute `in_channels % groups`, so `groups <= 0`
91-
(or a large value that narrows to zero through the `int64 → int`
92-
conversion) was an integer modulo-by-zero — a SIGFPE that bypassed the
93-
NIF's exception path and terminated the entire node. The guard
94-
validates the un-narrowed value at the NIF boundary.

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule Emily.MixProject do
22
use Mix.Project
33

44
@app :emily
5-
@version "0.5.1"
5+
@version "0.6.0"
66
@source_url "https://github.com/ausimian/emily"
77

88
# MLX pin. Drives the git tag the `:mlx_src` dep is cloned at (see

0 commit comments

Comments
 (0)