Skip to content

Instantly share code, notes, and snippets.

@cosinusoidally
Last active April 3, 2018 16:55
Show Gist options
  • Save cosinusoidally/964164ea173d49ff700a6baeeea0ade6 to your computer and use it in GitHub Desktop.
Save cosinusoidally/964164ea173d49ff700a6baeeea0ade6 to your computer and use it in GitHub Desktop.
Garbage collector safe example embedding ChakraCore in Spidermonkey shell using js-ctypes
/*
Tested against release/1.4 branch of https://github.com/Microsoft/ChakraCore
commit: 6f4c890505a58bbf198035d4b93bf8a726033af3
The below program will work on x86_64 Linux. It demonstrates how to safely call
in to libChakraCore from Spidermonkey using js-ctypes. Because the ChakraCore
GC scans the stack for JsValueRefs we must allocate JsValueRefs on the C stack.
When I say C stack I mean in the memory region between the current stack
pointer and the initial value of the stack pointer when the program started.
Values created through js-ctypes will be allocated on the heap. If we hand
these values to CharkaCore API functions this will cause JsValueRefs to be
created in the heap rather than on the stack. As far as the GC is concerned
these are garbage, and so can be collected. When this occurs the program
embedding ChakraCore may crash (I say may, because this is a nondeterministic
bug). We cannot work around this by calling JsAddRef to bump the reference
count (since the reference count starts at zero, if the GC runs between when
the JsValueRef is created and when JsAddRef is called we will crash). In order
to get around this issue we must allocated the JsValueRefs on the C stack. In
order to do this we use a helper function written in C (called
make_stack_variables). We call make_stack_variables from Spidermonkey using
js-ctypes, make_stack_variables then allocates some pointers on the stack and
calls back in to Spidermonkey (by using a function pointer to a js-ctypes
callback function (called js_callback)). The callback function then takes
pointers to the stack allocated variables and hands these in to any ChakraCore
API functions that we wish to call.
Once you have set everything up (instructions are further down in this file) you can run the following
js -e "unsafe=false;run_gc=true" example.js
There is lots of debug output. If all goes well, somewhere towards the end you should see the following:
ChakraCore Returned:
Hello world!
Note the above invocation does not segfault. The below invocation will segfault:
js -e "unsafe=true;run_gc=true" example.js
The "unsafe" variable controls whether the JsValueRefs from the ChakraCore API are stored on the C stack.
unsafe=false is safe, it stores JsValueRefs in the C stack
unsafe=true is unsafe, it stores the JsValueRefs in the heap (since the storage space is allocated using jsctypes).
the run_gc variable controls whether JsCollectGarbage will be explicitly called at several points during the
program. This turns a nondeterministic bug into a reproducable bug.
run the script in the same dir as libChakraCore.so and libstackhelper.so
In order to set everything up:
get Spidermonkey from here:
https://ftp.mozilla.org/pub/firefox/candidates/45.6.0esr-candidates/build1/jsshell-linux-x86_64.zip
SHA512SUM and signature here:
https://ftp.mozilla.org/pub/firefox/candidates/45.6.0esr-candidates/build1/linux-x86_64/en-US/firefox-45.6.0esr.checksums
https://ftp.mozilla.org/pub/firefox/candidates/45.6.0esr-candidates/build1/linux-x86_64/en-US/firefox-45.6.0esr.checksums.asc
If you can't get a hold of the above binary it my have been moved to the archive:
https://ftp.mozilla.org/pub/firefox/candidates/archived/
failing that just pick another one from somewhere in:
https://ftp.mozilla.org/pub/firefox/candidates
set up Spidermonkey by unzipping it and adding it to your PATH and LD_LIBRARY_PATH:
mkdir spidermonkey
cd spidermonkey
unzip ../jsshell-linux-x86_64.zip
export PATH=${PWD}:$PATH
export LD_LIBRARY_PATH=${PWD}
For the purposes of this example we need a helper shared library in order to allocate variables on the C stack.
Source to stackhelper.c is below. To build the library do:
gcc -g -c -Wall -fpic stackhelper.c
gcc -shared -o libstackhelper.so stackhelper.o
File stackhelper.c :
#include <stdint.h>
#include <stdio.h>
void make_stack_variables(void (*f)(uint64_t,uint64_t,uint64_t,uint64_t,uint64_t,uint64_t,uint64_t,uint64_t)){
void * p1=0;
void * p2=0;
void * p3=0;
void * p4=0;
void * p5=0;
void * p6=0;
void * p7=0;
void * p8=0;
printf("p1: Start our pointer is: %p\n",&p1);
printf("p1: Start our value is: %p\n\n",p1);
printf("p2: Start our pointer is: %p\n",&p2);
printf("p2: Start our value is: %p\n\n",p2);
printf("p3: Start our pointer is: %p\n",&p3);
printf("p3: Start our value is: %p\n\n",p3);
printf("p4: Start our pointer is: %p\n",&p4);
printf("p4: Start our value is: %p\n\n",p4);
printf("p5: Start our pointer is: %p\n",&p5);
printf("p5: Start our value is: %p\n\n",p5);
printf("p6: Start our pointer is: %p\n",&p6);
printf("p6: Start our value is: %p\n\n",p6);
printf("p7: Start our pointer is: %p\n",&p7);
printf("p7: Start our value is: %p\n\n",p7);
printf("p8: Start our pointer is: %p\n",&p8);
printf("p8: Start our value is: %p\n\n",p8);
(*f)((uint64_t)&p1,(uint64_t)&p2,(uint64_t)&p3,(uint64_t)&p4,(uint64_t)&p5,(uint64_t)&p6,(uint64_t)&p7,(uint64_t)&p8);
printf("p1: our pointer is: %p\n",&p1);
printf("p1: our value is: %p\n\n",p1);
printf("p2: our pointer is: %p\n",&p2);
printf("p2: our value is: %p\n\n",p2);
printf("p3: our pointer is: %p\n",&p3);
printf("p3: our value is: %p\n\n",p3);
printf("p4: our pointer is: %p\n",&p4);
printf("p4: our value is: %p\n\n",p4);
printf("p5: our pointer is: %p\n",&p5);
printf("p5: our value is: %p\n\n",p5);
printf("p6: our pointer is: %p\n",&p6);
printf("p6: our value is: %p\n\n",p6);
printf("p7: our pointer is: %p\n",&p7);
printf("p7: our value is: %p\n\n",p7);
printf("p8: our pointer is: %p\n",&p8);
printf("p8: our value is: %p\n\n",p8);
return;
}
End of file stackhelper.c
*/
// once you have built the above library put it in the same dir as libChakraCore.so
// run this script in the same dir as libChakraCore.so
// Need to create a global window object to store some globals:
window=this;
take_address=function(x){return ctypes.cast(x.address(),ctypes.uint64_t)};
p=print;
p(a=ctypes.open("./libChakraCore.so"));
p(DllMain = a.declare("DllMain", ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t));
p(JsCollectGarbage = a.declare("JsCollectGarbage",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t));
// p(JsCreateRuntime = a.declare("JsCreateRuntime", ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t));
p(JsCreateRuntime = a.declare("JsCreateRuntime", ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr));
// p(JsCreateContext = a.declare("JsCreateContext",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t));
p(JsCreateContext = a.declare("JsCreateContext",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr));
p(JsSetCurrentContext = a.declare("JsSetCurrentContext",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t));
// p(JsCreateStringUtf8 = a.declare("JsCreateStringUtf8",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t));
p(JsCreateStringUtf8 = a.declare("JsCreateStringUtf8",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr));
//p(JsCreateExternalArrayBuffer = a.declare("JsCreateExternalArrayBuffer",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t));
p(JsCreateExternalArrayBuffer = a.declare("JsCreateExternalArrayBuffer",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr));
//p(JsRun = a.declare("JsRun",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t));
p(JsRun = a.declare("JsRun",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr));
// p(JsConvertValueToString = a.declare("JsConvertValueToString",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t));
p(JsConvertValueToString = a.declare("JsConvertValueToString",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr));
// p(JsCopyStringUtf8 = a.declare("JsCopyStringUtf8",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t));
p(JsCopyStringUtf8 = a.declare("JsCopyStringUtf8",ctypes.default_abi, ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t.ptr));
p(pp=ctypes.open("./libstackhelper.so"));
p(make_stack_variables=pp.declare("make_stack_variables",ctypes.default_abi, ctypes.void_t,ctypes.voidptr_t));
p(stack_helper_wrapper_function_type=ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, [ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t,ctypes.uint64_t]));
gc=function(){};
try{
run_gc;
} catch(e){
run_gc=false;
}
try{
unsafe;
} catch(e){
unsafe=false;
}
if(run_gc){
chakra_gc=JsCollectGarbage;
}else{
chakra_gc=function(){};
}
window.global_array=[];
js_callback=function(p1,p2,p3,p4,p5,p6,p7,p8){
var ptrs;
if(unsafe===false){
ptrs=[
ctypes.cast(ctypes.uint64_t(p1),ctypes.uint64_t.ptr),
ctypes.cast(ctypes.uint64_t(p2),ctypes.uint64_t.ptr),
ctypes.cast(ctypes.uint64_t(p3),ctypes.uint64_t.ptr),
ctypes.cast(ctypes.uint64_t(p4),ctypes.uint64_t.ptr),
ctypes.cast(ctypes.uint64_t(p5),ctypes.uint64_t.ptr),
ctypes.cast(ctypes.uint64_t(p6),ctypes.uint64_t.ptr),
ctypes.cast(ctypes.uint64_t(p7),ctypes.uint64_t.ptr),
ctypes.cast(ctypes.uint64_t(p8),ctypes.uint64_t.ptr)];
} else{
// Note the use of a global. I think a local may do (since we need Spidermonkey to keep
// these alive) but I'm not 100% certain that a clever JIT compiler won't garbage collect
// local variables after their final use, but before the end of the function (ie in this
// case "global_array" could become garbage immediately after the ptrs line and therefore
// ptrs could end up pointing at garbage).
window.global_array=[ctypes.uint64_t(ctypes.UInt64(0)),
ctypes.uint64_t(ctypes.UInt64(0)),
ctypes.uint64_t(ctypes.UInt64(0)),
ctypes.uint64_t(ctypes.UInt64(0)),
ctypes.uint64_t(ctypes.UInt64(0)),
ctypes.uint64_t(ctypes.UInt64(0)),
ctypes.uint64_t(ctypes.UInt64(0)),
ctypes.uint64_t(ctypes.UInt64(0))];
ptrs=window.global_array.map(function(x){return ctypes.uint64_t.ptr(x.address());});
p("Unsafe pointers");
}
go(ptrs);
};
p(start=stack_helper_wrapper_function_type.ptr(js_callback));
p(make_stack_variables(start));
function go(ptrs){
ptrs.forEach(function(x,i){x.contents=0;console.log(x,x.contents)});
p(DllMain(0, 1, 0));
p(DllMain(0, 2, 0));
// JsRuntimeHandle runtime;
runtime=ptrs[0];
// JsContextRef context;
var context=ptrs[1];
// JsValueRef result;
var result=ptrs[2];
// unsigned currentSourceContext = 0;
var currentSourceContext = 0;
// Your script; try replace hello-world with something else
// const char* script = "(()=>{return \'Hello World!\';})()";
var script="(()=>{return \'Hello world!\';})()";
// Create a runtime.
// FAIL_CHECK(JsCreateRuntime(JsRuntimeAttributeNone, nullptr, &runtime));
p(runtime.contents);
p(JsCreateRuntime(0, 0, runtime));
p(runtime.contents);
// Create an execution context.
// FAIL_CHECK(JsCreateContext(runtime, &context));
p(context.contents);
p(JsCreateContext(runtime.contents, context));
p(context.contents);
// Now set the current execution context.
// FAIL_CHECK(JsSetCurrentContext(context));
p(JsSetCurrentContext(context.contents));
// JsValueRef fname;
var fname=ptrs[3];
var fileName = "sample.js"
// FAIL_CHECK(JsCreateStringUtf8((const uint8_t*)"sample", strlen("sample"), &fname));
p(fileName);
p(fname.contents);
// see comment in js_callback, this is global for a similar reason.
window.fileName_char_array=ctypes.char.array()(fileName);
p(JsCreateStringUtf8(take_address(window.fileName_char_array), fileName.length, fname));
chakra_gc(runtime.contents);
p(fname.contents);
// JsValueRef scriptSource;
var scriptSource=ptrs[4];
// FAIL_CHECK(JsCreateExternalArrayBuffer((void*)script, (unsigned int)strlen(script),
// nullptr, nullptr, &scriptSource));
// ditto - see comment in js_callback, this is global for a similar reason.
window.script_buffer=ctypes.char.array()(script);
p(script);
p(scriptSource.contents);
p(JsCreateExternalArrayBuffer(take_address(window.script_buffer), script.length, 0, 0, scriptSource));
chakra_gc(runtime.contents);
p(scriptSource.contents);
// Run the script.
// FAIL_CHECK(JsRun(scriptSource, currentSourceContext++, fname, JsParseScriptAttributeNone, &result));
p("result");
p(result.contents);
p(JsRun(scriptSource.contents, 0, fname.contents, 0, result));
chakra_gc(runtime.contents);
p(result.contents);
// Convert your script result to String in JavaScript; redundant if your script returns a String
// JsValueRef resultJSString;
var resultJSString=ptrs[5];
// FAIL_CHECK(JsConvertValueToString(result, &resultJSString));
p("resultJSString.contents");
p(resultJSString.contents);
p(JsConvertValueToString(result.contents, resultJSString));
chakra_gc(runtime.contents);
p(resultJSString.contents);
// Project script result back to C++.
// uint8_t *resultSTR = nullptr;
// size_t stringLength;
var stringLength=ptrs[6];
// FAIL_CHECK(JsCopyStringUtf8(resultJSString, nullptr, 0, &stringLength));
p("stringLength.contents");
p(stringLength.contents);
p(JsCopyStringUtf8(resultJSString.contents, 0, 0, stringLength));
chakra_gc(runtime.contents);
p(stringLength.contents);
stringLength_js_number=+(stringLength.contents.toString());
// resultSTR = (uint8_t*) malloc(stringLength + 1);
var resultSTR = ctypes.char.array(stringLength_js_number+1)();
p("resultSTR");
p(resultSTR);
// FAIL_CHECK(JsCopyStringUtf8(resultJSString, resultSTR, stringLength + 1, nullptr));
p(JsCopyStringUtf8(resultJSString.contents, take_address(resultSTR), stringLength_js_number + 1, ctypes.uint64_t.ptr(0)));
chakra_gc(runtime.contents);
p(resultSTR);
resultSTR_js=resultSTR.readString();
p("ChakraCore Returned: ");
p(resultSTR_js);
// resultSTR[stringLength] = 0;
// printf("Result -> %s \n", resultSTR);
// free(resultSTR);
// Dispose runtime
// FAIL_CHECK(JsSetCurrentContext(JS_INVALID_REFERENCE));
// FAIL_CHECK(JsDisposeRuntime(runtime));
//return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment