Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ci/spec22-gems/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ but no state.

`run-msys.sh` is the same acceptance on windows (spec 22 §8's last row:
the suite green with the gem gone on **every** published platform). Same
fixtures, same four jailed legs, same pinned PROBE lines — but it builds
fixtures, the same four jailed proof legs plus a fifth forensic one
(`sassc-matrix` — the incident-13 diag sheet on a pristine loader: the
sha256 legs, the ffi solo loads, the spelling × flag raw-loader matrix;
never gates), same pinned PROBE lines — but it builds
nothing: the runtime arrives as the factory's CI artifacts and the
press/extract tooling is the published windows `tfs` CLI. On msys the
POSIX harness's roll → link-unit → factory-build chain would re-run the
Expand Down
5 changes: 5 additions & 0 deletions ci/spec22-gems/fixtures/payload-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
# excludes /__tpkg__/; blob_sha256 stays as authored — advisory producer
# provenance, never a verification input for an embedded manifest).
#
# The library_aliases: block (spec 03 §2.5) is also stamped at press time
# — by run-msys.sh, one entry per vendored closure DLL, a DERIVED set
# (never hand-listed here; POSIX run.sh stamps nothing — the bare-name
# rule is a windows contract).
#
# The materialize: block is the spec 22 §4 class-R declaration under
# test: main.scss and the partial it @imports are files libsass's OWN
# C++ importer must fopen() on a raw HOST path (the interpreter's
Expand Down
219 changes: 136 additions & 83 deletions ci/spec22-gems/fixtures/probe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
# with the pinned class-R signature. Expected RED —
# the mechanism oracle: a GREEN here means the flip
# came from somewhere other than materialize:.
# sassc-matrix — pure forensics, never gates: the incident-13 diag
# sheet (sha256 legs, ffi solo loads, the spelling ×
# flag raw-loader matrix) run WITHOUT the require
# attempt, so the loader starts pristine. windows
# only (POSIX prints a skip line). Expected GREEN.
#
# Every invocation prints the instrumentation line
# `PROBE gem-loaded yes|no` first — whether a tebako-runtime gem got
Expand Down Expand Up @@ -150,7 +155,10 @@ def static_fetch
# match IS this payload's extraction — anything else (no export, no match,
# several matches) is a loud probe failure, never a silent fallthrough.
def materialized_styles_dir
cache = ENV["TEBAKO_EXEC_CACHE"].to_s
# The driver exports the HOST spelling (backslashes on windows), and
# Dir.glob treats backslash as an escape there — glob with the
# forward-slash spelling (ruby accepts it everywhere on windows).
cache = ENV["TEBAKO_EXEC_CACHE"].to_s.tr('\\', '/')
if cache.empty?
puts "PROBE sassc-partial fail TEBAKO_EXEC_CACHE is not exported (the driver predates class R?)"
exit 1
Expand All @@ -176,36 +184,33 @@ def sassc_partial(styles)
).render
end

# Incident 13 round 7 diagnostics. Round 6 (the first run carrying the
# ffi bisect) proved two things and broke one: the closure walk's answers
# match ground truth byte-for-byte (the pressed payload image, extracted
# on macOS: all 16 imports of libsass.so match llvm-objdump's read of the
# real import tables; every vendored sibling is a valid coff-x86-64 PE),
# and the ffi failure is the OS's own answer (the shim's covered route
# forwards LOAD_WITH_ALTERED_SEARCH_PATH — dln_c_dlmap_msys.patch forces
# 0x8 when the caller named no LOAD_LIBRARY_SEARCH_* order). What broke:
# FFI::DynamicLibrary.load_library went private in ffi 1.17, so the
# round-6 bisect died NoMethodError on its first leg and no dep-load
# verdict ever printed.
# Incident 13 round 8 diagnostics. Round 7 answered the byte and bind
# questions: every vendored module's sha256 through ruby's patched IO
# matched the image-extracted constants (no windows backend read bug),
# the closure walk materialized the siblings co-dir, and each module —
# libsass.so included — SOLO-LOADS through ffi's DynamicLibrary.open once
# its siblings are resident in the process. Yet ffi_lib's load of the
# in-image libsass.so spelling still 126'd at require time, and the ext/
# fallback's error 5 is the jail's EACCES synthesis on a file the image
# never held (the mkmf stub is not installed there — red herring). So
# the failure lives in the DEPENDENCY search of ffi's raw LoadLibraryExA
# (LOAD_WITH_ALTERED_SEARCH_PATH on the non-relative spelling), not in
# the bytes and not in the main module's own open.
#
# What has never been measured ON THE RUNNER:
# (a) the bytes the WINDOWS backend streams out — a windows-only backend
# read bug is poison the macOS extraction cannot show, so sha256 each
# vendored module through ruby's patched IO and compare against the
# image-extracted constants (the vendored DLLs are copied binaries —
# byte-stable across runs; libsass.so is compiled per run by gem
# install, so its comparison is informational, never a verdict);
# (b) each vendored sibling's OWN OS bind — the 126 names "a dep",
# never which;
# (c) the search-order semantics themselves: a fiddle-driven flag matrix
# against the materialized HOST spelling. The host spelling is not a
# covered path, so the shim passes it through byte-identical and
# fiddle drives the RAW loader; the default-order leg is the negative
# control (the standard order never searches the DLL's own dir, so
# 126 there is documented behavior — a SUCCESS there rewrites the
# model).
# Round 8's questions:
# (a) the spelling × flag matrix (sassc_raw_matrix) — the A: legs name
# the covered route's answer per flag; the host legs measure the
# bare OS loader; the 0x0 leg is the default-order negative control;
# (b) the already-loaded-table discriminator (sassc_ffi_load_legs runs
# libsass.so before AND after its siblings);
# (c) the DECLARED answer (the candidate fix): the payload manifests
# stamp library_aliases: for the vendored closure at press time
# (spec 03 §2.5), so the driver boot-materializes the siblings and
# PATH-leads their dir — the spec 22 §2.1 raw-surface mechanism,
# exercised end to end. ffi_lib's own A: spelling keeps its route;
# the alias carries the bare-name IMPORTS the PE closure lists.
# Never gates: the original LoadError re-raises after the verdicts, and
# no leg may kill the bisect (every leg rescues StandardError).
# no leg may kill the sheet (every leg rescues LoadError/StandardError).

# sha256 + byte counts of the three vendored modules, extracted from the
# round-6 run's pressed payload image (probe-gems-4.0.6.tfs) on macOS via
Expand All @@ -217,12 +222,17 @@ def sassc_partial(styles)
}.freeze

# The vendored modules' in-image paths, keyed by basename; empty when the
# sassc spec never activated (the require died before rubygems recorded it).
# sassc spec is not discoverable. find_by_name, never loaded_specs — the
# sassc-matrix leg runs this sheet WITHOUT a require attempt, so nothing
# has activated the spec; the bisect path (post-LoadError) finds the same
# record either way.
def sassc_module_paths
spec = Gem.loaded_specs["sassc"]
spec = Gem::Specification.find_by_name("sassc")
return {} if spec.nil?
native_dir = File.join(spec.gem_dir, "lib", "sassc")
SASSC_MODULE_WANTS.keys.to_h { |mod| [mod, File.join(native_dir, mod)] }
rescue LoadError, StandardError
{} # a broken gemhome index must not take the sheet down with it
end

def sassc_sha256_legs
Expand All @@ -239,79 +249,111 @@ def sassc_sha256_legs
end

# Two host-surface controls by bare name (a stock OS module; an
# api-ms-win-crt contract), then each vendored sibling individually, then
# the top module — all through ffi's public DynamicLibrary.open, i.e. the
# same covered route the failing ffi_lib took. A success stays loaded and
# would poison later legs, so every success is freed at once.
# api-ms-win-crt contract), then the vendored modules — all through
# ffi's public DynamicLibrary.open, i.e. the same covered route the
# failing ffi_lib took (ffi's win32 dl_open ignores the RTLD flags and
# binds LoadLibraryExA with LOAD_WITH_ALTERED_SEARCH_PATH for every
# non-relative spelling — DynamicLibrary.c). libsass.so runs FIRST
# (nothing vendored resident — the already-loaded-table discriminator),
# the siblings next, then libsass.so again: a first-fail/second-ok pair
# names the dep-search class, and a success stays loaded (ffi 1.17's
# DynamicLibrary has no public free — the round-7 `.free` legs were
# NoMethodError noise; the raw matrix below owns the freed-legs
# questions).
def sassc_ffi_load_legs
legs = { "ADVAPI32.dll" => "ADVAPI32.dll",
"api-ms-win-crt-runtime-l1-1-0.dll" => "api-ms-win-crt-runtime-l1-1-0.dll" }
sassc_module_paths.each { |label, path| legs[label] = path }
paths = sassc_module_paths
legs["libsass.so"] = paths["libsass.so"] if paths["libsass.so"]
%w[libwinpthread-1.dll libgcc_s_seh-1.dll].each { |mod| legs[mod] = paths[mod] if paths[mod] }
legs["libsass.so:again"] = paths["libsass.so"] if paths["libsass.so"]
legs.each do |label, spell|
lib = FFI::DynamicLibrary.open(spell, FFI::DynamicLibrary::RTLD_LAZY)
FFI::DynamicLibrary.open(spell, FFI::DynamicLibrary::RTLD_LAZY)
puts "PROBE-DIAG dep-load #{label} ok"
lib.free
rescue StandardError => le
rescue LoadError, StandardError => le
puts "PROBE-DIAG dep-load #{label} fail #{le.message.lines.first.to_s.strip}"
end
end

# The raw-loader flag matrix against the materialized HOST spelling. The
# dlmap cache mirrors the memfs tree under TEBAKO_EXEC_CACHE/tebako-dl-*;
# the host spelling is not a covered path, so the shim passes it through
# byte-identical and these legs measure the OS loader alone.
def sassc_fiddle_matrix
cache = ENV["TEBAKO_EXEC_CACHE"].to_s
if cache.empty?
puts "PROBE-DIAG fiddle skipped (TEBAKO_EXEC_CACHE unset)"
# The raw-loader matrix — spelling × flags — driven through FFI-bound
# kernel32 calls. Round 7 drove this through fiddle; fiddle left the
# default-gem set in ruby 4.0 ("fiddle is not part of the default gems"
# — the round-7 sheet died on the require, a LoadError StandardError
# never rescues). ffi is already on the sheet path (sassc's native.rb
# requires it before the failing ffi_lib) and its DynamicLibrary is the
# very surface under test, so the matrix needs no new dependency.
#
# The A: (VFS) legs are the covered-route question — does the shim serve
# the loader for THIS spelling/flag pair; the host legs measure the bare
# OS loader (the shim passes the materialized spelling through
# byte-identical). 0x0 (default order) is the negative control: the
# standard order never searches the DLL's own dir, so a 126 there is
# documented behavior and a SUCCESS rewrites the model. 0x8 is ffi's own
# binding (DynamicLibrary.c: LoadLibraryExA with LOAD_WITH_ALTERED_
# SEARCH_PATH for non-relative names). Every success is FreeLibrary'd at
# once so no leg poisons the next through the already-loaded table.
def sassc_raw_matrix
unless Gem.win_platform?
puts "PROBE-DIAG raw-matrix skipped (posix)"
return
end
require "fiddle"
kernel32 = Fiddle.dlopen("kernel32")
load_ex_a = Fiddle::Function.new(kernel32["LoadLibraryExA"],
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG],
Fiddle::TYPE_VOIDP)
free_lib = Fiddle::Function.new(kernel32["FreeLibrary"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_LONG)
last_err = Fiddle::Function.new(kernel32["GetLastError"], [], Fiddle::TYPE_LONG)
host_dir = Dir.glob(File.join(cache, "tebako-dl-*", "A_", "probe", "gemhome", "gems",
"sassc-2.4.0", "lib", "sassc")).first
if host_dir.nil?
puts "PROBE-DIAG fiddle skipped (no tebako-dl cache under #{cache})"
return
k32 = Module.new do
extend FFI::Library
ffi_lib "kernel32"
attach_function :load_ex, :LoadLibraryExA, %i[string pointer ulong], :pointer
attach_function :free_lib, :FreeLibrary, [:pointer], :int
attach_function :last_err, :GetLastError, [], :ulong
end
paths = sassc_module_paths
# The glob spelling, see materialized_styles_dir (backslash escapes).
cache = ENV["TEBAKO_EXEC_CACHE"].to_s.tr('\\', '/')
host_dir = cache.empty? ? nil : Dir.glob(File.join(cache, "tebako-dl-*", "A_", "probe", "gemhome", "gems",
"sassc-*", "lib", "sassc")).first
legs = []
[["default-order-NEGCTL", 0x0], ["altered", 0x8], ["search-default+dll-dir", 0x1100]].each do |fname, fl|
legs << ["libsass.so:vfs:#{fname}", paths["libsass.so"], fl]
legs << ["libsass.so:host:#{fname}", host_dir && File.join(host_dir, "libsass.so"), fl]
end
# libsass's three flag legs first (the search-order question), then the
# siblings' solo binds under the ffi-equivalent flag (the which-dep
# question). A succeeded load is freed immediately so no leg poisons
# the next via the loader's already-loaded table.
matrix = [["libsass.so:default-order-NEGCTL", File.join(host_dir, "libsass.so"), 0x0],
["libsass.so:altered", File.join(host_dir, "libsass.so"), 0x8],
["libsass.so:search-default+dll-dir", File.join(host_dir, "libsass.so"), 0x1100]]
SASSC_MODULE_WANTS.each_key { |mod| matrix << ["#{mod}:altered", File.join(host_dir, mod), 0x8] }
matrix.each do |label, host_path, fl|
unless File.exist?(host_path)
puts "PROBE-DIAG fiddle-load #{label} missing-on-host #{host_path}"
%w[libwinpthread-1.dll libgcc_s_seh-1.dll].each do |mod|
legs << ["#{mod}:vfs:altered", paths[mod], 0x8]
legs << ["#{mod}:host:altered", host_dir && File.join(host_dir, mod), 0x8]
end
legs.each do |label, spell, fl|
if spell.nil?
puts "PROBE-DIAG raw-load #{label} skipped (no spelling)"
next
end
h = load_ex_a.call(Fiddle::Pointer[host_path], nil, fl)
if h.nil? || h.zero?
puts "PROBE-DIAG fiddle-load #{label} fail os-err=#{last_err.call}"
h = k32.load_ex(spell, nil, fl)
if h.nil? || h.null?
puts "PROBE-DIAG raw-load #{label} fail os-err=#{k32.last_err}"
else
puts "PROBE-DIAG fiddle-load #{label} ok"
free_lib.call(h)
puts "PROBE-DIAG raw-load #{label} ok"
k32.free_lib(h)
end
rescue StandardError => fe
puts "PROBE-DIAG fiddle-load #{label} error #{fe.class}: #{fe.message.lines.first.to_s.strip}"
rescue LoadError, StandardError => fe
puts "PROBE-DIAG raw-load #{label} error #{fe.class}: #{fe.message.lines.first.to_s.strip}"
end
end

# The diag sheet, shared by the bisect rescue (post-LoadError state) and
# the sassc-matrix leg (pristine state). ffi must load for the sheet to
# exist at all — name it and bail when even that fails.
def run_sassc_sheet
begin
require "ffi"
rescue LoadError => le
puts "PROBE-DIAG sheet aborted: ffi itself does not load (#{le.message.lines.first.to_s.strip})"
return
end
rescue StandardError => fe
puts "PROBE-DIAG fiddle aborted #{fe.class}: #{fe.message.lines.first.to_s.strip}"
sassc_sha256_legs
sassc_ffi_load_legs
sassc_raw_matrix
end

def require_sassc_with_bisect
require "sassc"
rescue LoadError => e
sassc_sha256_legs
sassc_ffi_load_legs
sassc_fiddle_matrix
run_sassc_sheet
raise e
end

Expand Down Expand Up @@ -377,7 +419,18 @@ def require_sassc_with_bisect
rescue Exception => e # rubocop:disable Lint/RescueException -- the probe must see every failure mode
puts "PROBE sassc-partial-unmaterialized expected-fail #{e.class}: #{e.message.lines.first.to_s.strip}"
end
when "sassc-matrix"
# Forensics WITHOUT the require attempt (pristine loader state): the
# full diag sheet, never gates. windows-only — the raw-loader matrix
# measures the windows loader's own semantics; POSIX has no such
# question (dlopen serves the covered route directly).
if Gem.win_platform?
run_sassc_sheet
puts "PROBE sassc-matrix done"
else
puts "PROBE sassc-matrix skipped (posix)"
end
else
warn "probe.rb: unknown leg #{ARGV[0].inspect} (sinatra-fixed|sinatra-unfixed|sassc|sassc-unmaterialized)"
warn "probe.rb: unknown leg #{ARGV[0].inspect} (sinatra-fixed|sinatra-unfixed|sassc|sassc-unmaterialized|sassc-matrix)"
exit 64
end
Loading