From b9aa12371c6245f3a92fea0d8695d98f0e6e5139 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Sat, 11 Jul 2026 09:07:14 -0400 Subject: [PATCH 1/3] dinit: supervise Kandelo service processes --- Formula/dinit.rb | 237 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 238 insertions(+) create mode 100644 Formula/dinit.rb diff --git a/Formula/dinit.rb b/Formula/dinit.rb new file mode 100644 index 0000000..fcfce4a --- /dev/null +++ b/Formula/dinit.rb @@ -0,0 +1,237 @@ +require_relative "../Kandelo/formula_support/kandelo_formula_support" + +class Dinit < Formula + include KandeloFormulaSupport + + desc "Service manager and process supervisor for Kandelo" + homepage "https://davmac.org/projects/dinit/" + url "https://github.com/davmac314/dinit/archive/refs/tags/v0.19.4.tar.gz" + sha256 "3c0f624eb958f8e884631be4ef687da1e475ebaa6241e7ee330b864e6cd9e30b" + license "Apache-2.0" + + depends_on "m4" => :build + depends_on "wabt" => :build + depends_on "automattic/kandelo-homebrew/libcxx" + + skip_clean "bin/dinit" + skip_clean "bin/dinitctl" + skip_clean "bin/dinitcheck" + skip_clean "bin/dinit-monitor" + + GUEST_OPT_PREFIX = "/home/linuxbrew/.linuxbrew/opt/dinit".freeze + GUEST_LIBCXX_PREFIX = "/home/linuxbrew/.linuxbrew/opt/libcxx".freeze + + # LLVM lowers Wasm setjmp/longjmp through exceptions. An exception escaping + # these noexcept event loops is otherwise intercepted by C++ termination + # cleanup before the generated setjmp dispatch can receive it. + patch :DATA + + def install + kandelo_require_arch!("wasm32") + libcxx = formula_opt_prefix("automattic/kandelo-homebrew/libcxx") + build_cxx = kandelo_host_cxx + + # Dinit uses this native generator to produce its target configuration + # header. Build it in Kandelo's canonical native dev shell so target libcxx + # dependency paths cannot enter the host compiler environment. + system build_cxx, + "-DNDEBUG", "-std=c++14", "-O2", "-pthread", + "build/tools/mconfig-gen.cc", "-o", "build/tools/mconfig-gen" + + kandelo_wasm_build do |root| + prefix_maps = [ + "-ffile-prefix-map=#{buildpath}=/usr/src/dinit", + "-fdebug-prefix-map=#{buildpath}=/usr/src/dinit", + "-fmacro-prefix-map=#{buildpath}=/usr/src/dinit", + "-ffile-prefix-map=#{root}=/usr/src/kandelo", + "-fdebug-prefix-map=#{root}=/usr/src/kandelo", + "-fmacro-prefix-map=#{root}=/usr/src/kandelo", + "-ffile-prefix-map=#{libcxx}=#{GUEST_LIBCXX_PREFIX}", + "-fdebug-prefix-map=#{libcxx}=#{GUEST_LIBCXX_PREFIX}", + "-fmacro-prefix-map=#{libcxx}=#{GUEST_LIBCXX_PREFIX}", + ] + target_cxxflags = [ + "-std=c++14", + "-O2", + "-fwasm-exceptions", + "-nostdinc++", + "-isystem #{libcxx}/include/c++/v1", + *prefix_maps, + ].join(" ") + target_ldflags = "-L#{libcxx}/lib -lc++ -lc++abi" + + ENV["CXXFLAGS"] = target_cxxflags + ENV["TEST_CXXFLAGS"] = target_cxxflags + ENV["CPPFLAGS"] = "-D_POSIX_C_SOURCE=200809L" + ENV["LDFLAGS"] = target_ldflags + ENV["TEST_LDFLAGS"] = target_ldflags + ENV["CXX_FOR_BUILD"] = build_cxx.to_s + ENV["CXXFLAGS_FOR_BUILD"] = "-std=c++14 -O2" + ENV["CPPFLAGS_FOR_BUILD"] = "-DNDEBUG" + ENV["LDFLAGS_FOR_BUILD"] = "-pthread" + + system "./configure", + "--platform=Linux", + "--prefix=#{GUEST_OPT_PREFIX}", + "--exec-prefix=#{GUEST_OPT_PREFIX}", + "--sbindir=#{GUEST_OPT_PREFIX}/bin", + "--mandir=#{GUEST_OPT_PREFIX}/share/man", + "--syscontrolsocket=/run/dinitctl", + "--disable-strip", + "--disable-shutdown", + "--disable-cgroups", + "--disable-utmpx", + "--default-auto-restart=on-failure" + system "make", "-j#{ENV.make_jobs}" + + # LLVM already optimized each translation unit. A post-link Binaryen pass + # increases this program's fork-replay surface, so instrument the linked + # fork callers directly and leave instrumentation as the final transform. + fork_programs = %w[dinit dinit-monitor] + fork_programs.each { |program| kandelo_fork_instrument(buildpath/"src"/program) } + + programs = %w[dinit dinitctl dinitcheck dinit-monitor] + artifact_guards = "#{root}/scripts/wasm-artifact-guards.sh" + program_paths = programs.map { |program| (buildpath/"src"/program).to_s.shellescape }.join(" ") + fork_paths = fork_programs.map { |program| (buildpath/"src"/program).to_s.shellescape }.join(" ") + system "bash", "-c", <<~SH + set -euo pipefail + . #{artifact_guards.shellescape} + for program in #{program_paths}; do + wasm_require_no_legacy_asyncify "$program" + wasm_require_fork_instrumentation_if_needed "$program" + unexpected_env_imports=$(wasm-objdump -x "$program" | + awk '/<- env[.]/ { sub(/^.*<- env[.]/, ""); print $1 }' | + grep -Ev '^(__channel_base|memory|setjmp|longjmp)$' || true) + if [ -n "$unexpected_env_imports" ]; then + echo "ERROR: $program contains unresolved non-ABI env imports" >&2 + echo "$unexpected_env_imports" >&2 + exit 1 + fi + done + for program in #{fork_paths}; do + if ! wasm_has_complete_fork_instrumentation "$program"; then + echo "ERROR: $program has incomplete fork instrumentation" >&2 + exit 1 + fi + done + SH + + expected_abi = (Pathname(root)/"crates/shared/src/lib.rs").read[ + /^pub const ABI_VERSION: u32 = ([0-9]+);$/, + 1, + ] + odie "could not read Kandelo ABI version" if expected_abi.nil? + + abi_probe = <<~JS + import { readFileSync } from "node:fs"; + import { pathToFileURL } from "node:url"; + const { extractAbiVersion } = await import(pathToFileURL(process.argv[1]).href); + const bytes = readFileSync(process.argv[2]); + const program = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); + const abi = extractAbiVersion(program); + if (abi === null) process.exit(2); + process.stdout.write(String(abi)); + JS + constants = Pathname(root)/"host/src/constants.ts" + programs.each do |program| + wasm = buildpath/"src"/program + artifact_abi = cd(root) do + Utils.safe_popen_read( + "node", "--import", "tsx/esm", "--input-type=module", "--eval", abi_probe, + constants, wasm + ).strip + end + if artifact_abi != expected_abi + odie "#{program} ABI #{artifact_abi} does not match Kandelo ABI #{expected_abi}" + end + end + end + + %w[dinit dinitctl dinitcheck dinit-monitor].each do |program| + kandelo_install_bin(buildpath/"src", program, program) + end + man5.install "doc/manpages/dinit-service.5" + man8.install "doc/manpages/dinit.8", + "doc/manpages/dinitctl.8", + "doc/manpages/dinitcheck.8", + "doc/manpages/dinit-monitor.8" + end + + test do + assert_match(/Dinit version 0\.19\.4/, kandelo_run_wasm(bin/"dinit", ["--version"])) + assert_match(/Dinit version 0\.19\.4/, kandelo_run_wasm(bin/"dinitctl", ["--version"])) + assert_match(/Dinit version 0\.19\.4/, kandelo_run_wasm(bin/"dinit-monitor", ["--version"])) + assert_includes kandelo_run_wasm(bin/"dinitcheck", ["--help"]), "dinitcheck" + + probe_service = testpath/"probe.service" + probe_service.write <<~EOS + type = internal + EOS + monitor_service = testpath/"monitor.service" + monitor_service.write <<~EOS + type = process + command = #{GUEST_OPT_PREFIX}/bin/dinit-monitor --socket-path /tmp/dinitctl --initial --exit --command "#{GUEST_OPT_PREFIX}/bin/dinitcheck --help" probe + restart = false + EOS + + # dinit-monitor's --initial --exit path returns only after its notification + # command has forked, exec'd, exited, and been waited for. + output = kandelo_run_wasm( + bin/"dinit", + [ + "--container", + "--services-dir", "/etc/dinit.d", + "--socket-path", "/tmp/dinitctl", + "monitor" + ], + exec_programs: { + "#{GUEST_OPT_PREFIX}/bin/dinit-monitor" => bin/"dinit-monitor", + "#{GUEST_OPT_PREFIX}/bin/dinitcheck" => bin/"dinitcheck", + }, + guest_files: { + "/etc/dinit.d/monitor" => monitor_service, + "/etc/dinit.d/probe" => probe_service, + }, + ) + assert_match(/\[\s*OK\s*\] monitor/, output) + assert_match(/\[STOPPD\] monitor/, output) + + %w[dinit dinitctl dinitcheck dinit-monitor].each do |program| + binary = File.binread(bin/program) + refute_includes binary, prefix.to_s + refute_includes binary, "/nix/store/" + refute_match %r{/private/tmp/[^/]+/}, binary + refute_match %r{/Users/[^/]+/}, binary + end + assert_includes File.binread(bin/"dinit"), "#{GUEST_OPT_PREFIX}/bin" + end +end + +__END__ +diff --git a/dasynq/include/dasynq/pselect.h b/dasynq/include/dasynq/pselect.h +index d370be6..578ab39 100644 +--- a/dasynq/include/dasynq/pselect.h ++++ b/dasynq/include/dasynq/pselect.h +@@ -225,7 +225,7 @@ template class pselect_events : public signal_events + // + // do_wait - if false, returns immediately if no events are + // pending. +- void pull_events(bool do_wait) noexcept ++ void pull_events(bool do_wait) + { + struct timespec ts; + struct timespec *wait_ts = nullptr; +diff --git a/dasynq/include/dasynq/select.h b/dasynq/include/dasynq/select.h +index 5aae627..b6edc3e 100644 +--- a/dasynq/include/dasynq/select.h ++++ b/dasynq/include/dasynq/select.h +@@ -283,7 +283,7 @@ template class select_events : public signal_events + // + // do_wait - if false, returns immediately if no events are + // pending. +- void pull_events(bool do_wait) noexcept ++ void pull_events(bool do_wait) + { + struct timeval ts; + struct timeval *wait_ts = nullptr; diff --git a/README.md b/README.md index 1d2c102..1c04205 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Current migration controls and pilots include: - `asa`, FreeBSD's POSIX carriage-control translator for FORTRAN output; - `m4`, the GNU macro processor with process-executing builtins backed by the tap's Dash shell; - `gawk`, GNU's pattern scanning and text-processing language; +- `dinit`, the service manager, control client, configuration checker, and process monitor; - `binutils`, GNU's native WebAssembly archive, symbol, and inspection suite, with exact trailing/representable `.wasm.*` custom-section and strip transforms, plus explicit rejection of relocatable, dynamic, cross-format, or lossy rewrites; From fc43818d4233b1965be9c0112666d1854a1d8285 Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Sun, 12 Jul 2026 06:35:47 -0400 Subject: [PATCH 2/3] dinit: use the shared artifact contract --- Formula/dinit.rb | 49 +++++++----------------------------------------- 1 file changed, 7 insertions(+), 42 deletions(-) diff --git a/Formula/dinit.rb b/Formula/dinit.rb index fcfce4a..c99bea2 100644 --- a/Formula/dinit.rb +++ b/Formula/dinit.rb @@ -1,4 +1,4 @@ -require_relative "../Kandelo/formula_support/kandelo_formula_support" +require (Tap.fetch("automattic", "kandelo-homebrew").path/"Kandelo/formula_support/kandelo_formula_support").to_s class Dinit < Formula include KandeloFormulaSupport @@ -9,6 +9,7 @@ class Dinit < Formula sha256 "3c0f624eb958f8e884631be4ef687da1e475ebaa6241e7ee330b864e6cd9e30b" license "Apache-2.0" + depends_on "binaryen" => :build depends_on "m4" => :build depends_on "wabt" => :build depends_on "automattic/kandelo-homebrew/libcxx" @@ -91,15 +92,15 @@ def install fork_programs.each { |program| kandelo_fork_instrument(buildpath/"src"/program) } programs = %w[dinit dinitctl dinitcheck dinit-monitor] - artifact_guards = "#{root}/scripts/wasm-artifact-guards.sh" + programs.each do |program| + fork_policy = fork_programs.include?(program) ? :required : :auto + kandelo_validate_wasm_artifact(buildpath/"src"/program, fork: fork_policy) + end + program_paths = programs.map { |program| (buildpath/"src"/program).to_s.shellescape }.join(" ") - fork_paths = fork_programs.map { |program| (buildpath/"src"/program).to_s.shellescape }.join(" ") system "bash", "-c", <<~SH set -euo pipefail - . #{artifact_guards.shellescape} for program in #{program_paths}; do - wasm_require_no_legacy_asyncify "$program" - wasm_require_fork_instrumentation_if_needed "$program" unexpected_env_imports=$(wasm-objdump -x "$program" | awk '/<- env[.]/ { sub(/^.*<- env[.]/, ""); print $1 }' | grep -Ev '^(__channel_base|memory|setjmp|longjmp)$' || true) @@ -109,43 +110,7 @@ def install exit 1 fi done - for program in #{fork_paths}; do - if ! wasm_has_complete_fork_instrumentation "$program"; then - echo "ERROR: $program has incomplete fork instrumentation" >&2 - exit 1 - fi - done SH - - expected_abi = (Pathname(root)/"crates/shared/src/lib.rs").read[ - /^pub const ABI_VERSION: u32 = ([0-9]+);$/, - 1, - ] - odie "could not read Kandelo ABI version" if expected_abi.nil? - - abi_probe = <<~JS - import { readFileSync } from "node:fs"; - import { pathToFileURL } from "node:url"; - const { extractAbiVersion } = await import(pathToFileURL(process.argv[1]).href); - const bytes = readFileSync(process.argv[2]); - const program = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); - const abi = extractAbiVersion(program); - if (abi === null) process.exit(2); - process.stdout.write(String(abi)); - JS - constants = Pathname(root)/"host/src/constants.ts" - programs.each do |program| - wasm = buildpath/"src"/program - artifact_abi = cd(root) do - Utils.safe_popen_read( - "node", "--import", "tsx/esm", "--input-type=module", "--eval", abi_probe, - constants, wasm - ).strip - end - if artifact_abi != expected_abi - odie "#{program} ABI #{artifact_abi} does not match Kandelo ABI #{expected_abi}" - end - end end %w[dinit dinitctl dinitcheck dinit-monitor].each do |program| From fb6b3da7fb1d948c750867cdee054fd980e13a7d Mon Sep 17 00:00:00 2001 From: Brandon Payton Date: Sun, 12 Jul 2026 09:31:24 -0400 Subject: [PATCH 3/3] dinit: enforce fork-free control tools Dinitctl and dinitcheck do not import fork and are not instrumented. Make that negative boundary explicit so a future dependency or link change cannot silently turn either control-plane tool into a fork-capable artifact. --- Formula/dinit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Formula/dinit.rb b/Formula/dinit.rb index c99bea2..5ac08c5 100644 --- a/Formula/dinit.rb +++ b/Formula/dinit.rb @@ -93,7 +93,7 @@ def install programs = %w[dinit dinitctl dinitcheck dinit-monitor] programs.each do |program| - fork_policy = fork_programs.include?(program) ? :required : :auto + fork_policy = fork_programs.include?(program) ? :required : :forbidden kandelo_validate_wasm_artifact(buildpath/"src"/program, fork: fork_policy) end