Skip to content

Potential leakage with exceptions #286

@romainfrancois

Description

@romainfrancois

While looking at tidyverse/dplyr#1062, I need up examining yet another time the whole exception business in Rcpp. I think there is a problem.

#include <Rcpp.h>
using namespace Rcpp ;

// [[Rcpp::export]]
void foo(){
        stop( "foo %s", "bar" ) ;
}

/*** R
        foo()

*/

When I sourceCpp this through valgrind, I get:

> Rcpp::sourceCpp( "/tmp/test.cpp")

> foo()
Error in eval(expr, envir, enclos) : foo bar
Calls: <Anonymous> ... source -> withVisible -> eval -> eval -> foo -> <Anonymous>
Execution halted
==68583==
==68583== HEAP SUMMARY:
==68583==     in use at exit: 33,147,961 bytes in 15,982 blocks
==68583==   total heap usage: 41,762 allocs, 25,780 frees, 67,956,199 bytes allocated
==68583==
==68583== 32 bytes in 1 blocks are possibly lost in loss record 21 of 1,770
==68583==    at 0x4C2B0E0: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==68583==    by 0xC9563B8: std::string::_Rep::_S_create(unsigned long, unsigned long, std::allocator<char> const&) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)
==68583==    by 0xC957AE0: char* std::string::_S_construct<char const*>(char const*, char const*, std::allocator<char> const&, std::forward_iterator_tag) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)
==68583==    by 0xC957EF7: std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)
==68583==    by 0xD5BA785: Rcpp::exception::exception(char const*) (exceptions.h:31)
==68583==    by 0xD5BC5F7: void Rcpp::stop<char [4]>(char const*, char const (&) [4]) (exceptions.h:256)
==68583==    by 0xD5B9EA6: foo() (test.cpp:6)
==68583==    by 0xD5B9ED9: sourceCpp_34287_foo (test.cpp:21)
==68583==    by 0x4EC84F4: ??? (in /usr/lib/R/lib/libR.so)
==68583==    by 0x4F08055: Rf_eval (in /usr/lib/R/lib/libR.so)
==68583==    by 0x4F0B631: ??? (in /usr/lib/R/lib/libR.so)
==68583==    by 0x4F07F30: Rf_eval (in /usr/lib/R/lib/libR.so)
==68583==
==68583== 144 bytes in 1 blocks are possibly lost in loss record 37 of 1,770
==68583==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==68583==    by 0xC8F94E2: __cxa_allocate_exception (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)
==68583==    by 0xD5BC5D7: void Rcpp::stop<char [4]>(char const*, char const (&) [4]) (exceptions.h:256)
==68583==    by 0xD5B9EA6: foo() (test.cpp:6)
==68583==    by 0xD5B9ED9: sourceCpp_34287_foo (test.cpp:21)
==68583==    by 0x4EC84F4: ??? (in /usr/lib/R/lib/libR.so)
==68583==    by 0x4F08055: Rf_eval (in /usr/lib/R/lib/libR.so)
==68583==    by 0x4F0B631: ??? (in /usr/lib/R/lib/libR.so)
==68583==    by 0x4F07F30: Rf_eval (in /usr/lib/R/lib/libR.so)
==68583==    by 0x4F09144: Rf_applyClosure (in /usr/lib/R/lib/libR.so)
==68583==    by 0x4F07C0D: Rf_eval (in /usr/lib/R/lib/libR.so)
==68583==    by 0x4F0C4F3: ??? (in /usr/lib/R/lib/libR.so)
==68583==
==68583== LEAK SUMMARY:
==68583==    definitely lost: 0 bytes in 0 blocks
==68583==    indirectly lost: 0 bytes in 0 blocks
==68583==      possibly lost: 176 bytes in 2 blocks
==68583==    still reachable: 33,147,785 bytes in 15,980 blocks
==68583==         suppressed: 0 bytes in 0 blocks
==68583== Reachable blocks (those to which a pointer was found) are not shown.
==68583== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==68583==
==68583== For counts of detected and suppressed errors, rerun with: -v
==68583== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

I believe that the problem is that the destructor for the exception is never called because of jumping. i.e. stop generates an Rcpp::exception:

class exception : public std::exception {
    public:
        explicit exception(const std::string& message_) : message(message_){}
        exception(const std::string& message_, const char* file, int line ) : message(message_){
            rcpp_set_stack_trace( stack_trace(file,line) ) ;
        }
        virtual ~exception() throw(){}
        virtual const char* what() const throw() {
            return message.c_str() ;
        }
    private:
        std::string message ;
    } ;

that is caught though END_RCPP :

#ifndef VOID_END_RCPP
#define VOID_END_RCPP                                                          \
  }                                                                            \
  catch (Rcpp::internal::InterruptedException &__ex__) {                       \
    Rf_onintr();                                                               \
  }                                                                            \
  catch (std::exception &__ex__) {                                             \
    forward_exception_to_r(__ex__);                                            \
  }                                                                            \
  catch (...) {                                                                \
    ::Rf_error("c++ exception (unknown reason)");                              \
  }
#endif

#ifndef END_RCPP
#define END_RCPP VOID_END_RCPP return R_NilValue;
#endif

and then in forward_exception_to_r we end up jumping with a callback to R's stop :

inline void forward_exception_to_r( const std::exception& ex){
    SEXP stop_sym  = Rf_install( "stop" ) ;
    Rcpp::Shield<SEXP> condition( exception_to_r_condition(ex) );
    Rcpp::Shield<SEXP> expr( Rf_lang2( stop_sym , condition ) ) ;
    Rf_eval( expr, R_GlobalEnv ) ;
}

The problem here is that the exception is never destructed, and therefore the std::string message it holds is not destructed either, hence the complaints from valgrind.

See this simpler example:

#include <Rcpp.h>
using namespace Rcpp ;


struct dummy{
        dummy(int value_) : value(value_){}

        ~dummy(){
                Rprintf( "~dummy" ) ;
        }
        int value ;
} ;

struct dummy_exception : public std::exception {
        dummy_exception(int value) : exception(), x(value) {}

        ~dummy_exception() throw(){}

        dummy x ;
} ;


// [[Rcpp::export]]
void foo(){
        throw dummy_exception(3) ;
}

/*** R
        foo()

*/

The point here is that I want to see ~dummy, and I don't:

$ R --vanilla < test.R

R version 3.1.3 (2015-03-09) -- "Smooth Sidewalk"
Copyright (C) 2015 The R Foundation for Statistical Computing
Platform: x86_64-pc-linux-gnu (64-bit)

R is free software and comes with ABSOLUTELY NO WARRANTY.
You are welcome to redistribute it under certain conditions.
Type 'license()' or 'licence()' for distribution details.

  Natural language support but running in an English locale

R is a collaborative project with many contributors.
Type 'contributors()' for more information and
'citation()' on how to cite R or R packages in publications.

Type 'demo()' for some demos, 'help()' for on-line help, or
'help.start()' for an HTML browser interface to help.
Type 'q()' to quit R.

> Rcpp::sourceCpp( "/tmp/test.cpp")

> foo()
Error in eval(expr, envir, enclos) : std::exception
Calls: <Anonymous> ... source -> withVisible -> eval -> eval -> foo -> <Anonymous>
Execution halted

whereas if I handle the try-catch manually without then calling R's stop the exception is properly destructed:

// [[Rcpp::export]]
void foo(){
        try{
                throw dummy_exception(3) ;
        } catch( const std::exception& ex ){

        }
}

with this I get:

$ R --vanilla < test.R

R version 3.1.3 (2015-03-09) -- "Smooth Sidewalk"
Copyright (C) 2015 The R Foundation for Statistical Computing
Platform: x86_64-pc-linux-gnu (64-bit)

R is free software and comes with ABSOLUTELY NO WARRANTY.
You are welcome to redistribute it under certain conditions.
Type 'license()' or 'licence()' for distribution details.

  Natural language support but running in an English locale

R is a collaborative project with many contributors.
Type 'contributors()' for more information and
'citation()' on how to cite R or R packages in publications.

Type 'demo()' for some demos, 'help()' for on-line help, or
'help.start()' for an HTML browser interface to help.
Type 'q()' to quit R.

> Rcpp::sourceCpp( "/tmp/test.cpp")

> foo()
~dummy>
>

Now leaking a std::string might not be a big deal, but it's definitely real.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions