From 1e4f62bd255a1dd435a56db012821dc5dacf5f69 Mon Sep 17 00:00:00 2001 From: Oaksprout Date: Wed, 29 Jul 2026 11:47:11 +0100 Subject: [PATCH] Fix Integer#size-derived width constants on LLP64 (Windows) [#1057] Integer#size reports the C `long` byte width, which is 4 on 64-bit Windows (LLP64) even though pointers are 8 bytes there, so every 0.size-derived width constant in the gem was wrong on that platform. Four sites, all switched to RbConfig::SIZEOF['void*']: - Utility::NativeInteger MIN_VALUE/MAX_VALUE (fixnum bounds used by MutexAtomicFixnum, MutexSemaphore, MutexCountDownLatch, CyclicBarrier) - ThreadSafe::Util::FIXNUM_BIT_SIZE / MAX_INT - ThreadSafe::Util::XorShiftRandom's transform-selection branch On LP64 platforms (CRuby elsewhere, JRuby, TruffleRuby) this is a no-op: RbConfig::SIZEOF['void*'] equals the prior 0.size-derived value. On Win64 the bounds become correct (2**62-1 instead of 2**30-1) and the xorshift PRNG uses its intended 64-bit transform. New spec/concurrent/utility/native_integer_spec.rb asserts literal expected values keyed on RbConfig::SIZEOF['void*'] rather than recomputing them from the same formula under test, so a reversion is caught by CI's Windows job instead of passing self-referentially, plus a MutexAtomicFixnum consumer-level regression for the same bug. --- CHANGELOG.md | 2 + .../concurrent/thread_safe/util.rb | 6 +- .../thread_safe/util/xor_shift_random.rb | 5 +- .../concurrent/utility/native_integer.rb | 8 ++- .../concurrent/utility/native_integer_spec.rb | 57 +++++++++++++++++++ 5 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 spec/concurrent/utility/native_integer_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6461846ee..d282bb769 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Current +* (#1057) Fix `Integer#size`-derived width constants (`Utility::NativeInteger` bounds, `ThreadSafe::Util::FIXNUM_BIT_SIZE`/`MAX_INT`, `XorShiftRandom`'s transform selection) being wrong on 64-bit Windows (LLP64), where `Integer#size` reports the 4-byte C `long` width instead of the 8-byte pointer width. + ## Release v1.3.8 (19 July 2026) concurrent-ruby: diff --git a/lib/concurrent-ruby/concurrent/thread_safe/util.rb b/lib/concurrent-ruby/concurrent/thread_safe/util.rb index c67084a26..b0b519dfa 100644 --- a/lib/concurrent-ruby/concurrent/thread_safe/util.rb +++ b/lib/concurrent-ruby/concurrent/thread_safe/util.rb @@ -1,3 +1,5 @@ +require 'rbconfig/sizeof' + module Concurrent # @!visibility private @@ -7,7 +9,9 @@ module ThreadSafe module Util # TODO (pitr-ch 15-Oct-2016): migrate to Utility::NativeInteger - FIXNUM_BIT_SIZE = (0.size * 8) - 2 + # Use the pointer width, not Integer#size (the C `long` width), since + # the two diverge on LLP64 platforms (64-bit Windows) - see #1057. + FIXNUM_BIT_SIZE = (RbConfig::SIZEOF['void*'] * 8) - 2 MAX_INT = (2 ** FIXNUM_BIT_SIZE) - 1 # TODO (pitr-ch 15-Oct-2016): migrate to Utility::ProcessorCounter CPU_COUNT = 16 # is there a way to determine this? diff --git a/lib/concurrent-ruby/concurrent/thread_safe/util/xor_shift_random.rb b/lib/concurrent-ruby/concurrent/thread_safe/util/xor_shift_random.rb index c231d182c..65da42d4e 100644 --- a/lib/concurrent-ruby/concurrent/thread_safe/util/xor_shift_random.rb +++ b/lib/concurrent-ruby/concurrent/thread_safe/util/xor_shift_random.rb @@ -1,4 +1,5 @@ require 'concurrent/thread_safe/util' +require 'rbconfig/sizeof' module Concurrent @@ -29,7 +30,9 @@ def get end # xorshift based on: http://www.jstatsoft.org/v08/i14/paper - if 0.size == 4 + # Use the pointer width, not Integer#size (the C `long` width), since + # the two diverge on LLP64 platforms (64-bit Windows) - see #1057. + if RbConfig::SIZEOF['void*'] == 4 # using the "yˆ=y>>a; yˆ=y<>c;" transform with the (a,b,c) tuple with values (3,1,14) to minimise Bignum overflows def xorshift(x) x ^= x >> 3 diff --git a/lib/concurrent-ruby/concurrent/utility/native_integer.rb b/lib/concurrent-ruby/concurrent/utility/native_integer.rb index de1cdc306..60ffac272 100644 --- a/lib/concurrent-ruby/concurrent/utility/native_integer.rb +++ b/lib/concurrent-ruby/concurrent/utility/native_integer.rb @@ -1,11 +1,15 @@ +require 'rbconfig/sizeof' + module Concurrent # @!visibility private module Utility # @private module NativeInteger # http://stackoverflow.com/questions/535721/ruby-max-integer - MIN_VALUE = -(2**(0.size * 8 - 2)) - MAX_VALUE = (2**(0.size * 8 - 2) - 1) + # Use the pointer width, not Integer#size (the C `long` width), since + # the two diverge on LLP64 platforms (64-bit Windows) - see #1057. + MIN_VALUE = -(2**(RbConfig::SIZEOF['void*'] * 8 - 2)) + MAX_VALUE = (2**(RbConfig::SIZEOF['void*'] * 8 - 2) - 1) def ensure_upper_bound(value) if value > MAX_VALUE diff --git a/spec/concurrent/utility/native_integer_spec.rb b/spec/concurrent/utility/native_integer_spec.rb new file mode 100644 index 000000000..2d3f31b39 --- /dev/null +++ b/spec/concurrent/utility/native_integer_spec.rb @@ -0,0 +1,57 @@ +require 'rbconfig/sizeof' +require 'concurrent/utility/native_integer' +require 'concurrent/thread_safe/util' +require 'concurrent/atomic/atomic_fixnum' + +# Regression for #1057: `Integer#size` reports the C `long` byte width, which +# is 4 on 64-bit Windows (LLP64) even though pointers are 8 bytes there, so +# every `0.size`-derived width constant used to be wrong on that platform. +# These specs assert literal values (not the bounds formula itself), so a +# reversion to `0.size`-derived bounds fails visibly on Win64 CI instead of +# passing self-referentially. +module Concurrent + + RSpec.describe Utility::NativeInteger do + if RbConfig::SIZEOF['void*'] == 8 + it 'derives a 62-bit signed bound from an 8-byte pointer width' do + expect(Utility::NativeInteger::MAX_VALUE).to eq 4611686018427387903 + expect(Utility::NativeInteger::MIN_VALUE).to eq(-4611686018427387904) + end + elsif RbConfig::SIZEOF['void*'] == 4 + it 'derives a 30-bit signed bound from a 4-byte pointer width' do + expect(Utility::NativeInteger::MAX_VALUE).to eq 1073741823 + expect(Utility::NativeInteger::MIN_VALUE).to eq(-1073741824) + end + end + end + + RSpec.describe MutexAtomicFixnum do + # Consumer-level regression: on Win64 the old `0.size`-derived bound + # (~1.07e9) rejected valid values well within a real Fixnum's range. + if RbConfig::SIZEOF['void*'] == 8 + it 'accepts a value beyond the old 32-bit-derived bound' do + expect { described_class.new(2**31) }.not_to raise_error + end + end + + it 'accepts Utility::NativeInteger::MAX_VALUE' do + expect { described_class.new(Utility::NativeInteger::MAX_VALUE) }.not_to raise_error + end + + it 'raises RangeError one past Utility::NativeInteger::MAX_VALUE' do + expect { described_class.new(Utility::NativeInteger::MAX_VALUE + 1) }.to raise_error(RangeError) + end + end + + module ThreadSafe + module Util + RSpec.describe 'ThreadSafe::Util::FIXNUM_BIT_SIZE' do + if RbConfig::SIZEOF['void*'] == 8 + it 'is 62 on an 8-byte pointer width' do + expect(FIXNUM_BIT_SIZE).to eq 62 + end + end + end + end + end +end