Skip to content

Commit d50ade6

Browse files
committed
SF bug 705836: struct.pack of floats in non-native endian order
pack_float, pack_double, save_float: All the routines for creating IEEE-format packed representations of floats and doubles simply ignored that rounding can (in rare cases) propagate out of a long string of 1 bits. At worst, the end-off carry can (by mistake) interfere with the exponent value, and then unpacking yields a result wrong by a factor of 2. In less severe cases, it can end up losing more low-order bits than intended, or fail to catch overflow *caused* by rounding. Bugfix candidate, but I already backported this to 2.2. In 2.3, this code remains in severe need of refactoring.
1 parent 62364ff commit d50ade6

4 files changed

Lines changed: 115 additions & 18 deletions

File tree

Lib/test/test_struct.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,3 +390,49 @@ def test_p_code():
390390
(code, input, got, expectedback))
391391

392392
test_p_code()
393+
394+
395+
###########################################################################
396+
# SF bug 705836. "<f" and ">f" had a severe rounding bug, where a carry
397+
# from the low-order discarded bits could propagate into the exponent
398+
# field, causing the result to be wrong by a factor of 2.
399+
400+
def test_705836():
401+
import math
402+
403+
for base in range(1, 33):
404+
# smaller <- largest representable float less than base.
405+
delta = 0.5
406+
while base - delta / 2.0 != base:
407+
delta /= 2.0
408+
smaller = base - delta
409+
# Packing this rounds away a solid string of trailing 1 bits.
410+
packed = struct.pack("<f", smaller)
411+
unpacked = struct.unpack("<f", packed)[0]
412+
# This failed at base = 2, 4, and 32, with unpacked = 1, 2, and
413+
# 16, respectively.
414+
verify(base == unpacked)
415+
bigpacked = struct.pack(">f", smaller)
416+
verify(bigpacked == string_reverse(packed),
417+
">f pack should be byte-reversal of <f pack")
418+
unpacked = struct.unpack(">f", bigpacked)[0]
419+
verify(base == unpacked)
420+
421+
# Largest finite IEEE single.
422+
big = (1 << 24) - 1
423+
big = math.ldexp(big, 127 - 23)
424+
packed = struct.pack(">f", big)
425+
unpacked = struct.unpack(">f", packed)[0]
426+
verify(big == unpacked)
427+
428+
# The same, but tack on a 1 bit so it rounds up to infinity.
429+
big = (1 << 25) - 1
430+
big = math.ldexp(big, 127 - 24)
431+
try:
432+
packed = struct.pack(">f", big)
433+
except OverflowError:
434+
pass
435+
else:
436+
TestFailed("expected OverflowError")
437+
438+
test_705836()

Misc/NEWS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ Core and builtins
3737
Extension modules
3838
-----------------
3939

40+
- The platform-independent routines for packing floats in IEEE formats
41+
(struct.pack's <f, >f, <d, and >d codes; pickle and cPickle's protocol 1
42+
pickling of floats) ignored that rounding can cause a carry to
43+
propagate. The worst consequence was that, in rare cases, <f and >f
44+
could produce strings that, when unpacked again, were a factor of 2
45+
away from the original float. This has been fixed. See SF bug
46+
#705836.
47+
4048
- New function time.tzset() provides access to the C library tzet()
4149
function, if supported. (SF patch #675422.)
4250

Modules/cPickle.c

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,12 +1156,8 @@ save_float(Picklerobject *self, PyObject *args)
11561156
return -1;
11571157
}
11581158

1159-
if (e >= 1024) {
1160-
/* XXX 1024 itself is reserved for Inf/NaN */
1161-
PyErr_SetString(PyExc_OverflowError,
1162-
"float too large to pack with d format");
1163-
return -1;
1164-
}
1159+
if (e >= 1024)
1160+
goto Overflow;
11651161
else if (e < -1022) {
11661162
/* Gradual underflow */
11671163
f = ldexp(f, 1022 + e);
@@ -1176,9 +1172,26 @@ save_float(Picklerobject *self, PyObject *args)
11761172
flo the low 24 bits (== 52 bits) */
11771173
f *= 268435456.0; /* 2**28 */
11781174
fhi = (long) floor(f); /* Truncate */
1175+
assert(fhi < 268435456);
1176+
11791177
f -= (double)fhi;
11801178
f *= 16777216.0; /* 2**24 */
11811179
flo = (long) floor(f + 0.5); /* Round */
1180+
assert(flo <= 16777216);
1181+
if (flo >> 24) {
1182+
/* The carry propagated out of a string of 24 1 bits. */
1183+
flo = 0;
1184+
++fhi;
1185+
if (fhi >> 28) {
1186+
/* And it also progagated out of the next
1187+
* 28 bits.
1188+
*/
1189+
fhi = 0;
1190+
++e;
1191+
if (e >= 2047)
1192+
goto Overflow;
1193+
}
1194+
}
11821195

11831196
/* First byte */
11841197
*p = (s<<7) | (e>>4);
@@ -1224,6 +1237,11 @@ save_float(Picklerobject *self, PyObject *args)
12241237
}
12251238

12261239
return 0;
1240+
1241+
Overflow:
1242+
PyErr_SetString(PyExc_OverflowError,
1243+
"float too large to pack with d format");
1244+
return -1;
12271245
}
12281246

12291247

Modules/structmodule.c

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,8 @@ pack_float(double x, /* The number to pack */
224224
return -1;
225225
}
226226

227-
if (e >= 128) {
228-
/* XXX 128 itself is reserved for Inf/NaN */
229-
PyErr_SetString(PyExc_OverflowError,
230-
"float too large to pack with f format");
231-
return -1;
232-
}
227+
if (e >= 128)
228+
goto Overflow;
233229
else if (e < -126) {
234230
/* Gradual underflow */
235231
f = ldexp(f, 126 + e);
@@ -242,6 +238,14 @@ pack_float(double x, /* The number to pack */
242238

243239
f *= 8388608.0; /* 2**23 */
244240
fbits = (long) floor(f + 0.5); /* Round */
241+
assert(fbits <= 8388608);
242+
if (fbits >> 23) {
243+
/* The carry propagated out of a string of 23 1 bits. */
244+
fbits = 0;
245+
++e;
246+
if (e >= 255)
247+
goto Overflow;
248+
}
245249

246250
/* First byte */
247251
*p = (s<<7) | (e>>1);
@@ -260,6 +264,11 @@ pack_float(double x, /* The number to pack */
260264

261265
/* Done */
262266
return 0;
267+
268+
Overflow:
269+
PyErr_SetString(PyExc_OverflowError,
270+
"float too large to pack with f format");
271+
return -1;
263272
}
264273

265274
static int
@@ -295,12 +304,8 @@ pack_double(double x, /* The number to pack */
295304
return -1;
296305
}
297306

298-
if (e >= 1024) {
299-
/* XXX 1024 itself is reserved for Inf/NaN */
300-
PyErr_SetString(PyExc_OverflowError,
301-
"float too large to pack with d format");
302-
return -1;
303-
}
307+
if (e >= 1024)
308+
goto Overflow;
304309
else if (e < -1022) {
305310
/* Gradual underflow */
306311
f = ldexp(f, 1022 + e);
@@ -314,9 +319,24 @@ pack_double(double x, /* The number to pack */
314319
/* fhi receives the high 28 bits; flo the low 24 bits (== 52 bits) */
315320
f *= 268435456.0; /* 2**28 */
316321
fhi = (long) floor(f); /* Truncate */
322+
assert(fhi < 268435456);
323+
317324
f -= (double)fhi;
318325
f *= 16777216.0; /* 2**24 */
319326
flo = (long) floor(f + 0.5); /* Round */
327+
assert(flo <= 16777216);
328+
if (flo >> 24) {
329+
/* The carry propagated out of a string of 24 1 bits. */
330+
flo = 0;
331+
++fhi;
332+
if (fhi >> 28) {
333+
/* And it also progagated out of the next 28 bits. */
334+
fhi = 0;
335+
++e;
336+
if (e >= 2047)
337+
goto Overflow;
338+
}
339+
}
320340

321341
/* First byte */
322342
*p = (s<<7) | (e>>4);
@@ -352,6 +372,11 @@ pack_double(double x, /* The number to pack */
352372

353373
/* Done */
354374
return 0;
375+
376+
Overflow:
377+
PyErr_SetString(PyExc_OverflowError,
378+
"float too large to pack with d format");
379+
return -1;
355380
}
356381

357382
static PyObject *

0 commit comments

Comments
 (0)