[Rd] Ncell GC trigger reaches SIZE_MAX after grow/shrink/regrow in AdjustHeapSize
Ivan Krylov
kry|ov@r00t @end|ng |rom gm@||@com
Mon Oct 19 22:44:36 CEST 2026
В Mon, 5 Oct 2026 01:43:31 -0400
Tyler <tylermw using gmail.com> пишет:
> I found a reproducible node-heap sizing bug in R 4.6.0 on arm64 macOS.
> After allocating and releasing a list, shrinking the heap with gc(),
> and allocating a smaller list, the Ncell GC trigger becomes
> approximately 1.844674e19, although fewer than one million Ncells are
> live.
Thank you for the report! I can also reproduce the R_NSize overflow on
Linux/amd64 when R is compiled using Clang, but not GCC. As you
mention, R builds with -fsanitize=undefined using both GCC and Clang
show a runtime error when the double -> size_t conversion overflows,
even if R_NSize stays fine.
R_size_t next_in_use = R_NodesInUse + (R_NodesInUse - last_in_use);
(gdb) p R_NodesInUse
$6 = 992417
(gdb) p last_in_use
$7 = 2332966
(gdb) p /x (R_NodesInUse - last_in_use)
$8 = 0xffffffffffeb8b7b
R_size_t next_nsize = (R_size_t) (next_in_use / R_NGrowFrac);
// ../../../src/main/memory.c:1204:52: runtime error: 2.63525e+19 is
// outside the range of representable values of type 'long unsigned int'
GCC compiles this as:
R_NGrowFrac -> xmm5
(double)PTRDIFF_MAX -> xmm1
; convert next_in_use to double accounting for the sign flag set
mov %rdx,%rdi
and $0x1,%edx
pxor %xmm0,%xmm0
shr %rdi
or %rdx,%rdi
cvtsi2sd %rdi,%xmm0
addsd %xmm0,%xmm0 ; (double)next_in_use -> xmm0
divsd %xmm5,%xmm0 ; xmm0 / xmm5 -> xmm0
(gdb) p $xmm0.v2_double[0]
$35 = 2.6352491533870297e+19
comisd %xmm1,%xmm0 ; result exceeds PTRDIFF_MAX? (yes)
...
subsd %xmm1,%xmm0 ; xmm0 - xmm1 -> xmm0
cvttsd2si %xmm0,%rdi ; failed conversion stores (1<<63) in rdi
btc $0x3f,%rdi ; flips bit 63 in rdi, leaving 0
; rdi -> next_nsize
(gdb) p /x next_nsize
$51 = 0x0
Clang compiles the same code as:
R_NGrowFrac -> xmm3
next_in_use -> r9
movq %r9,%xmm1 ; (double)r9 -> xmm1
punpckldq 0x101164(%rip),%xmm1
subpd 0x10116c(%rip),%xmm1
movapd %xmm1,%xmm2 ; xmm1 -> xmm2
unpckhpd %xmm1,%xmm2
addsd %xmm1,%xmm2
divsd %xmm3,%xmm2 ; xmm2 / xmm3 -> xmm2
(gdb) p $xmm2.v2_double[0]
$32 = 2.6352491533870293e+19
cvttsd2si %xmm2,%rsi ; failed conversion stores (1<<63) in rsi
mov %rsi,%r9
sar $0x3f,%r9 ; account for the sign bit?
subsd 0x1011f0(%rip),%xmm2
cvttsd2si %xmm2,%r10 ; stores PTRDIFF_MAX in r10
and %r9,%r10
or %rsi,%r10 ; r10 -> next_nsize
(gdb) p /x next_nsize
$46 = 0x8000000000000000
Thus the branch
if (next_nsize > R_NSize + change)
change = next_nsize - R_NSize;
is taken on Clang but not on GCC.
I think that your patch is sufficient to avoid the next_in_use overflow
and the resulting invalid double -> size_t conversion.
--
Best regards,
Ivan
More information about the R-devel
mailing list