From 86623ba3b613c55e9c92c129ed96f2437a11a493 Mon Sep 17 00:00:00 2001 From: Sean Dolbec Date: Mon, 2 Nov 2026 18:54:33 -0500 Subject: [PATCH 1/4] Update loofah, nokogiri and rails-html-sanitizer for new advisories bundler-audit on main fails on advisories published today against loofah 2.25.2, nokogiri 1.19.4 and rails-html-sanitizer 1.7.1. Conservative update of only those three gems to loofah 2.25.3, nokogiri 1.19.7 and rails-html-sanitizer 1.7.2. Co-Authored-By: Claude Opus 5.5 --- web/Gemfile.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/web/Gemfile.lock b/web/Gemfile.lock index 5b09a6d26..5c743f345 100644 --- a/web/Gemfile.lock +++ b/web/Gemfile.lock @@ -199,9 +199,9 @@ GEM language_server-protocol (3.17.0.6) lint_roller (1.1.0) logger (1.7.0) - loofah (2.25.2) + loofah (2.25.3) crass (~> 1.0.2) - nokogiri (>= 1.12.0) + nokogiri (>= 1.19.5) mail (2.9.1) logger mini_mime (>= 0.1.1) @@ -235,21 +235,21 @@ GEM net-smtp (0.5.2) net-protocol nio4r (2.7.5) - nokogiri (1.19.4-aarch64-linux-gnu) + nokogiri (1.19.7-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.4-aarch64-linux-musl) + nokogiri (1.19.7-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.19.4-arm-linux-gnu) + nokogiri (1.19.7-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.19.4-arm-linux-musl) + nokogiri (1.19.7-arm-linux-musl) racc (~> 1.4) - nokogiri (1.19.4-arm64-darwin) + nokogiri (1.19.7-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.4-x86_64-darwin) + nokogiri (1.19.7-x86_64-darwin) racc (~> 1.4) - nokogiri (1.19.4-x86_64-linux-gnu) + nokogiri (1.19.7-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.4-x86_64-linux-musl) + nokogiri (1.19.7-x86_64-linux-musl) racc (~> 1.4) nostr (0.7.0) bech32 (~> 1.4) @@ -319,8 +319,8 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.7.1) - loofah (~> 2.25, >= 2.25.2) + rails-html-sanitizer (1.7.2) + loofah (~> 2.25, >= 2.25.3) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) railties (8.1.4) actionpack (= 8.1.4) @@ -635,7 +635,7 @@ CHECKSUMS language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0 lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 - loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918 + loofah (2.25.3) sha256=545f14eef0cc2a7de80df560530d593232b7c07e515c5c4e37532d9cb5b0185c mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8 marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b @@ -653,14 +653,14 @@ CHECKSUMS net-protocol (0.4.0) sha256=f9f90c8c6f64794a45160bf422177a336db12809de3beeeaa26b88fcd2e8c853 net-smtp (0.5.2) sha256=35103cc04d6b2d94632f9928ede07483c24b70b9c0db1f231fea43909b678f3c nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 - nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f - nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af - nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca - nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb - nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527 - nokogiri (1.19.4-x86_64-darwin) sha256=7fd17057d3e1f00e9954a74b3cd76595d3d4a5ef233b7ed9599047c204f70551 - nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a - nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b + nokogiri (1.19.7-aarch64-linux-gnu) sha256=8f5e15d299602456406e1f03a9f3456cceb33a0722811e818d2ee2214eb4c84a + nokogiri (1.19.7-aarch64-linux-musl) sha256=efe0c3602e6e54521bc21c681930ce5568e2d11203de17400710b2b3ca2e4cb4 + nokogiri (1.19.7-arm-linux-gnu) sha256=7fd1907125691aa89e838cc91161761c758b4e17bb32f0dd9b5515a82cc759af + nokogiri (1.19.7-arm-linux-musl) sha256=28d0ff0c87b6bb2e56580323a6fb049d5d4a02b94f0958fdd47e9ac146991c77 + nokogiri (1.19.7-arm64-darwin) sha256=f27d7f25f4873fbc3aa35a2cc2fb680c98555fec1925348da439d0869d541142 + nokogiri (1.19.7-x86_64-darwin) sha256=508e81e8c5f84aaa240dce981bf1c43faae60808726f0ce7024ee92effc5cafe + nokogiri (1.19.7-x86_64-linux-gnu) sha256=25865ccb462f714e712fdf8906fde07849cb6704a808e3d629d75baa5af5dd3b + nokogiri (1.19.7-x86_64-linux-musl) sha256=464d1f3dfb0d53cf54c0c6a7e23e8e91cf68ec3cafc2b27dc4a7682f2d9bf546 nostr (0.7.0) sha256=2971efb01821857203d5e32569b3ab1bfa0f4904da4b9c1a7822112e904ccd90 oj (3.17.7) sha256=7a21d3ac8e544a850f1cdaf711b0c8144ba587d14d3e4917c0855fd7b0483db7 openssl (4.0.2) sha256=1037ad2868ae58df9ad917891c0c0f9815a1172f6846d4bcdd508e4c2ee747c2 @@ -688,7 +688,7 @@ CHECKSUMS rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 rails (8.1.4) sha256=5f3ecdf9a2a3b79b2e31b8fee3b64a00f7284d76e5631f87d0e58e665911a33b rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d - rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2 + rails-html-sanitizer (1.7.2) sha256=a21ef45b7a6639fc8e2282473c547514cb86ae271616162a946bf89148fa2424 railties (8.1.4) sha256=b5e7a8e8984e33114d68af62227971960aae1e6c02bd7c9d826ffad97fff583c rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 From 067deb59cd870887b6092c62958e635ddc0cf677 Mon Sep 17 00:00:00 2001 From: Sean Dolbec Date: Mon, 2 Nov 2026 18:55:04 -0500 Subject: [PATCH 2/4] Make the host vitals test portable to macOS The test read /proc/loadavg and /proc/meminfo directly, so it failed on a Mac. HostVitals now reads /proc through one small method that the test stubs with fixed contents, checking the exact CPU and memory percentages instead of only their types. Disk and database size still come from the real host. The real /proc read still runs where /proc exists (Linux CI). A new test pins that an unreadable reading is nil. Co-Authored-By: Claude Opus 5.5 --- web/app/services/host_vitals.rb | 9 +++++-- web/test/services/host_vitals_test.rb | 35 ++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/web/app/services/host_vitals.rb b/web/app/services/host_vitals.rb index f24e2aa1d..2dbd2ca47 100644 --- a/web/app/services/host_vitals.rb +++ b/web/app/services/host_vitals.rb @@ -68,14 +68,14 @@ def cgroup_cpu_usec # Without a cgroup (a laptop, say): the one-minute load average as a share of the cores. def load_percent - load1 = File.read('/proc/loadavg').split.first.to_f + load1 = read_host_file('/proc/loadavg').split.first.to_f [(load1 / Etc.nprocessors * 100).round, 100].min rescue SystemCallError, ZeroDivisionError nil end def memory_percent - info = File.read('/proc/meminfo').lines.to_h do |line| + info = read_host_file('/proc/meminfo').lines.to_h do |line| key, value = line.split(':') [key, value.to_i] end @@ -86,6 +86,11 @@ def memory_percent nil end + # Linux-only /proc files; tests stub this so the parsing is checked on any host. + def read_host_file(path) + File.read(path) + end + # Use of the filesystem holding the database, as df reports it. def disk_percent output = IO.popen(['df', '-Pk', database_path.dirname.to_s], err: File::NULL, &:read) diff --git a/web/test/services/host_vitals_test.rb b/web/test/services/host_vitals_test.rb index 34d2db15a..6b96338d0 100644 --- a/web/test/services/host_vitals_test.rb +++ b/web/test/services/host_vitals_test.rb @@ -3,12 +3,41 @@ require 'test_helper' class HostVitalsTest < ActiveSupport::TestCase - test 'reads load, memory, disk and database size from this host' do + # /proc is Linux-only, so these readings are stubbed and the parsing is checked on any host (CI or a Mac). + PROC_FILES = { + '/proc/loadavg' => "1.50 0.80 0.40 2/345 6789\n", + '/proc/meminfo' => "MemTotal: 16000000 kB\nMemFree: 2000000 kB\nMemAvailable: 4000000 kB\n" + }.freeze + + test 'reads load, memory, disk and database size' do + HostVitals.stub(:cgroup_cpu_usec, nil) do + HostVitals.stub(:read_host_file, ->(path) { PROC_FILES.fetch(path) }) do + Etc.stub(:nprocessors, 4) do + reading = HostVitals.read + assert_equal 38, reading.cpu, 'one-minute load 1.5 on 4 cores' + assert_equal 75, reading.memory, '12 of 16 GB in use' + assert reading.disk.between?(0, 100) + assert reading.db_bytes.positive? + end + end + end + end + + test 'a reading this host cannot take is nil, never 0' do + HostVitals.stub(:cgroup_cpu_usec, nil) do + HostVitals.stub(:read_host_file, ->(path) { raise Errno::ENOENT, path }) do + reading = HostVitals.read + assert_nil reading.cpu + assert_nil reading.memory + end + end + end + + test 'reads CPU and memory from a real Linux host' do + skip 'needs Linux /proc' unless File.readable?('/proc/meminfo') reading = HostVitals.read assert_kind_of Integer, reading.cpu assert reading.memory.between?(0, 100) - assert reading.disk.between?(0, 100) - assert reading.db_bytes.positive? end test 'CPU is this container\'s own CPU time, not the host load average' do From 50ad0d16181984182d79aad2f91496de2b50c162 Mon Sep 17 00:00:00 2001 From: Sean Dolbec Date: Mon, 2 Nov 2026 18:55:49 -0500 Subject: [PATCH 3/4] Read the instance label as UTF-8 under any locale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under a C/POSIX locale (a shell without LANG, as on some Macs) Ruby returns ENV values as binary, so a label such as "SER8 ยท Incus" did not equal the UTF-8 string and the platform overview test failed. The label is now read as UTF-8, and a value that is not valid UTF-8 shows as "Not configured" instead of raising. Co-Authored-By: Claude Opus 5.5 --- web/app/services/instance_overview.rb | 6 +++++- web/test/integration/platform_test.rb | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/app/services/instance_overview.rb b/web/app/services/instance_overview.rb index 2854566f4..5f7a97ecb 100644 --- a/web/app/services/instance_overview.rb +++ b/web/app/services/instance_overview.rb @@ -94,7 +94,11 @@ def instance end def deployment_label - value = ENV['RUPERT_INSTANCE_LABEL'].to_s.strip + # Read as UTF-8 whatever the server's locale; under a C/POSIX locale ENV values arrive as binary. + value = ENV['RUPERT_INSTANCE_LABEL'].to_s.dup.force_encoding(Encoding::UTF_8) + return 'Not configured' unless value.valid_encoding? + + value = value.strip value.present? && value.length <= 80 && !value.match?(/[\x00-\x1f\x7f]/) ? value : 'Not configured' end diff --git a/web/test/integration/platform_test.rb b/web/test/integration/platform_test.rb index 06d1b45c7..716b18815 100644 --- a/web/test/integration/platform_test.rb +++ b/web/test/integration/platform_test.rb @@ -206,6 +206,8 @@ class PlatformTest < ActionDispatch::IntegrationTest %w[PRIVATE_SYNC_DETAILS PRIVATE_BACKUP_FILENAME PRIVATE_BACKUP_LINK].each { |value| refute_includes response.body, value } ENV['RUPERT_INSTANCE_LABEL'] = "Invalid\nlabel" assert_equal 'Not configured', InstanceOverview.call(actor: @owner)[:instance][:label] + ENV['RUPERT_INSTANCE_LABEL'] = "Not UTF-8 \xFF".b + assert_equal 'Not configured', InstanceOverview.call(actor: @owner)[:instance][:label] ensure ENV['RUPERT_INSTANCE_LABEL'] = old_label end From 9a7edfbee55dda939ec969a1a0f8ed042b9ec7b2 Mon Sep 17 00:00:00 2001 From: Sean Dolbec Date: Mon, 2 Nov 2026 19:04:21 -0500 Subject: [PATCH 4/4] Give each card reconciliation test its own job lock file PosPaymentReconciliationJob takes one machine-wide flock and skips its sweep when the lock is held. Parallel test workers shared that file, so when two PosCardPaymentTest cases ran the job at the same moment one silently did nothing. That intermittently failed the overlap test (line 72) and the rotation test (line 56) on main and on several PRs. The lock path is now one small method, and the tests point it at a temporary file per test. Holding the real lock from another process reproduces both failures on the old code, and they pass on the new. Production still uses tmp/locks/pos-payment-reconciliation.lock. Co-Authored-By: Claude Opus 5.5 --- web/app/jobs/pos_payment_reconciliation_job.rb | 9 ++++++--- web/test/services/pos_card_payment_test.rb | 8 +++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/web/app/jobs/pos_payment_reconciliation_job.rb b/web/app/jobs/pos_payment_reconciliation_job.rb index c821d542e..2cd4522dd 100644 --- a/web/app/jobs/pos_payment_reconciliation_job.rb +++ b/web/app/jobs/pos_payment_reconciliation_job.rb @@ -7,9 +7,8 @@ def perform # SQLite deployments share one filesystem. An advisory process lock keeps # a slow gateway request from overlapping a later scheduled sweep; it is # released automatically if the worker exits. - directory = Rails.root.join('tmp/locks') - FileUtils.mkdir_p(directory) - File.open(directory.join('pos-payment-reconciliation.lock'), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock| + FileUtils.mkdir_p(lock_path.dirname) + File.open(lock_path, File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock| return unless lock.flock(File::LOCK_EX | File::LOCK_NB) reconcile_batch @@ -20,6 +19,10 @@ def perform private + def lock_path + Rails.root.join('tmp/locks/pos-payment-reconciliation.lock') + end + def reconcile_batch PosPaymentAttempt.unscoped.joins(:tenant).where(tenants: { status: 'active' }) .where(status: %w[submitting unknown]).where('submitted_at < ?', 1.minute.ago) diff --git a/web/test/services/pos_card_payment_test.rb b/web/test/services/pos_card_payment_test.rb index 68296ec0f..9308b506b 100644 --- a/web/test/services/pos_card_payment_test.rb +++ b/web/test/services/pos_card_payment_test.rb @@ -3,6 +3,9 @@ require 'test_helper' class PosCardPaymentTest < ActiveSupport::TestCase setup do + # Parallel test workers would otherwise share the job's one lock file, and a job that finds it held skips its sweep. + @lock_dir = Dir.mktmpdir('pos-reconciliation-lock') + PosPaymentReconciliationJob.any_instance.stubs(:lock_path).returns(Pathname(@lock_dir).join('reconciliation.lock')) Current.tenant = tenants(:default_tenant) @location = Location.create!(source: 'ctrax', externalId: 'card-fixture/1', name: 'Card fixture') @user = users(:regular_user) @@ -40,7 +43,10 @@ class PosCardPaymentTest < ActiveSupport::TestCase { status: 200, headers: { 'Content-Type' => 'application/json' }, body: data.merge(messages: { resultCode: 'Ok' }).to_json } end end - teardown { Current.reset } + teardown do + Current.reset + FileUtils.rm_rf(@lock_dir) + end test 'reconciliation rotates past unresolved attempts without sending charges' do 51.times do |i|