Elixir and Erlang/OTP versions
Erlang/OTP 28 [erts-16.4] [64-bit] [smp] [jit]
Elixir 1.19.5 (compiled with Erlang/OTP 28)
The generated boot script shape is the same on current main. The
underlying OTP race is erlang/otp#11353 (fix proposed in
erlang/otp#11354) and affects at least OTP 27 and 28.
Operating system
Linux (Debian 13), x86_64 and arm64.
Current behavior
Boot scripts generated by mix release (start.script,
start_clean.script) contain a single primLoad with only the
~29-module bootstrap set (error_handler, code, code_server,
logger*, proc_lib, ...). Everything else — including stdlib
modules the logger needs while handling early kernel-start work
(maps, sys, queue, io_lib) — is lazy-loaded during boot. The
stock start_clean.script shipped inside an OTP install instead
preloads all of kernel+stdlib before any kernel process starts.
That difference exposes every mix-release boot to erlang/otp#11353:
code_server:init registers its name before publishing its mode in
persistent_term, and error_handler switches to
code:ensure_loaded/1 (which reads that term) as soon as the name
resolves — so a lazy load landing in that init window dies with a
persistent_term:get(code_server) badarg. When the loading process is
the logger kernel process, the boot dies:
Kernel pid terminated (logger) ({badarg,[{persistent_term,get,[code_server],...},
{code_server,get_mode,0,...},{code,ensure_loaded,1,...},
{error_handler,undefined_function,3,...},{logger_backend,call_handlers,3,...},
{proc_lib,exit_p,3,...}]})
In production this hit us through bin/<release> eval — the standard
migration/maintenance path — failing fleet-wide on a host while the
running application stayed healthy. A deterministic trigger is any
early-boot logger event; e.g. running the release from a directory the
release user cannot read makes erl_prim_loader emit file_error
reports for the "." code-path entry on every lazy load (an easy state
to be in: cd /root && runuser -u app -- bin/app eval ...). With that
trigger the crash reproduces 5/5; with a readable cwd the same boot
passes 5/5. Full analysis, an Elixir-free reproduction script, and a
crash-dump anatomy are in erlang/otp#11353.
Expected behavior
Release boots should not be able to lazy-load, during the kernel-start
window, modules that OTP's own boot scripts always preload. Two
options, either of which closes the exposure regardless of when/whether
the OTP fix ships:
-
Generate primLoad phases for kernel+stdlib modules the way the
stock start_clean.script does (load-before-kernel-start), or
-
Minimally, extend the bootstrap primLoad with the closure the
early logger path can touch. The set we deployed and verified
(crash 5/5 → pass, plus 3/3 on the originally affected host):
maps sys queue binary gb_sets gb_trees beam_lib os io io_lib
io_lib_format io_lib_pretty unicode string calendar proplists
gen_statem logger_olp logger_proxy logger_h_common logger_std_h
logger_formatter erl_features
Workaround we ship today, as a release step, in case it is useful to
anyone landing here (patches both generated scripts and regenerates the
.boot files):
# mix.exs — releases: [myapp: [steps: [:assemble, &preload_boot_modules/1, :tar]]]
def preload_boot_modules(%Mix.Release{version_path: vsn_path} = release) do
for name <- ["start", "start_clean"],
script = Path.join(vsn_path, name <> ".script"),
File.exists?(script) do
{:ok, [{:script, info, instrs}]} = :file.consult(script)
{:done, patched} =
Enum.reduce(instrs, {:pending, []}, fn
{:primLoad, mods}, {:pending, acc} ->
{:done, [{:primLoad, Enum.sort(Enum.uniq(mods ++ @boot_preload_modules))} | acc]}
instr, {state, acc} ->
{state, [instr | acc]}
end)
File.write!(script, :io_lib.format(~c"%% coding: utf-8~n~tp.~n", [
{:script, info, Enum.reverse(patched)}
]))
:ok = :systools.script2boot(script |> Path.rootname() |> String.to_charlist())
end
release
end
Elixir and Erlang/OTP versions
The generated boot script shape is the same on current main. The
underlying OTP race is erlang/otp#11353 (fix proposed in
erlang/otp#11354) and affects at least OTP 27 and 28.
Operating system
Linux (Debian 13), x86_64 and arm64.
Current behavior
Boot scripts generated by
mix release(start.script,start_clean.script) contain a singleprimLoadwith only the~29-module bootstrap set (
error_handler,code,code_server,logger*,proc_lib, ...). Everything else — including stdlibmodules the logger needs while handling early kernel-start work
(
maps,sys,queue,io_lib) — is lazy-loaded during boot. Thestock
start_clean.scriptshipped inside an OTP install insteadpreloads all of kernel+stdlib before any kernel process starts.
That difference exposes every mix-release boot to erlang/otp#11353:
code_server:initregisters its name before publishing its mode inpersistent_term, anderror_handlerswitches tocode:ensure_loaded/1(which reads that term) as soon as the nameresolves — so a lazy load landing in that init window dies with a
persistent_term:get(code_server)badarg. When the loading process isthe logger kernel process, the boot dies:
In production this hit us through
bin/<release> eval— the standardmigration/maintenance path — failing fleet-wide on a host while the
running application stayed healthy. A deterministic trigger is any
early-boot logger event; e.g. running the release from a directory the
release user cannot read makes
erl_prim_loaderemitfile_errorreports for the
"."code-path entry on every lazy load (an easy stateto be in:
cd /root && runuser -u app -- bin/app eval ...). With thattrigger the crash reproduces 5/5; with a readable cwd the same boot
passes 5/5. Full analysis, an Elixir-free reproduction script, and a
crash-dump anatomy are in erlang/otp#11353.
Expected behavior
Release boots should not be able to lazy-load, during the kernel-start
window, modules that OTP's own boot scripts always preload. Two
options, either of which closes the exposure regardless of when/whether
the OTP fix ships:
Generate
primLoadphases for kernel+stdlib modules the way thestock
start_clean.scriptdoes (load-before-kernel-start), orMinimally, extend the bootstrap
primLoadwith the closure theearly logger path can touch. The set we deployed and verified
(crash 5/5 → pass, plus 3/3 on the originally affected host):
Workaround we ship today, as a release step, in case it is useful to
anyone landing here (patches both generated scripts and regenerates the
.bootfiles):