Skip to content

Commit d1674cf

Browse files
authored
Merge pull request #859 from lionel-/restore-unwind
Fix protect-unwind
2 parents 6886a43 + a8c5463 commit d1674cf

File tree

24 files changed

+509
-129
lines changed

24 files changed

+509
-129
lines changed

ChangeLog

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,71 @@
1+
2+
2018-06-02 Lionel Henry <[email protected]>
3+
4+
* inst/unitTests/runit.interface.R: New test for the case where
5+
the client package was compiled without protected evaluation
6+
enabled. On R 3.5, longjump exceptions thrown from imported
7+
functions are still caught and dealt with properly by the client
8+
package.
9+
10+
* inst/unitTests/runit.interface.R: Test both Rcpp_eval() and
11+
Rcpp_fast_eval().
12+
13+
2018-06-01 Lionel Henry <[email protected]>
14+
15+
* inst/unitTests/runit.interface.R: New tests for interfaces and unwind.
16+
These tests build two packages, and that exports a function via
17+
Rcpp::interfaces(cpp) and the other that calls it. The attributes are
18+
regenerated and the packages rebuilt each time the tests are run. The
19+
tests check in particular that the C++ stack is properly unwound when a
20+
long jump occurs.
21+
22+
2018-05-31 Lionel Henry <[email protected]>
23+
24+
* inst/include/Rcpp/api/meat/Rcpp_eval.h: Fix protected evaluation.
25+
26+
Setting `RCPP_PROTECTED_EVAL` before including Rcpp.h enables a new R
27+
3.5 API for safe evaluation of R code. R longjumps are now correctly
28+
intercepted and rethrown. Thanks to this the C++ stack is now safely
29+
unwound when a longjump is detected while calling into R code. This
30+
includes the following cases: thrown errors, caught condition of any
31+
class, long return, restart invokation, debugger exit. Note that this is
32+
still experimental!
33+
34+
When `RCPP_PROTECTED_EVAL` is enabled, Rcpp_eval() uses the
35+
protect-unwind API under the hood in order to gain safety. It is fully
36+
backward-compatibile and still catches errors and interrupts to rethrow
37+
them as typed C++ exceptions. If you don't need to catch those, consider
38+
using Rcpp_fast_eval() instead to avoid the catching overhead.
39+
40+
Rcpp_fast_eval() is a wrapper around Rf_eval(). Unlike Rcpp_eval(), it
41+
does not evaluate R code within tryCatch() and thus avoids the overhead
42+
of wrapping and evaluating the expression in a tryCatch() call. When
43+
Rcpp is compiled with a lower version than R 3.5, Rcpp_fast_eval() falls
44+
back to Rf_eval() without any protection from long jumps, even when
45+
`RCPP_PROTECTED_EVAL` is set. Either add R 3.5 to your `Depends` or make
46+
sure the legacy Rcpp_eval() function is called instead of Rcpp_fast_eval()
47+
when your package is compiled with an older version of R.
48+
49+
Note that Rcpp_fast_eval() behaves a bit differently to Rcpp_eval(). The
50+
former has the semantics of the C function Rf_eval() whereas the latter
51+
behaves like the R function base::eval(). This has subtle implications
52+
for control flow. For instance evaluating a return() expression within a
53+
frame environment returns from that frame rather than from the
54+
Rcpp_eval() call.
55+
56+
* inst/include/Rcpp/macros/macros.h: Leave the try/catch scope before
57+
resuming jump to ensure proper destruction of the exception reference.
58+
59+
* inst/include/Rcpp/exceptions.h: Functions to create and check a
60+
longjump sentinel. This sentinel is used as return value in contexts
61+
where it is not safe to resume a jump (i.e. in the glue code of cpp
62+
interfaces).
63+
64+
* inst/include/Rcpp/macros/macros.h: Return a longjump sentinel in
65+
END_RCPP_RETURN_ERROR.
66+
67+
* src/attributes.cpp: Detect longjump sentinels and resume jump.
68+
169
2018-05-09 Dirk Eddelbuettel <[email protected]>
270

371
* DESCRIPTION: Release 0.12.17

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: Rcpp
22
Title: Seamless R and C++ Integration
3-
Version: 0.12.17
3+
Version: 0.12.17.1
44
Date: 2018-05-09
55
Author: Dirk Eddelbuettel, Romain Francois, JJ Allaire, Kevin Ushey, Qiang Kou,
66
Nathan Russell, Douglas Bates and John Chambers

inst/include/Rcpp/Environment.h

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,7 @@ namespace Rcpp{
109109

110110
/* We need to evaluate if it is a promise */
111111
if( TYPEOF(res) == PROMSXP){
112-
#if defined(RCPP_USE_UNWIND_PROTECT)
113-
res = internal::Rcpp_eval_impl(res, env);
114-
#else
115-
res = Rf_eval(res, env);
116-
#endif
112+
res = internal::Rcpp_eval_impl( res, env ) ;
117113
}
118114
return res ;
119115
}
@@ -133,11 +129,7 @@ namespace Rcpp{
133129

134130
/* We need to evaluate if it is a promise */
135131
if( TYPEOF(res) == PROMSXP){
136-
#if defined(RCPP_USE_UNWIND_PROTECT)
137-
res = internal::Rcpp_eval_impl(res, env);
138-
#else
139-
res = Rf_eval(res, env);
140-
#endif
132+
res = internal::Rcpp_eval_impl( res, env ) ;
141133
}
142134
return res ;
143135
}
@@ -159,11 +151,7 @@ namespace Rcpp{
159151

160152
/* We need to evaluate if it is a promise */
161153
if( TYPEOF(res) == PROMSXP){
162-
#if defined(RCPP_USE_UNWIND_PROTECT)
163-
res = internal::Rcpp_eval_impl(res, env);
164-
#else
165-
res = Rf_eval(res, env);
166-
#endif
154+
res = internal::Rcpp_eval_impl( res, env ) ;
167155
}
168156
return res ;
169157
}
@@ -186,11 +174,7 @@ namespace Rcpp{
186174

187175
/* We need to evaluate if it is a promise */
188176
if( TYPEOF(res) == PROMSXP){
189-
#if defined(RCPP_USE_UNWIND_PROTECT)
190-
res = internal::Rcpp_eval_impl(res, env);
191-
#else
192-
res = Rf_eval(res, env);
193-
#endif
177+
res = internal::Rcpp_eval_impl( res, env ) ;
194178
}
195179
return res ;
196180
}

inst/include/Rcpp/Language.h

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,10 @@ namespace Rcpp{
145145
}
146146

147147
SEXP fast_eval() const {
148-
#if defined(RCPP_USE_UNWIND_PROTECT)
149-
return internal::Rcpp_eval_impl( Storage::get__(), R_GlobalEnv);
150-
#else
151-
return Rf_eval(Storage::get__(), R_GlobalEnv);
152-
#endif
148+
return internal::Rcpp_eval_impl( Storage::get__(), R_GlobalEnv) ;
153149
}
154150
SEXP fast_eval(SEXP env ) const {
155-
#if defined(RCPP_USE_UNWIND_PROTECT)
156151
return internal::Rcpp_eval_impl( Storage::get__(), env) ;
157-
#else
158-
return Rf_eval(Storage::get__(), env);
159-
#endif
160152
}
161153

162154
void update( SEXP x){

inst/include/Rcpp/api/meat/Rcpp_eval.h

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,12 @@
2121
#include <Rcpp/Interrupt.h>
2222
#include <Rversion.h>
2323

24-
// outer definition from RcppCommon.h
25-
#if defined(RCPP_USE_UNWIND_PROTECT)
26-
#if (defined(RCPP_PROTECTED_EVAL) && defined(R_VERSION) && R_VERSION >= R_Version(3, 5, 0))
27-
// file-local and only used here
28-
#define RCPP_USE_PROTECT_UNWIND
29-
#endif
24+
#if (defined(RCPP_PROTECTED_EVAL) && defined(R_VERSION) && R_VERSION >= R_Version(3, 5, 0))
25+
#define RCPP_USE_PROTECT_UNWIND
26+
#include <csetjmp>
3027
#endif
3128

29+
3230
namespace Rcpp {
3331
namespace internal {
3432

@@ -39,18 +37,17 @@ namespace internal {
3937
SEXP env;
4038
EvalData(SEXP expr_, SEXP env_) : expr(expr_), env(env_) { }
4139
};
40+
struct EvalUnwindData {
41+
std::jmp_buf jmpbuf;
42+
};
4243

43-
inline void Rcpp_maybe_throw(void* data, Rboolean jump) {
44+
// First jump back to the protected context with a C longjmp because
45+
// `Rcpp_protected_eval()` is called from C and we can't safely throw
46+
// exceptions across C frames.
47+
inline void Rcpp_maybe_throw(void* unwind_data, Rboolean jump) {
4448
if (jump) {
45-
SEXP token = static_cast<SEXP>(data);
46-
47-
// Keep the token protected while unwinding because R code might run
48-
// in C++ destructors. Can't use PROTECT() for this because
49-
// UNPROTECT() might be called in a destructor, for instance if a
50-
// Shield<SEXP> is on the stack.
51-
::R_PreserveObject(token);
52-
53-
throw LongjumpException(token);
49+
EvalUnwindData* data = static_cast<EvalUnwindData*>(unwind_data);
50+
longjmp(data->jmpbuf, 1);
5451
}
5552
}
5653

@@ -80,9 +77,21 @@ namespace internal {
8077

8178
inline SEXP Rcpp_fast_eval(SEXP expr, SEXP env) {
8279
internal::EvalData data(expr, env);
80+
internal::EvalUnwindData unwind_data;
8381
Shield<SEXP> token(::R_MakeUnwindCont());
82+
83+
if (setjmp(unwind_data.jmpbuf)) {
84+
// Keep the token protected while unwinding because R code might run
85+
// in C++ destructors. Can't use PROTECT() for this because
86+
// UNPROTECT() might be called in a destructor, for instance if a
87+
// Shield<SEXP> is on the stack.
88+
::R_PreserveObject(token);
89+
90+
throw internal::LongjumpException(token);
91+
}
92+
8493
return ::R_UnwindProtect(internal::Rcpp_protected_eval, &data,
85-
internal::Rcpp_maybe_throw, token,
94+
internal::Rcpp_maybe_throw, &unwind_data,
8695
token);
8796
}
8897

@@ -112,11 +121,7 @@ inline SEXP Rcpp_eval(SEXP expr, SEXP env) {
112121
SET_TAG(CDDR(call), ::Rf_install("error"));
113122
SET_TAG(CDDR(CDR(call)), ::Rf_install("interrupt"));
114123

115-
#if defined(RCPP_USE_UNWIND_PROTECT)
116-
Shield<SEXP> res(::Rf_eval(call, R_GlobalEnv)) // execute the call
117-
#else
118124
Shield<SEXP> res(internal::Rcpp_eval_impl(call, R_GlobalEnv));
119-
#endif
120125

121126
// check for condition results (errors, interrupts)
122127
if (Rf_inherits(res, "condition")) {
@@ -125,12 +130,7 @@ inline SEXP Rcpp_eval(SEXP expr, SEXP env) {
125130

126131
Shield<SEXP> conditionMessageCall(::Rf_lang2(::Rf_install("conditionMessage"), res));
127132

128-
#if defined(RCPP_USE_UNWIND_PROTECT)
129-
Shield<SEXP> conditionMessage(internal::Rcpp_eval_impl(conditionMessageCall,
130-
R_GlobalEnv));
131-
#else
132-
Shield<SEXP> conditionMessage(::Rf_eval(conditionMessageCall, R_GlobalEnv));
133-
#endif
133+
Shield<SEXP> conditionMessage(internal::Rcpp_eval_impl(conditionMessageCall, R_GlobalEnv));
134134
throw eval_error(CHAR(STRING_ELT(conditionMessage, 0)));
135135
}
136136

inst/include/Rcpp/exceptions.h

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,23 +111,51 @@ namespace Rcpp {
111111
throw Rcpp::exception(message.c_str());
112112
} // #nocov end
113113

114-
#if defined(RCPP_USE_UNWIND_PROTECT)
115114
namespace internal {
116115

116+
inline SEXP longjumpSentinel(SEXP token) {
117+
SEXP sentinel = PROTECT(Rf_allocVector(VECSXP, 1));
118+
SET_VECTOR_ELT(sentinel, 0, token);
119+
120+
SEXP sentinelClass = PROTECT(Rf_mkString("Rcpp:longjumpSentinel"));
121+
Rf_setAttrib(sentinel, R_ClassSymbol, sentinelClass) ;
122+
123+
UNPROTECT(2);
124+
return sentinel;
125+
}
126+
127+
inline bool isLongjumpSentinel(SEXP x) {
128+
return
129+
Rf_inherits(x, "Rcpp:longjumpSentinel") &&
130+
TYPEOF(x) == VECSXP &&
131+
Rf_length(x) == 1;
132+
}
133+
134+
inline SEXP getLongjumpToken(SEXP sentinel) {
135+
return VECTOR_ELT(sentinel, 0);
136+
}
137+
117138
struct LongjumpException {
118139
SEXP token;
119-
LongjumpException(SEXP token_) : token(token_) { }
140+
LongjumpException(SEXP token_) : token(token_) {
141+
if (isLongjumpSentinel(token)) {
142+
token = getLongjumpToken(token);
143+
}
144+
}
120145
};
121146

122147
inline void resumeJump(SEXP token) {
148+
if (isLongjumpSentinel(token)) {
149+
token = getLongjumpToken(token);
150+
}
123151
::R_ReleaseObject(token);
124-
#if (defined(RCPP_PROTECTED_EVAL) && defined(R_VERSION) && R_VERSION >= R_Version(3, 5, 0))
152+
#if (defined(R_VERSION) && R_VERSION >= R_Version(3, 5, 0))
125153
::R_ContinueUnwind(token);
126154
#endif
155+
Rf_error("Internal error: Rcpp longjump failed to resume");
127156
}
128157

129158
} // namespace internal
130-
#endif
131159

132160
} // namespace Rcpp
133161

0 commit comments

Comments
 (0)