Project

General

Profile

Bug #20662 » showbug.rb

Program for demonstrating the bugs - cabo (Carsten Bormann), 08/02/2024 03:16 PM

 
# pack('g')/unpack('g') single-precision NaN bug check
# Carsten Bormann cabo@tzi.org 2024-08-01
#
# Demonstrate that:
# - unpack("g") always sets the quiet bit to 1 in the Float result
# (location of bug not obvious to me)
# - pack("g") completely discards any actual NaN value and always packs the same bytes for a NaN
# ("bug as implemented" in VALUE_to_float)

class String
def hexs
bytes.map{|x| "%02x" % x}.join(" ")
end
end

def out(legend, *a)
puts legend + ": " + a.inspect
end

# The first three example suffice to show the problem
# The rest is cross-checking
EXAMPLES = [0x7fc00000, 0x7fffe000, 0x7fbfe000, 0x7ffff000, 0x7fbff000]
EXAMPLES.concat (EXAMPLES.map {|x| 0x80000000 | x}) # negative

def flip_upper_exponent_bit_first_byte(s)
s.setbyte(0, s.getbyte(0) ^ 0x40)
end

puts RUBY_DESCRIPTION
puts

EXAMPLES.each do |as_int|
as_bytes = [as_int].pack("N")
out "hex input for correct single NaN", "%x" % as_int, as_bytes.hexs

# Compute the NaN payload as a definite Float by flipping the upper exponent bit
# (Of course, unpack("g") works correctly with definite floats)

as_norm_bytes = [as_int ^ 0x40000000].pack("N")
out "- single nan payload normalized to 1.0..2.0", as_norm_bytes.hexs, as_norm_bytes.unpack("g").first

# For known good values, let's manually assemble the bit strings (knowing NaN exponent is all-ones)
# and cross-check (also checking pack('G') and unpack('G'))

single_bits = as_bytes.unpack("B32").first
double_bits = single_bits[0...9] + "1" * 3 + single_bits[9...32] + "0" * 29
correct_double_bytes = [double_bits].pack("B64")
correct_double_nan = correct_double_bytes.unpack("G").first
out "correct double_bytes, nan", correct_double_bytes.hexs, correct_double_nan

norm_correct_double_bytes = correct_double_bytes.dup
flip_upper_exponent_bit_first_byte(norm_correct_double_bytes)
norm_correct_double_nan = norm_correct_double_bytes.unpack("G").first
out "- correct double NaN payload normalized to 1.0..2.0", norm_correct_double_nan

cross_check_nan_bytes = [correct_double_nan].pack("G")
flip_upper_exponent_bit_first_byte(cross_check_nan_bytes)
norm_cross_check_double_nan = cross_check_nan_bytes.unpack("G").first
out "- double NaN payload normalized to 1.0..2.0, via pack('G')", norm_cross_check_double_nan

# Breakage 1: unpack("g") always sets the quiet bit to 1 in the Float result

unpacked_single_nan = as_bytes.unpack("g").first # broken -- sets quiet bit to 1
double_packed_unpacked_single_nan = [unpacked_single_nan].pack("G")

out "unpacked_single [unpack('g')].pack('G') output always quiet", unpacked_single_nan, double_packed_unpacked_single_nan.hexs

if double_packed_unpacked_single_nan != correct_double_bytes
out "- *** quieted value differs", double_packed_unpacked_single_nan.hexs, correct_double_bytes.hexs
end

# Breakage 2: pack("g") completely loses any NaN input and always packs the same bytes for a NaN

single_packed_unpacked_single_nan = [unpacked_single_nan].pack("g")

err = "for once, correct single NaN"
if single_packed_unpacked_single_nan != as_bytes
err = "*** the lost NaN -- output always the same"
end

out "#{err}: [unpack('g')].pack('g')", unpacked_single_nan, single_packed_unpacked_single_nan.hexs
puts
end
(1-1/3)