Updating Fil-C sources and ports
Filnix maintains two toolchain variants on main, plus an
independent application-patch extraction pin:
- Release:
lib/filc-upstream.jsonselects an exact official release commit frompizlonator/fil-c;lib/filc-hashes.jsonrecords its sparse source hashes. This isfilcc,packages.default,pkgsFilc, the ordinary apps/demos and the default NixOS modules. - Staging:
lib/filc-staging.jsonselects an experimental fork commit;lib/filc-staging-hashes.jsonrecords its hashes. This isfilcc-stagingandlegacyPackages.${system}.pkgsFilcStaging. Campaigns can calllib.${system}.mkPkgsFilc { staging = true; ... }without a Filnix branch. It is opt-in, may be unsound, and is not a supported release. ports/upstream.json:portsRevselects the Git history and tree used to extract application patches. Nix builds consume the checked-in patches and package archives. The native Projeny package is built fromprojenyRevwith its separateprojenyHash, fetching onlyprojects/projeny/.scripts/update-ports-pin.pysets both revisions to the same commit;projenyRevmay run ahead alone when the importer needs a newer tool, as it does forURL:descriptors (Projeny downloads and verifies the release archive instead of reading one checked in beside the descriptor).
A ports update therefore does not change the compiler's derivation.
The glibc forks under upstream's projects/ directory belong
to the core pin: they participate in its ABI and
bootstrap. Directory location alone is not the boundary. New application
patches can still require a newer compiler/runtime feature; test each
updated port with the chosen toolchain before accepting it. Both
toolchains share sourcePatterns, port declarations and Nix
integration, but each bootstrap consistently uses its selected compiler,
runtime, libc, libc++, compiler-rt, yolounwind and SaRCAsm/minilute. Do
not mix their libraries or silently fall back from release to
staging.
Fil-C 0.687 release
The release pin is v0.687,
2db72c646daa,
published October 23, 2026. This is a complete toolchain/ABI migration:
function origins grow from 40 to 48 bytes on 64-bit platforms, and each
static setjmp site reuses a rooted jump buffer through
filc_jmp_buf_recreate. Rebuild all Fil-C objects, DSOs and
plugins together; never substitute a 0.687 development compiler into a
0.686 runtime closure (including through
FILC_DEV_LLVM).
The tag also includes the small-alloca non-exiting allocation path,
safepoint analysis corrections, memmove root/barrier fixes, fiber stack
ownership fixes, allocator sweep/decommit ordering, earlier UB
containment, and our SaRCAsm .rept and call-argument
carrier fixes (#336 and #337). The release uses upstream sources without
adding local compiler/runtime semantic patches. The adoption review
found no concrete new memory-safety defect, but package builds and
focused regressions are not a proof of memory safety.
Peregrine adds portable executables through an explicit Cosmo driver
mode. Filnix retains its glibc toolchain: it neither enables
--filc-cosmo nor packages the dual-architecture
Cosmo/shadow compilation closure. Upstream's
package-build.sh distribution assembly is not our Nix build
pipeline. Staging and nanofil are independent and are not advanced by
this release pin.
See the 0.687 port import report for the descriptor refresh, active package changes, missing upstream archives and validation.
Historical 0.686/fork comparison
The following comparison records the previous release and fork
experiment; it is not a statement that the current staging pin is based
on 0.687. The previous release was v0.686,
163fae598eaf,
published October 4, 2026. It includes many of our contributions:
coroutine and limited musttail support, setjmp with
-fno-builtin, common linkage, union-record capability
handling, FP-state preservation/fenv, PI mutex and spinlock fixes,
mount/prctl compatibility and SaRCAsm improvements. It also fixes ARM64
ordering, GC roots and fork-related deadlocks.
Current staging (2026-11-01): branch staging-deluge
of mbrock/fil-c at 18b6663a2785.
It is upstream deluge at 7b9ca2bdbb21
(October 31) with the previous staging commits rebased on top, plus the
new ones described below. Deluge brings Filip's LLVM JIT and changes the
Fil-C ABI so that callees root their pointer arguments (f565a56df1f),
which lets musttail calls pop the caller's frame. Rebuild every Fil-C
object for this pin; nothing compiled for the previous staging pin links
with it.
Derived-lower sharing (below) conflicted with the ABI change in
FilPizlonator's liveness. Arguments are now frame-live, so a pointer
derived from an argument is rooted through the argument's slot, which
the callee's prologue fills. Oracle reviewed the rebase and found it
sound; it also found that the getter cache broke JIT modules that take a
mapped global's address (its miss path referenced a runtime symbol the
JIT shim does not resolve), so the cache is now off in JIT mode
(5050cddc78a7). sharedlowersargs in the fork and
fork-regressions.tests.derived-arg-roots here cover
pointers derived from arguments after a musttail call has popped the
caller's frame.
Oracle also raised an unconfirmed upstream question: a frame-popping musttail to a function defined in another module calls the target's getter after the caller's frame is gone and before the callee's prologue roots the arguments. If that getter can reach a safepoint (an ifunc's resolver, for example), the arguments are briefly unrooted. Not reproduced; to raise with Filip.
LLVM JIT
The JIT API (filc_llvm_jit.h) builds LLVM IR in a Fil-C
program and compiles it in-process; KillUB and FilPizlonator always run
on it, so JIT code is checked like compiled C. Programs opt in by
linking with -enable-filc-jit, which selects
filc_jit_crt.o. The runtime dlopens
libfilcllvmjit.so, a native shim linking filc0's LLVM,
built here by runtime/llvmjit.nix from filc0's
dev output. Upstream looks for it next to libpizlo; Nix
keeps them in separate store paths, so libpizlo is built with
FILC_LLVMJIT_SO_PATH (fork commit d7e274cc6073). Upstream's
shim resolved only some of the runtime symbols that FilPizlonator's
output calls, so any JIT function with a loop failed to link
(filc_pollcheck_slow); af0c07f5ac35 adds the pollcheck, the
store barrier's marking state, pointer CAS and the thread-local
allocators, found by comparing libpizlo's exports with what compiled
Fil-C libraries import. The header that libpas includes is generated
from LLVM's C API headers, which the sparse llvmjit-src
source provides. checks.staging-llvm-jit builds two small
JIT programs: one calls back into the host, the other runs a loop, a
mapped host global, pointer stores and a pointer CAS across fresh
compiles while the GC collects. A program linked without
-enable-filc-jit is refused.
Four context-free wrappers (LLVMLookupIntrinsicID,
LLVMIntrinsicIsOverloaded,
LLVMIsMultithreaded, LLVMGetUndefMaskElem)
locked a NULL context and crashed, and
LLVMLookupIntrinsicID passed names without the
llvm. prefix to LLVM, which asserts on them. Both are fixed
on branch llvm-jit-contextless-locks,
which is based on deluge for an upstream pull request, and
cherry-picked onto staging. Release pins have no
llvmjit-src hash and build exactly as before.
FilLLVMJIT_EnableSharedObjectCache(bytes) (65acb385557b,
hardened in e40ff0ed2aaa and 18b6663a2785) lets processes forked after
the call share compiled code, so FPM workers stop compiling the same
traces each. The shim maps a shared anonymous region;
CompileModule keys each compile with BLAKE3 over the
verified module's bitcode, the target, the options and every global
mapping's pointer and lower bound, and on a hit links the object another
process produced instead of optimizing and generating code. A hit
therefore only gives a module the capabilities its own session mapped.
Entries are published lock-free (arena cursor, then a release CAS into a
slot), copied out before linking, and the cache refuses to start under
FILC_LLVM_OPTIONS. Oracle reviewed the design and the
implementation; the review caught a size underflow that let an oversized
object be written past the region, now rejected and covered by
tests/llvm-jit/shared-cache.c.
It also pointed out that Fil-C code can write its own memory through
/proc/self/mem, with no capability, and with the cache that
one escape would reach every worker. The runtime now refuses writable
opens whose file is a procfs mem (checked on the opened fd,
so symlinks, dirfds and /proc/self/fd reopens are caught),
but a second thread can still race the check before the fd is closed,
and code that can change the mount table can disguise the file. Closing
it properly needs the open done where no other thread sees the fd (a
helper thread with its own fd table) on every writable open, which is a
runtime change for all programs and is not done. So the cache stays
opt-in: nothing enables it by default, and PHP turns it on only with
PHP_JIT_LLVM_CACHE_SIZE.
The previous staging pin was branch filnix-staging
at af813053ecb5,
upstream's 0.687 versioning commit (a80e32804fd4) with our commits
rebased on top: the follow-ups described below, the union series
squashed into one commit, and a fix so
filc_jmp_buf_recreate sets the saved-signal-mask state on
every reuse. The pthread cancellation patches were dropped. The rest of
this section is the history behind it.
Staging preserved the reviewed comparison experiment, simplify-union-storage
at e4427dbc5b77. It replaces the older Filnix
fix-quickjs-union-abi pin, not the official release. Its
merged upstream base is e33a8e2c5376,
before the release's final commits. The experiment remains a draft comparison
PR, not a merge-ready claim of soundness.
The staging pin, 0b4ff52d6315
(branch staging-pin-next), adds these follow-ups on top of
that experiment: near-linear -O0 frame slot coloring
(92c381beeb23), a narrower union alignment rejection (2e92032bd923),
three FilPizlonator optimizations for PHP's interpreter (described
below), the libpas changes described under "PHP allocator and GC work",
and the glibc memcmp fix in the table below. The union
change: the experiment rejected every pointer-bearing union with
declared alignment below a word, which broke systemd's packed
HashmapBase storage union
(src/basic/hashmap.c). Release and deluge
compile it, and upstream's systemd 256.4 port leaves that file
unmodified. Staging now checks pointer offsets and strides relative to a
word-aligned start and relies on the runtime alignment check of each
pointer access, as release does. Global and TLS initializers store
pointer leaves without access checks, so a pointer at a non-word offset
in an initializer is now a compile error. In release it triggers a
compiler assertion (globals) or an unchecked capability write across two
aux words (TLS).
The three optimizations, each reviewed with Oracle for soundness:
- Frame alias scope (5dd226b7fbcb). A function's
Fil-C frame escapes into the thread's frame list, so every store
recording a pointer's lower made alias analysis assume it might alias
any load, and object header loads were not CSEd across them (PHP's ADD
handler loaded an upper bound 14 times). Loads and stores of the frame
now get their own alias scope that other loads and stores are
noaliaswith; calls stay untagged, so the GC and runtime still observe frames at every call. The tagging runs once, right after lowering;-filc-frame-alias-scope=0turns it off. PHP 8.5: 0.2–7.5% fewer instructions in micro-benchmarks. It applies cleanly todelugeand is meant for upstream. - Getter cache (10590066910c on
php-runtime-hotspots). A use of a global defined in another module, such as PHP'sEG(), called that module's getter every time. Permanent global objects are now cached in a module-private slot with acquire/release ordering; TLS and heap-backed results stay uncached. It assumes modules holding cached globals are notdlclosed. PHP 8.5: −2.4% instructions onZend/bench.php. - Derived-lower sharing (5a6a2131648c on
lower-root-fold). A pointer made from another by a GEP or cast, or a phi or select whose inputs all derive from one pointer, has that pointer's lower, so it no longer gets a frame slot of its own; its liveness keeps the base pointer's slot live. Before, a pointer walking an array stored the same lower into two frame slots on every iteration (PHP'szend_init_cvsloop: 17 to 13 instructions per CV). Roots dominate what they root and unreachable blocks are skipped;-filc-share-derived-lowers=0turns it off. php-bench (fixtures v2): 2.1–2.7% fewer instructions on DokuWiki, MediaWiki and WordPress, 3.4% onZend/bench.php.
Their regressions are framealiasscope,
framealiasthreads, framealiasnonlocal,
framealiasaddress, getter_cache_tls,
sharedlowers and sharedlowersunreachable in
the fork's filc/tests. The fork's single-file tests give
the same results with each optimization on and off (3936 tests for the
frame scope, 5308 for derived-lower sharing).
Tried and dropped: keeping address-taken locals on the stack when
their address only reaches nocapture call arguments (branch
copy-locals-for-calls, ba5e4ca03f85, off by default). Each
such call got a lazily allocated heap copy, copied in before the call
and back out after, and constant-size memsets into locals became stores.
Oracle found two blockers. The memset expansion left capabilities in
place that the runtime memset clears: on partial words, unaligned
destinations and under-aligned allocas, and a non-constant fill byte
could be laundered back into a capability. And LLVM's inferred
nocapture is false under Fil-C's zargs(),
which exposes the argument packet, so a callee could see the copy where
it expects the original. Both change behavior without breaking memory
safety. The unrestricted version's ceiling on WordPress was 9 to 8.3 GC
cycles per pass and 1.7% fewer instructions (MediaWiki 1.7%, DokuWiki
flat), too little to justify a Fil-C-aware nocapture
analysis or PHP source patches.
Also tried and not pinned: loop-invariant check loads (branch
loop-check-hoist, 01a7fe034c9a). In a loop whose only
returning calls are slow pollchecks, DeleteRedundantPollchecks loaded
the upper bound and the free/readonly flags that FilPizlonator marks as
check loads once in the preheader, when the object's lower was proven
nonnull there, and again after each slow pollcheck, where a signal
handler or another thread's free can take effect; checks still trapped
at the same access. It works (PHP's zend_init_cvs loop
keeps both in registers; regressions loopcheckhoist and
loopcheckhoistfree), but php-bench instructions did not
move: +0.1% on DokuWiki and MediaWiki, 0.0% on WordPress, +0.2% on
Zend/bench.php.
Residual work absent from v0.686, established by source comparison:
| Area | Staging change | Regression |
|---|---|---|
| Function descriptors | Local implementation alias prevents wrong-module calls through
dlsym/RTLD_LOCAL |
diagnostics.release-fork-known-gaps.tests.descriptors |
| Exception unwinding | Save nested cleanup and per-fiber unwind state; release pins now
apply it as
patches/fil-c/runtime-exception-unwind.patch |
fork-regressions.tests.nested-cleanup,
.fiber-unwind |
| Pointer CAS | Revalidate shadow state after the primary load, with an ARM64 load-load fence | diagnostics.release-fork-known-gaps.tests.cas-expected-cap |
| ARM64 glibc | Obtain saved jump-buffer frame identity through
zget_jmp_buf_frame |
staging cancellation C++ cleanup |
| Cancellation | Local libpizlo, ARM64 gate and glibc patches | staging-cancellation,
staging-cancellation-native (x86-only harness) |
| Unions/varargs | Complete aggregate-varargs transport, rejection of pointer words misaligned relative to a word-aligned start, misaligned-pointer initializer errors, null-padding initialization and optimizer shadow-state guards | fork's union/varargs/copy/memset suites (including
unionpackedstorage, unionpackedstoragefail,
initmisalignedptrfail), QuickJS and systemd |
| Small memmove/memset | Payload copies and fills of 16 bytes or more under the poll-check limit call libc instead of a byte loop with a compiler fence per byte; capability (aux) handling is unchanged | fork-regressions.tests.payload-memops |
| glibc memcmp | memcmp_bytes finds the first differing byte from
ctz(a ^ b) instead of taking its arguments' addresses,
which put eight heap allocations in every memcmp and
bcmp call (proposed upstream from mbrock/fil-c
branch glibc-memcmp-noalloc) |
diagnostics.release-fork-known-gaps.tests.memcmp-noalloc |
| Scavenger and GC | After each FUGC cycle, the scavenger does not take pages emptied
since that cycle began, until the next cycle replaces the hold or
FUGC_SCAVENGE_HOLD_MS passes (default 10000; 0 disables).
Pages are held for one cycle, not 10 s each |
diagnostics.release-fork-known-gaps.tests.scavenge-hold |
| SaRCAsm | Reject a stack-pointer carrier passed as a call argument; expand
.rept/.endr
(patches/sarcasm-*.patch, applied by
packages/sarcasm.nix; proposed upstream from
mbrock/fil-c branches sarcasm-callarg-carrier
and sarcasm-rept) |
staging-sarcasm-fixes |
Filip's release independently lowers pointer-bearing unions to pointer storage. The experiment now follows that general direction rather than its earlier ABI-only carriers, but retains different initializer/layout handling and SROA/InstCombine guards. Neither implementation should be mixed with a different variant's by-value aggregate ABI without validation.
Staging is not “0.686 plus fixes.” In particular, it
predates release commit 45adc761aeee,
which requires -yolo-assembler to recognize
zunsafe_call and related intrinsics. Preserving this
experimental snapshot does not give it the official release's tightened
unsafe-call boundary.
Declined: in-place realloc growth. Fil-C's
realloc() always allocates a new object, copies and frees
the old one, so programs that grow a buffer one append at a time take
quadratic time (PHP 8.5 works around this in
patches/php-8.5-realloc-filc.patch). A general fix is
drafted on mbrock/fil-c branch libpas-realloc-inplace,
not built or tested. Design, after Oracle review: a growing realloc
allocates the larger of the new size and 1.5× the old, zeroes the whole
slot capacity (data and aux) before the object is published, and flags
it FILC_OBJECT_FLAG_GROWABLE;
filc_object_aux_size() sizes every aux creation by slot
capacity for such objects; a later grow that fits only CASes
upper, refusing upper == lower; shrinks to at
least half keep upper, so it never decreases until free,
whose store to upper becomes atomic.
The first draft was unsound twice: growth could CAS
upper up from lower while free()
was running and resurrect a freed object, which compiled code would then
read (it omits the free check when both bounds checks are present); and
zeroing large slack registered the live object as an allocation root,
which the collector marks without scanning, so its aux and children
could be reclaimed. Lazily zeroing slack would also let weakly ordered
CPUs read a reused slot's old bytes.
It was declined because of its semantics: an object grown in place
keeps its identity, so pointers from before the realloc stay valid and
see the grown object (and a retained-upper shrink keeps the
tail reachable). That is memory-safe but gives up the use-after-realloc
detection every Fil-C realloc has today, and PHP, which motivated it, is
already fixed by its own patch. The branch stays as a design record. If
it is ever revisited, it needs: deterministic free/grow interleavings
(no growth from upper == lower); a GC test with a growable
object whose aux holds the only reference to a child and an atomic box,
grown across collection; coverage of eager, lazy and small-memmove aux
creation, grown to the last word of capacity; pointer stores into newly
exposed words during concurrent marking; dirty reused slots, checking
that exposed bytes and capabilities are zero; small/medium/large size
classes, exact capacity and one past it, retained and moving shrinks,
aligned-realloc fallback, free after growth, and stale-alias behavior
matching the chosen contract (thread).
PHP allocator and GC work. Profiling PHP 8.5 serving
WordPress, MediaWiki and DokuWiki found two runtime costs that staging
removes (thread).
First, FUGC collects about every 200 ms, but the scavenger returned
pages empty for 300 ms, so the pages a sweep freed went back to the OS
and were faulted back in, zeroed, during the next cycle: kernel page
faults, zeroing and madvise were 8–17% of cycles. FUGC now
caps the scavenger's age limit at the start of its last cycle, so a
sweep's pages are held only until the next cycle; the hold lapses after
FUGC_SCAVENGE_HOLD_MS (10 s) if no cycle follows. It is not
a 10 s hold per page. Pages that stay empty through a whole cycle, or
after the hold lapses because the program went idle, are still returned.
The hold is best effort, read without a lock, and forced scavenges
ignore it. Kernel cycles per pass fell from 6.7 to 2.8 G on WordPress
and from 4.5 to 2.0 G on MediaWiki; peak RSS was the same on WordPress
and 5–15% higher on MediaWiki. Second, filc_memmove and
filc_memset ran every copy under 10000 bytes through a byte
loop with a compiler fence per byte. WordPress makes 3.5 M such copies
(mean 61 B) and 1.2 M fills (mean 37 B) per pass over its requests. From
16 bytes on, the payload now goes through libc, cutting user
instructions by 4–5%. The byte loops never made payload words atomic, so
racing readers see nothing they could not see before. Memset already
skipped capability clearing for objects without aux, about 80% of the
bytes PHP fills.
memcmp allocations. Generic glibc
memcmp inlines memcmp_bytes, which took the
addresses of its two word arguments. Fil-C keeps address-taken locals on
the heap, so every memcmp and bcmp call made
eight 32-byte allocations before its first branch. PHP 8.5 serving
WordPress made 21 M of them per pass over its requests (678 MB, about a
third of all it allocated), mostly from zend_hash_find,
zend_is_identical and string functions, and they set how
often FUGC collected. With the fix, GC cycles per pass fell from 8 to 6
on WordPress and from 5.7 to 4.3 on MediaWiki; user instructions fell
3.7% and 4.0%, user cycles about 5% on both; DokuWiki was unchanged.
Oracle found the change exact for every caller (all pass differing
words), with no bounds or constant-time guarantee affected: generic
memcmp never was constant time.
Cancellation cost. The staging cancellation patches added signal-mask syscalls that release Fil-C does not make, from two sources:
zthread_cancel_setblocked the private cancellation signal, updated the state and unblocked it again on every call. glibc's__libc_cleanup_push_defer, which stdio's lock path reaches on every locked call, calls it to force deferred cancellation, so eachfprintfcost twort_sigprocmaskcalls: 20,000fprintfs to anopen_memstreammade 41,319, at 1.35 µs per call against 0.53 µs with__fsetlocking(FSETLOCKING_BYCALLER).zthread_cancel_setnow returns without touching the mask when cancellation is enabled and the call changes nothing (260 calls, 0.53 µs). Disabled and exiting calls keep the slow path:siglongjmpand signal-handler returns restore native masks without consulting cancellation state, and the redundant block repairs them (tests/cancellation-longjmp-mask.c, instaging-cancellation).filc_cancellable_syscallcallsrt_sigprocmask(SIG_UNBLOCK, SIGRTMIN)before every cancellation-point syscall (read,write,sendto,close, ...), even though the signal is almost always already unblocked. Not fixed. Proposed: a per-thread flag recording that the signal is known unblocked, cleared wherever it gets blocked (the cancellation handler'suc_sigmask,zthread_cancel_set, the usersigprocmaskpath, and the native mask restorations insiglongjmpand handler return).
PHP 8.5 serving DokuWiki made 3063 rt_sigprocmask calls
over 20 requests on staging, 1929 with the first fix, and 311 on
release: about 57 per request saved, and the remaining ~80 per request
come from the second source, together roughly 40–60 µs of a 13 ms
request (thread).
The release toolchain does not apply the local cancellation patches or the old x86 inotify implementation substitution. It retains the Nix locale-archive patch, installed locale, store paths and wrappers. Thus “release-backed” means official compiler/runtime semantics with Nix packaging, not unmodified build inputs. Selecting official also withdraws residual behavior previously provided on Filnix main; packages needing it must explicitly use staging.
Fork regressions are separate derivations so one official failure
does not hide the remaining results. Ordinary
checks.SYSTEM.fork-regressions runs the six currently
passing release cases: pointer atomics, union-record ABI, nested
cleanup, fiber unwinding, payload memops and derived-argument roots.
checks.SYSTEM.staging-fork-regressions retains all ten
cases.
The four known release gaps are separate, strict opt-in diagnostics
under lib.SYSTEM.diagnostics.release-fork-known-gaps:
cas-expected-cap, descriptors,
memcmp-noalloc and scavenge-hold. Their
assertions, O0/O2 runs and timeouts are unchanged; these diagnostics are
expected to fail with the current release, not report success for
missing behavior. The table above abbreviates their prefix as
diagnostics. A release that gains one of these fixes should
move that case back into the ordinary release suite. For example:
nix build --no-link --keep-going .#checks.x86_64-linux.fork-regressions
nix build --no-link .#checks.x86_64-linux.staging-fork-regressions
# Strict comparison: expected to fail on the current release.
nix build --no-link --keep-going .#lib.x86_64-linux.diagnostics.release-fork-known-gaps
nix build --no-link .#lib.x86_64-linux.diagnostics.release-fork-known-gaps.tests.descriptorsThe known ARM64 binary128 directed-rounding limitation remains
visible in fenv and staging-fenv; this is not
established to be a 0.686 regression. ARM64 validation is limited to the
native gates and representative ports below.
Verification of the split
Both complete x86-64 toolchains were rebuilt without
FILC_DEV_LLVM. All nine staging gates passed: cancellation,
native cancellation, the six-case fork aggregate, coroutines, fenv, GC
roots, link hygiene, wrapper roles and SaRCAsm. Release passed
coroutines, fenv, GC roots, link hygiene, wrapper roles, SaRCAsm, UTF-8
locale, PI mutexes and spinlocks. Its unsafe-call boundary check passed
at O0/O2, distinguishing ordinary external calls from explicitly
opted-in intrinsics. The release fork aggregate failed in four
independent cases: descriptors, nested cleanup, fiber unwinding and CAS
capability writeback. The pointer-atomic and union-record cases passed
in both variants.
The same 71 selected fork runtime cases were compiled and run at
O0/O2/O3 with each rebuilt toolchain. Staging passed
213/213; release passed 205/213. The
differences were byvalvaarg7 (overaligned aggregate
varargs, all three levels), unionnullinit (C++
member-pointer null initialization, all three levels), and
unionshadowstate (copy/zeroing capability semantics,
O2/O3). These are fork comparison tests, not a claim that the official
upstream suite fails or that staging has proved memory safety.
QuickJS, Expat and SQLite built from both package sets. The
package-local QuickJS worker/SAB repair from
fix-quickjs-worker-sab is retained independently of either
compiler pin. Both QuickJS builds passed the upstream suite, SAB
reader/refcount/aliasing tests, worker sharing and regexp checks; the
installed worker test also passed five repeated runs per variant. Expat
passed incremental parsing, entity expansion and mismatched-tag error
checks; SQLite passed transaction rollback, row ordering and
database-integrity smoke checks.
The earlier experiment's 84 Clang/LLVM tests used a development
compiler with existing runtime libraries. That evidence is separate from
the fresh runtime comparison above. All 18 pinned sparse sources were
built and all 10 source/import policy tests passed, including component
coherence and compiler assignment checks for both architectures. On
native ARM64, all 18 source hashes were independently verified and all
10 policy tests passed, including native Projeny coverage. Both complete
ARM64 toolchains built without FILC_DEV_LLVM; both LLVM and
installed QuickJS binaries were verified as AArch64 ELF executables.
Both variants passed coroutines, GC roots, link hygiene, wrapper roles,
QuickJS's upstream/SAB/worker/qjsc/regexp checks and zlib's
static/shared/64-bit tests. The official unsafe-call boundary
passed.
Staging passed all six native fork cases, their aggregate, and cancellation (112 scenario results, each with 20 passes). Release passed pointer atomics, union-record ABI and all ten CAS runs, but failed descriptors, nested cleanup and fiber unwinding. The passing ARM64 CAS runs differ from x86's observed bad writeback; they do not establish universal race freedom or that the residual CAS issue is fixed on every platform.
The SaRCAsm integration harness now selects an architecture-specific
assembly fixture rather than attempting to compile x86 instructions on
ARM64. The native checks passed in both variants, preserving the
unchanged load/identity assertions and requiring the deliberate OOB load
to trap with asm_load in the diagnostic. Both x86 variants
still pass. Both native fenv gates fail at fenv.c:27,
lquotient(FE_UPWARD) > lquotient(FE_DOWNWARD); those
failures remain strict, and subsequent fenv assertions were not
reached.
No requested native target remains pending. The x86-only
staging-cancellation-native harness was not run or counted
as ARM64 coverage; the broader 213-case runtime comparison was performed
on x86, not rerun on ARM64. These results validate the requested builds
and gates, not all ports or the soundness of the experimental
compiler.
A full nix flake check --no-build --all-systems remains
blocked by a missing dank-bashrc.drv, reproduced at the
original Filnix main revision; targeted checks bypass that unrelated
issue.
The older Filnix branches are historical checkpoints. Their toolchain
choices are now represented by these pins on main; keeping
them does not require switching branches. No fork or remote branch
deletion is needed.
Historical September 14 cancellation baseline
At this checkpoint, the core and ports pins selected
b6dd63481f796f8bff8502165c7dfc61091dbbd6. Both glibc source
components move from 2.40 to 2.44. The native Projeny build and all
seven source/import tests pass. All projects present at the new pin were
passed through the patch importer; existing-version changes include
Mesa, Ruby, Tar and the OpenSSL 3.6.4 port. New version patches are
retained for subsequent package upgrades. Older curated package versions
remain explicit in ports.nix; extraction does not silently
change their source archives or claim that every new version builds.
The shared runtime/glibc cancellation patches and test evidence are
described in the
implementation checkpoint. LLVM build and install both honor
NIX_BUILD_CORES through an explicit Ninja job limit. The
compiler bootstrap is rebuilt for this source update.
Update application patches
Fetch the upstream clone, then update the ports pin and Projeny source hash atomically. This reads the selected Git tree without changing the checkout:
git -C "$HOME/fil-c" fetch origin deluge
python3 scripts/update-ports-pin.py --repo "$HOME/fil-c" --rev origin/delugeList the projects present at that revision:
make -C ports list REPO_DIR="$HOME/fil-c"Regenerate selected patches (the targets run even when patches already exist):
make -C ports patch/gettext-0.22.5.patch REPO_DIR="$HOME/fil-c"
# Equivalent, with the revision read from ports/upstream.json:
ports/extract-patch.sh gettext-0.22.5 "$HOME/fil-c"Use nix develop -c make -C ports -j4 to regenerate all
projects at the pin. Review the resulting diffs and update the active
declarations in ports.nix or their package files under
ports/ when necessary. ports/patches.nix is a
historical inventory and does not affect builds. The pin is the default
for future extraction; it does not claim that every existing, curated
patch was extracted at that revision, nor does changing it automatically
upgrade all ports.
The extractor diffs the original import against the pinned tree. Other branches, later commits, dirty files and untracked build artifacts do not participate. An explicit fourth argument overrides the pin for investigating an older project version that no longer exists at the default revision:
ports/extract-patch.sh PROJECT "$HOME/fil-c" /tmp/patch-review FULL_COMMIT_IDPatch ownership and standalone upstream patches
ports/patch/ is generated upstream material;
patches/ is maintained locally. Keep additions in a
separate local patch applied after the upstream patch. See the
local patch convention for provenance headers and refresh review.
Importers reject the local directory as an output destination, and
make clean is restricted to the generated directory.
Some upstream ports already exist as standalone patches rather than
vendored project trees. ports/patch-sources.json maps an
extraction name to its upstream Git path. These files are copied
byte-for-byte from portsRev, and are included in
make -C ports and make -C ports list:
ports/extract-patch.sh boost-filc "$HOME/fil-c"
# Imports pizlix/boost-filc.patch into ports/patch/boost-filc.patch.The generated patch is an input, not a port declaration. Wire it into
the actual package attribute in ports.nix, check
source-version compatibility and patch order, and test the result.
Merely adding a ports/patches.nix inventory entry does not
change a derivation. Use the version
audit to compare the active declarations with Nixpkgs and the
selected upstream tree.
Projeny ports
nix build .#projeny builds the native C++ tool and runs
its upstream test suite. It is also available in
nix develop. Its runtime archive helpers are wrapped into
PATH; it has no dependency on the Fil-C compiler or runtime.
The importer accepts .projeny descriptors alongside
vendored directories:
nix develop -c make -C ports libffi.projeny
# Writes ports/patch/libffi-3.8.0.patch, using the descriptor's Origname.
nix develop -c ports/extract-patch.sh libffi.projeny "$HOME/fil-c" /tmp/patch-reviewFil-C’s own Projeny, minilute and SaRCAsm directories are excluded from release-patch extraction.
The importer reads the descriptor and original archive from the
pinned Git tree, uses Projeny to reconstruct the port in a temporary
directory, and compares that with the original release through the same
filters as other ports. Generated Autotools files are omitted, so the
libffi derivation runs autoreconf. Neither a developer's Projeny
worktree nor its status files participate in extraction. Descriptors
with URL: <url> <blake3> headers (dash, curl,
git, php) instead of Archive: need network access:
projeny download fetches each mirror in order until one
matches its hash. Errors preserve the previous patch. Binary changes are
rejected explicitly: Projeny's binary encoding is not Git's, and Nix's
ordinary patch phase cannot apply binary diffs. Such a future port
should consume materialized source.
A vendored directory may port the same release as a descriptor: at
b6dd63481f79, projects/openssl-3.6.4/ is the
SaRCAsm port and openssl.projeny (Origname
openssl-3.6.4) uses zunsafe forwarders into ordinary
assembly. The directory keeps openssl-3.6.4.patch; the
descriptor then writes openssl-3.6.4-projeny.patch.
Previously the descriptor's patch silently replaced the SaRCAsm one,
breaking openssl-sarcasm.
Run importer regression tests with the packaged tool available:
nix develop -c python3 tests/upstream-sources.pyNovember 2026 selective application bumps
The active release ports select OpenSSH 10.6p2, rsync 3.5.2, GNU
patch 2.8.1 (gnupatch), target patchelf 0.19.3, nghttp2
1.71.0, LZ4 1.11.0, zlib 1.4 and SaRCAsm OpenSSL 3.6.5. Default OpenSSL
stays at 3.5.7; native patchelf stays at Nixpkgs' version. The
compiler/runtime, glibc, SaRCAsm, minilute and importer pins are
unchanged. mg stays on Nixpkgs' ibara/OpenBSD source, rather than
switching to the unrelated troglobit fork. Mesa, Rust and zegarmistrz
changes are not part of this refresh.
Selected descriptors were materialized with the existing native
Projeny at upstream
f641231a1559bb90d5e16a2f1f247d8732db88e7, independently of
ports/upstream.json. To reproduce the review output:
rev=f641231a1559bb90d5e16a2f1f247d8732db88e7
for p in openssh rsync patch patchelf nghttp2 lz4 zlib sarcastic-openssl; do
nix develop -c ports/extract-patch.sh "$p.projeny" "$HOME/fil-c" \
"/tmp/port-review/$p" "$rev"
doneDo not run the whole importer into one directory at this revision:
sarcastic-openssl.projeny and
zunsafe-openssl.projeny both have
Origname: openssl-3.6.5. Preserve the former as
openssl-sarcasm-3.6.5.patch; never overwrite the
default-backend patch.
OpenSSH retains the descriptor's GSSAPI/KEX/socket-activation
adaptations. Nixpkgs' ordinary OpenSSH has no competing GSSAPI or
socket-activation patch; retain its locale, privsep-install and
ssh-keysign patches. Unlike Autotools-generated Makefiles, OpenSSH's
Makefile.in is hand-maintained: append its descriptor hunk
(with openssh/ path prefixes removed) after extraction,
since the generic importer omits it. Nixpkgs' autoreconf hook
regenerates configure/config.h.in from the patched configure.ac. Socket
activation remains conditional on --with-systemd, not
enabled by this refresh; the additional NIST hybrid KEX is not added to
the default list.
For zlib, keep only the source-level ARM CRC assembly disable from
the extracted patch, not the deleted placeholder Makefile or configured
zconf.h. Drop Nixpkgs' export-variable patch and LZ4's CVE-2025-62813
patch because their fixes are included in these releases. Keep Zstd's
existing Fil-C patches and add upstream's
LZ4F_contentChecksum_t compatibility fix.
Patchelf's test programs are native ELF fixtures (including non-PIE and unannotated assembly), while the tested and installed patchelf is Fil-C. The fixture build disables the native wrapper's automatic RPATH, uses native ldd, and preserves the fixture's own interpreter rather than copying the Fil-C tool's interpreter into it. No test failures are ignored.
Rsync 3.5.2 requires libidn2 for its default IDN support. Its new
I/O-priority option calls ioprio_set, which the released
runtime rejects as unsupported syscall 251. A package-local guard uses
rsync's existing unsupported-operation warning for I/O priority; CPU
niceness still works. Two new embedded test scripts need Nix shebangs.
The new hardlinks-emlink-recovery test joins
source-change-size-continues in the exclusions because both
require dlsym(RTLD_NEXT, ...) interposition, which Fil-C
rejects. Neither requires a compiler/runtime change to support ordinary
transfers.
Validation used ordinary
legacyPackages.x86_64-linux.pkgsFilc, not staging:
- All eight selected packages built, along with Zstd and curl reverse dependencies. The release compiler wrapper's derivation remained identical.
- SaRCAsm OpenSSL: all 353 upstream test recipes, 4,569 assertions, passed; the existing AES out-of-bounds safety check trapped as intended. SHA-256 and AES-128 known-answer encryption/decryption also passed.
- Rsync: 256 upstream tests passed, 107 skipped, plus 13 explicit exclusions. This includes IDN, CPU niceness, and the repaired embedded-script fixtures.
- Patchelf: 69 passed, six skipped, no failures. Rewriting the RPATH and interpreter of a Fil-C executable and then running it passed.
- GNU patch: 46 passed, two expected failures, no unexpected failures; applying and reversing a patch also passed.
- zlib's static/shared/64-bit tests passed. Independent library probes checked CRC known answers, embedded-NUL compression roundtrips, corrupt zlib input, LZ4 output bounds, and nghttp2 HPACK dynamic-table roundtrips.
- LZ4's shared-only Makefile build passed. A release Zstd override
with
-DZSTD_LZ4_SUPPORT=ONpassed LZ4-frame encode/decode interoperability. Rebuilt curl decoded HTTP gzip and reported zlib 1.4/nghttp2 1.71.0. - A loopback OpenSSH daemon passed public-key acceptance/rejection,
the optional
mlkem1024nistp384-sha384KEX, seccomp setup and SIGHUP re-exec. Rsync over that SSH connection passed zstd/LZ4/zlib transfers with hardlinks, symlinks and deletion. GSSAPI authentication and systemd socket activation were not exercised; the ordinary package enables neither. - Ten of eleven
tests/upstream-sources.pytests passed. The staging-runtime patch-list expectation intest_toolchain_variantsfails identically on the unchanged baseline; this refresh leaves that unrelated test untouched.
Initial September 2026 ports refresh
The ports pin is
4867f1179f1c3dbe5484ec0f98c2fcc7d401e50c. Existing
checked-in patches were regenerated for their existing versions; libffi
moves from 3.4.6 to Projeny's 3.8.0 and uses upstream's closure
allocation fix. Archived patches for ports not currently enabled are
retained, but regeneration alone does not validate those packages. In
particular, the OpenSSL 3.5.7 update is the async context-switch fix;
the separate 3.6.4 assembly port needed the subsequent core and SaRCAsm
update described below.
That initial refresh advanced the core only to
2adb1051abf8a73778d8cb3cd94f4126363e5a08: upstream's fix
for C++ exceptions crossing zcall. The first libffi run
passed 1,738 checks but failed both exception-unwinding cases with the
old runtime. This core update changes only two runtime files; the LLVM
source hash stays unchanged. checks.x86_64-linux.libffi
covers calls, closures, pointer capabilities, variadic arguments and C++
exception propagation. With that fix, libffi's full suite reports 1,742
expected passes, no failures and two unsupported tests. Projeny's native
suite reports 996 passes; all six source/import regression tests pass as
well.
Ruby uses upstream's pthread coroutine backend: selecting native
assembly left coroutine_transfer unresolved and made
extension probes falsely reject Ruby APIs. Its derivation now uses
mkRuby/mkRubyVersion, keeping the 3.3.10
source, soname and gem metadata consistent. The gem configuration
importer consumes the port list directly.
The refreshed active ports (libffi, Bison, Grep, M4, Tar, OpenSSL, libwebp and Ruby) build with this core. Runtime checks cover parser generation, macro expansion, matching, archive and lossless image roundtrips, OpenSSL async AES, and Ruby Fiddle calls/closures, BigDecimal, io/console and 100 finalizers.
Historical SaRCAsm integration checkpoint
At this checkpoint, the core used
4867f1179f1c3dbe5484ec0f98c2fcc7d401e50c, matching the
ports pin. SaRCAsm and minilute have separate sparse source components
at the core revision: they implement the compiler/runtime ABI, while
changes to their sources do not invalidate LLVM's source component.
Minilute includes only its own tree and the vendored Luau subtree it
needs. Both tools build natively, avoiding a compiler bootstrap
cycle.
The compiler wrapper pins the Fil-C resource directory containing
SaRCAsm. Its ccache check hashes wrapper contents, including the pinned
runtime and assembler paths, rather than relying on Nix-normalized
timestamps and file sizes. SaRCAsm invokes its pinned GNU assembler by
absolute path after inserting Fil-C capability checks. The loader is now
named ld-fil1-x86_64.so, including its ELF soname and the
stdenv's dynamic-linker metadata. Bootstrap glibc explicitly uses
-yolo-assembler, as in upstream's bootstrap script.
OpenSSL 3.6.4 is an alternative package, leaving the existing 3.5.7
port as the default. It is also exposed as
pkgsFilc.openssl-sarcasm for dependency overrides:
nix build -L .#openssl-sarcasm
nix run .#openssl-sarcasm -- version -aIts perlasm generators run with SARCASM=1, assembly uses
the compiler's SaRCAsm default, and the unsupported VIA PadLock engine
is disabled. The 3.5.7 port continues using its runtime forwarders and
ordinary assembler. The alternative runs the upstream OpenSSL test suite
during its build.
nix build -L .#filcc .#sarcasm .#checks.x86_64-linux.sarcasm \
.#checks.x86_64-linux.libffi .#checks.x86_64-linux.libtool-symbols \
.#checks.x86_64-linux.openssl-sarcasmThe SaRCAsm integration check compiles annotated assembly through the
final compiler, verifies pointer-return capabilities, and requires an
out-of-bounds assembly load to report a Fil-C safety error. The OpenSSL
check verifies an AES known-answer vector and requires an invalid output
pointer to trap inside AES_encrypt. The full suite patches
test-helper shebangs for the Nix sandbox before execution.
At this revision the OpenSSL suite passes all 4,561 tests across 352 files. The installed binary also passes SHA-256, an AES encryption/decryption roundtrip, AES-GCM and asynchronous AES-CBC checks. The default OpenSSL 3.5.7 still builds and passes its SHA-256 smoke check with Fil-C 0.684. Libffi reports 1,742 expected passes, no failures and two unsupported tests; its C++ exception checks and the libtool symbol check also pass.
Update a toolchain pin
Use the local clone to compute all source hashes before recording the new pin:
# Ordinary updates follow official releases, not deluge HEAD or our fork.
git -C "$HOME/fil-c" fetch origin tag v0.687
scripts/update-filc-source-hashes.py --repo "$HOME/fil-c" \
--rev v0.687 --release 0.687
# Experiment updates leave the official pin and its hashes untouched.
scripts/update-filc-source-hashes.py --repo "$HOME/fil-c" \
--variant staging --rev FULL_FORK_COMMIT_IDReview compiler/runtime soundness and run targeted regressions before adopting either pin. Keep the staging URL pointed at the fork, and publish a fork commit before pinning it for others. A package-build success is not a safety audit.
Without --rev, this recomputes hashes at the selected
variant's existing revision. The script uses temporary detached
worktrees and removes them afterwards; it does not change the clone's
checked-out branch or files. --pull explicitly opts into
pulling the clone first. A hashing failure leaves the existing pin and
hashes untouched. Empty component selections are rejected, so a renamed
or removed upstream directory must be addressed before recording the
update.
sourcePatterns in lib/filc-upstream.json
are Git non-cone sparse-checkout patterns, shared by
fetchgit and the hash updater. Anchored selections omit
unrelated ancestor files such as README.md and
build_*.sh. The compiler, C++ libraries and runtime have
separate selections; filc/tests is excluded. The C++
selection includes LLVM-libc, whose shared conversion utilities libc++
uses even when the target C library is glibc. When a build needs another
upstream file, extend its selection and regenerate hashes at the same
core revision.
Each fetch has a stable name (filc0-src,
libpas-src, etc.). If an upstream revision changes only
unselected files, its source hash and store output path stay the same,
as do downstream output paths. Fetch and dependent .drv
files can change to describe the new revision without requiring those
outputs to rebuild. Switching existing installations to these
names/selections causes one rebuild; subsequent updates benefit from the
finer dependency boundaries.
Verify without rebuilding LLVM
python3 tests/upstream-sources.pyThis uses small temporary Git repositories and the pinned nixpkgs to
check source hashes, Nix output reuse across revisions,
component-specific changes, pin-update failure handling, reproducible
patch extraction, and the real compiler derivation's independence from
portsRev. It does not compile code.
For a core update, also build the toolchain and representative ports:
nix build -L --no-link .#filcc .#checks.x86_64-linux.libtool-symbols \
.#legacyPackages.x86_64-linux.pkgsFilc.expat \
.#legacyPackages.x86_64-linux.pkgsFilc.libffi \
.#legacyPackages.x86_64-linux.pkgsFilc.gmpWith Swash installed,
prefix that command with
swash start --tag PROJECT=filnix -- to run it in the
background. Swash prints a session ID; swash poll ID
retrieves saved output and swash follow ID follows it
through completion, returning the build's exit status. Detaching a
follower leaves the build running.
The Lute 1.0.0 extraction also contains roughly 492,000 lines of vendored third-party source changes. It is not consumed by a Filnix port and is not checked in as a release patch; the pinned upstream tree remains its source. Other newly extracted version patches are retained as an archive, without implicitly enabling those package versions.