I have a Ruby on Rails api which handles a simple API call and returns some encrypted data. The encryption is done in C++, using the ruby native C api. (reference here).
The native part works fine when compiled and linked as a standalone program, and also when used with ruby in IRB.
However, when I use it from within the Rails API, I sometimes get a "Stack level too deep" error.
The error seems to occur or not depending on the size of the data processed.
According to this answer, the stack 'level' is actually stack space, so it would make sense that if I have more data to process, then I have more data in the stack, so it fills up quicker etc...
I had initially left all my variables in the stack, for simplicity and to avoid forgetting to free allocated memory. Seeing this error, I switched to a dynamical allocation approach. However contrarily to what I was expecting, the Stack level too deep error occurs for even smaller data size.
data_controller.rb
def load_data data_path, width
authorize!
encrypted = NativeDataProtector.encrypt(data_path, get_key(), get_iv())
return [ encrypted, "application/octet-stream" ]
end
native_encryptor.cpp
VALUE encrypt_n(VALUE _self, VALUE data_path, VALUE key, VALUE salt){
DataProtector protector;
string *b64 = protector.encrypt(StringValueCStr(data_path), \
StringValueCStr(key), \
StringValueCStr(salt));
VALUE ret = rb_str_new(b64->c_str(), b64->length());
delete(b64);
return ret;
}
extern "C" void Init_data_protector() {
VALUE mod = rb_define_module("NativeDataProtector");
rb_define_module_function(mod, "encrypt", (VALUE(*)(ANYARGS))encrypt_n, 3);
}
encrypt.h
#include <ruby.h>
#include "extconf.h"
#include <iostream>
#include <fstream>
#include <vector>
#include <list>
#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/err.h>
class DataProtector {
private :
int pad_cleartext(vector<unsigned char> *cleartext);
vector<unsigned char> *read_data(string path);
int aes_encrypt(vector<unsigned char> *plaintext, string key,
string iv, unsigned char *ciphertext);
string to_b64(unsigned char* in);
void handleErrors(void);
public :
string *encrypt(string data_path, string key, string salt);
};
encrypt.cpp
string *DataProtector::encrypt(string data_path, string key, string salt) {
vector<unsigned char> *cleartext = readData(data_path);
int length = pad_cleartext(cleartext);
unsigned char* output = new unsigned char[length + 16];
int ciphertext_len;
// encrypt
string *encrypted = new string("");
ciphertext_len = aes_encrypt(&((*cleartext), key, iv, output);
(*encrypted) += to_b64(output);
delete(cleartext);
delete(output);
return encrypted;
}
int DataProtector::aes_encrypt(vector<unsigned char> *plaintext, string key,
string iv, unsigned char *ciphertext)
{
EVP_CIPHER_CTX *ctx;
int len;
int ciphertext_len;
/* Create and initialise the context */
if(!(ctx = EVP_CIPHER_CTX_new())) handleErrors();
/* Initialise the encryption operation. IMPORTANT - ensure you use a key
* and IV size appropriate for your cipher
* In this example we are using 256 bit AES (i.e. a 256 bit key). The
* IV size for *most* modes is the same as the block size. For AES this
* is 128 bits */
if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, (const unsigned char *)key.c_str(), (const unsigned char *)iv.c_str()))
handleErrors();
/* Provide the message to be encrypted, and obtain the encrypted output.
* EVP_EncryptUpdate can be called multiple times if necessary
*/
if(1 != EVP_EncryptUpdate(ctx, ciphertext, &len, reinterpret_cast<unsigned char*>(plaintext->data()), plaintext->size()))
handleErrors();
ciphertext_len = len;
/* Finalise the encryption. Further ciphertext bytes may be written at
* this stage.
*/
if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) handleErrors();
ciphertext_len += len;
/* Clean up */
EVP_CIPHER_CTX_free(ctx);
return ciphertext_len;
}
int DataProtector::pad_cleartext(vector<unsigned char> *in) {
// padds to length multiple of 16
int nb_blocks = in->size() / 16 + ((in->size()%16 == 0)? 1:1);
int size = nb_blocks*16;
for (unsigned int i=in->size(); i<size; i++) {
unsigned char c = '0';
in->push_back(c);
}
return size;
}
vector<unsigned char> *DataProtector::read_data(string path) {
streampos size;
ifstream file(path, ios::binary);
file.seekg(0, ios::end);
size = file.tellg();
file.seekg(0, ios::beg);
vector<unsigned char> *data = new vector<unsigned char>(fileSize);
file.read((char*) &data[0], size);
return data;
}
void DataProtector::handleErrors(void) {
ERR_print_errors_fp(stderr);
abort();
}
(the actual encryption is from here)
The error stack trace I get :
SystemStackError (stack level too deep):
app/controllers/data_controller.rb:41:in `encrypt'
app/controllers/data_controller.rb:41:in `load_data'
app/controllers/data_controller.rb:15:in `show'
Is Believe that the reason for this error is too much data allocated on the stack, and not a recursion issue. However, I don't understand why switching to heap allocation did not improve anything.
I can imagine 2 solutions :
- cutting up the data in ruby and calling the native method several times with less data.
- increasing the ruby stack size. However both these solutions are unideal for my project, for performance/resource issues.
Is there any other way I can reduce the usage of the stack by my program ?