Tomas Kalibera: Unprotecting by Value



In short, UNPROTECT_PTR is dangerous and should not be used. This text describes why and describes how to replace it, including mset-based functions that have been introduced as a substitute for situations when unprotection by value is really needed. This could be of interest to anyone who writes native code to interface with the R heap, and definitely to all who use UNPROTECT_PTR in their code.

Background

R provides several functions to protect pointers to R objects held by local C variables (typed SEXP) from the garbage collector. As documented in Writing R Extensions, there are two structures to hold protected pointers: the pointer protection stack and the precious list.

The pointer protection stack is accessed using PROTECT/UNPROTECT. Pointers are unprotected by being removed from the top of the stack. One can also use PROTECT_WITH_INDEX and then REPROTECT to replace a pointer defined by its position in the stack, which allows to simplify and speed-up code that repeatedly updates local variables holding pointers (in such scenarios, one could in principle still use a sequence of PROTECT/UNPROTECT operations, instead). The pointer protection stack needs to be managed in-line with the C call stack: after returning from a function, the stack depth should be the same as when the function was called (pointer protection balance). These and other rules are described in Writing R Extensions and The danger of PROTECT errors, they are relatively easy follow and check, both visually and by tools (also, pointer-protection balance is checked to some level at runtime). The stack-based protection and unprotection are fast, do not require additional allocation and are automatically handled during R errors (long jumps): a long jump recovers the previous stack depth, unprotecting the values that have been left on the stack by the code executed after the jump was set but before the jump was executed.

Although such situations are very rare, sometimes achieving pointer protection balance is difficult, sometimes say package code wishes to keep some allocated space without returning a pointer to it (hence without making the caller protect it, when we have global variables pointing to R heap and for some reason cannot turn them into locals). This is addressed by the precious list, which is accessed using R_PreserveObject/R_ReleaseObject. It is implement as a linked list (and yes, R_PreserveObject allocates!) and objects are unprotected by value. There is no automated unprotection on error, the user is always responsible for unprotecting objects stored on the precious list. To achieve that in case of R errors (long jumps) or in callbacks (e.g. unloading of a package), it may be necessary to allocate a dummy object, set up its finalizer, and let the finalizer release needed objects from the precious list. R_PreserveObject and R_ReleaseObject are also much slower than PROTECT/UNPROTECT.

The API was still not sufficient for very special applications, applications which used generated code that allocated memory from the R heap, such as the R parser generated by bison. The parser code uses a stack of semantic values, which are pointers to objects on the R heap. Values are pushed on the stack by the tokenizer during shift operations, are both pushed and removed during actions of reduce operations, and are removed on some parse errors. R errors (long jumps) can also occur during parsing. The stack is local to a parsing function. The key problem is that the code of the parser is generated and bison cannot be customized enough to ensure insertion of PROTECT/UNPROTECT operations. It would be natural to allocate the semantic values stack on the R heap, protect it, and protect semantic values when held in local variables but not yet on the semantic values stack, all using PROTECT/UNPROTECT. But, this is not possible. In principle, R_PreserveObject/R_ReleaseObject could be used, but one would have to handle the errors and, most importantly, the performance overhead would not be acceptable.

To work around this problem, UNPROTECT_PTR has been introduced. It allows relatively fast unprotect-by-value operation for semantic values protected in the pointer protection stack. When new semantic values are created, they are immediatelly put on the protection stack using PROTECT by the tokenizer and reduce rules. The values are unprotected by UNPROTECT_PTR inside the reduce rules, and the pointer protection stack depth is restored after certain parse errors that did not cause a long jump (one can also define a destructor in bison for some tokens and make it call UNPROTECT_PTR, as done in the Rd parser in package tools). UNPROTECT_PTR removes the first occurrence of the pointer (starting at stack top) and squeezes the stack, reducing the stack depth. Using UNPROTECT_PTR this way causes pointer protection imbalance by design (the tokenizer and reduce rules are implemented in different functions), which increases cognitive complexity of the code. It is, however, faster than the precious list and uses less memory, when used carefully it works with R long jumps (automated unprotection), and it may well be that there is not a better way to do protection in the parser than unprotect-by-value (if we don’t modify the generated parser code). It has been used for many years in the parser and, unfortunately, started to be used also outside the parser where not necessary.

It has been known and documented that combining UNPROTECT_PTR with PROTECT_WITH_INDEX is dangerous, because by removing a certain object from the stack by UNPROTECT_PTR and squeezing the stack, the protect index may become invalid/unexpected (objects locations on the stack change). REPROTECT would then replace the wrong pointer, resulting in a memory leak (the object intended for unprotection stays protected) and, worse, premature unprotection (REPROTECT would replace an object that still was to be protected). Code which uses UNPROTECT_PTR is also rather hard to read.

UNPROTECT_PTR is dangerous

While working on some improvements of the parser I realized that UNPROTECT_PTR is unsafe also in combination with PROTECT/UNPROTECT. The problem occurs when the same pointer is stored multiple times on the protection stack. One can accidentally use UNPROTECT_PTR to unprotect the unintended instance of the object, an instance that was intended to be unprotected by UNPROTECT, instead. At the point of UNPROTECT_PTR, nothing bad yet happens, but, when one later gets to the UNPROTECT, the wrong object gets unprotected, resulting in a premature unprotection (protect bug). Unfortunately, it is quite common particularly in the parser for the same pointer to be protected multiple times (R_NilValue, symbols).

To illustrate this, imagine this sequence of pointers on the stack (3 is protected last, A and A’ are the same pointer, A is intended for unprotection by value):

1A2A'3

after UNPROTECT_PTR(A), we get

1A23

instead of what the enclosing code expected:

12A'3

The depth is ok, say the code later does UNPROTECT(1) intending to unprotect 3 and actually doing so, so still ok. But, eventually UNPROTECT(1) may be called in the intention to unprotect 2, but in fact unprotects A. So, 2 will still be kept alive (memory leak, possibly temporary, so not that bad), but A will be prematurely unprotected, causing a protect bug (and one that may be very hard to debug).

In principle, R_NilValue and symbols do not need to be protected at all, but they are and sometimes it makes the code more readable when the distinction is not made. Moreover, any function returning a pointer may sometimes return a fresh pointer and sometimes a pointer that already exists (including in the parser, where some list manipulating functions work(ed) that way). So, this seems to be a real danger. Also, using UNPROTECT_PTR the way as in the parser makes verification of other, purely stack-based PROTECT/UNPROTECT operations, harder, both manually and by tools, because it is not made explicit which pointers were intended to be unprotected by UNPROTECT_PTR and which by R_ReleaseObject.

Phasing out UNPROTECT_PTR

I have thus removed the use of UNPROTECT_PTR from all R base code. It was relatively easy in the few cases when used outside the parser, I have just rewritten the code using stack-based protection functions. I think in all cases this actually simplified the code.

For use in the parsers (the R parser and the two parsers from package tools), I’ve introduced API for value-based unprotection outside the pointer protection stack. These functions use a precious multi-set to protect these objects; the multi-set is allocated on the R heap and needs to be protected by the caller (e.g. using PROTECT). Consequently, it is automatically unprotected on the long jump, and hence all pointers protected in the mset get indirectly unprotected as well. The current implementation uses a (vector-) list instead of a pair-list, so is also faster than R_PreserveObject/R_ReleaseObject, but this is just an implementation detail that can change and certainly the unprotection could be made faster if it turns out to be a bottleneck in practice. The main benefit is that these functions use a separate structure for unprotection by value, not polluting the pointer protection stack.

SEXP R_NewPreciousMSet(int initialSize);
void R_PreserveInMSet(SEXP x, SEXP mset);
void R_ReleaseFromMSet(SEXP x, SEXP mset);
void R_ReleaseMSet(SEXP mset, int keepSize);

To use this API, one needs first to create a new mset using R_NewPreciousMSet and PROTECT it. The mset is expanded automatically as needed (R_PreserveInMSet may allocate). Objects are released by value via R_ReleaseFromMSet using the same (naive) algorithm as was used in UNPROTECT_PTR, so there should be no performance hit (in principle, the operations could be faster as they do not have to deal with objects intended for stack-based protection). One does not have to release objects explicitly, they will all be released when the mset is garbage collected (e.g. on a long jump that would unprotect the mset). For performance reasons, one may however use R_ReleaseMSet to clear the mset but keep it allocated, if the allocated size is not bigger than given number of elements (this can be used e.g. on errors that are not implemented as long jumps). As anything in R-devel code base, the API is still subject to change.

Switching from UNPROTECT_PTR to the new API is harder than it may first seem as one has to identify the PROTECT operations that are intended for unprotection by value (and rewrite the code when some code paths unprotect the same “variable” in one way and other code paths in another).

Choosing the right API

I think for memory protection one should always use PROTECT/UNPROTECT, possibly with PROTECT_WITH_INDEX/REPROTECT in performance critical code. R_PreserveObject/R_ReleaseObject help if we have global variables holding on to R memory, but global variables should be avoided anyway also for other reasons, so this should be very rare. Also, arranging for unprotection on error is a bit tedious. R_PreserveInMSet/R_ReleaseFromMSet should be used only in bison/yacc parsers and UNPROTECT_PTR should be phased out from all code.