Fil-C on Nix

Filnix wiki

upstream-updates.md

Updating Fil-C sources and ports

Filnix maintains two toolchain variants on main, plus an independent application-patch extraction pin:

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:

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:

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.descriptors

The 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/deluge

List 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_ID

Patch 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-review

Fil-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.py

November 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"
done

Do 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:

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 -a

Its 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-sarcasm

The 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_ID

Review 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.py

This 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.gmp

With 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.