In the named-object variant of StretchyList::push_back(), the freshly allocated cons cell is neither protected nor reachable when the Symbol constructor runs Rf_install():
https://github.com/RcppCore/Rcpp/blob/master/inst/include/Rcpp/api/meat/StretchyList.h#L36-L45
StretchyList_Impl<StoragePolicy>& StretchyList_Impl<StoragePolicy>::push_back__impl( const T& obj, traits::true_type ){
Shield<SEXP> s( wrap(obj.object) ) ;
SEXP tmp = Rf_cons( s, R_NilValue );
Symbol tag = obj.name ; // Rf_install() -- can allocate and trigger GC
SET_TAG(tmp, tag) ;
SEXP self = Storage::get__() ;
SETCDR( CAR(self), tmp) ;
SETCAR( self, tmp ) ;
return *this ;
}
If the tag name is not yet interned, Rf_install() allocates (mkChar() plus the SYMSXP), which can trigger a garbage collection. At that point tmp is only referenced from the C stack, so it is collected; SET_TAG() then writes into a reclaimed node, and SETCDR() / SETCAR() link that reclaimed node into the (preserved) list.
Note that the named variant of push_front() in the same file gets the ordering right, constructing the Symbol before calling Rf_cons(), so only push_back() is affected.
Reproducible example
writeLines('
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
SEXP stretchy_push_back(std::string name) {
StretchyList out;
out.push_back(Named(name, 42));
return out;
}
', "repro.cpp")
Rcpp::sourceCpp("repro.cpp")
# a symbol name that has never been interned in this session, so that
# Rf_install() inside push_back() has to allocate
name <- paste(sample(c(letters, LETTERS), 32, TRUE), collapse = "")
gctorture(TRUE)
x <- stretchy_push_back(name)
gctorture(FALSE)
cat("typeof:", typeof(x), "\n")
str(x)
Output:
typeof: symbol
symbol oZoNOOQgnEmozfyTBxxQsXnamjjNqEYY
Under gctorture(), the GC triggered inside Rf_install() collects the cons cell, and its node is immediately reused for the newly created SYMSXP -- so the "pairlist" we get back is actually the symbol itself. Expected output would be a one-element pairlist, e.g.:
typeof: pairlist
Dotted pair list of 1
$ <name>: num 42
Depending on allocation timing this can also surface as corruption of unrelated objects (the reclaimed node can be reused for anything), so the symptom in real code would typically be a segfault or silently corrupted data rather than this tidy misclassification.
Observed with Rcpp master (3a9aeaa) on R 4.6.1.
A PR with a fix (reordering push_back__impl() to construct the Symbol before Rf_cons(), matching push_front__impl()) will follow shortly.
In the named-object variant of
StretchyList::push_back(), the freshly allocated cons cell is neither protected nor reachable when theSymbolconstructor runsRf_install():https://github.com/RcppCore/Rcpp/blob/master/inst/include/Rcpp/api/meat/StretchyList.h#L36-L45
If the tag name is not yet interned,
Rf_install()allocates (mkChar()plus the SYMSXP), which can trigger a garbage collection. At that pointtmpis only referenced from the C stack, so it is collected;SET_TAG()then writes into a reclaimed node, andSETCDR()/SETCAR()link that reclaimed node into the (preserved) list.Note that the named variant of
push_front()in the same file gets the ordering right, constructing theSymbolbefore callingRf_cons(), so onlypush_back()is affected.Reproducible example
Output:
Under
gctorture(), the GC triggered insideRf_install()collects the cons cell, and its node is immediately reused for the newly created SYMSXP -- so the "pairlist" we get back is actually the symbol itself. Expected output would be a one-element pairlist, e.g.:Depending on allocation timing this can also surface as corruption of unrelated objects (the reclaimed node can be reused for anything), so the symptom in real code would typically be a segfault or silently corrupted data rather than this tidy misclassification.
Observed with Rcpp master (3a9aeaa) on R 4.6.1.
A PR with a fix (reordering
push_back__impl()to construct theSymbolbeforeRf_cons(), matchingpush_front__impl()) will follow shortly.