HowTo: Embed and Sandbox User Defined Code

David Nichols edited this page Jun 26, 2016 · 4 revisions

HowTo: Embed and Sandbox User-Defined Code

Allow your application or script to be extended / instrumented by safely embedding, sandboxing, and executing user-defined code

Create the Program Logic Container

Embedding Qore is performed with the Program class which serves as the logic container for Qore code; sandboxing restrictions and control over the API present in the Program object are specified with a binary or'ed combination of parse options.

Sandboxing Restrictions

For example, to allow code to be embedded with the basic Qore language API but with only the possibility to execute logic (I/O, threading, external access, etc prohibited), then you can create your Program object with the PO_LOCKDOWN parse restriction as in the following example:

Program p(PO_LOCKDOWN);

Some other example sandboxing restriction/permission constants are listed in the following table:

Sandbox Option Description
PO_ALLOW_INJECTION Allows code/dependency injections in the Program container
PO_NO_DATABASE Prohibits access to DB functionality in the Program container
PO_NO_EXTERNAL_INFO Prohibits access to information about the external computing environment (see %no-external-info for more info)
PO_NO_EXTERNAL_PROCESS Prohibits access to external process functionality (ex: system(), exec(), backquote(), etc)
PO_NO_FILESYSTEM Disallows access to the filesystem
PO_NO_MODULES Disallows accessing or loading modules
PO_NO_NETWORK Prohibits accessing the network
PO_NO_PROCESS_CONTROL Prohibits access to functionality that affects the process as a whole (abort(), exit(), exec(), fork(), and many more)
PO_NO_TERMINAL_IO Disallows input or output to the terminal
PO_NO_THREAD_CONTROL Prohibits access to functionality related to starting or terminating threads, as well as thread-local data and managing thread resources

Note:

Define the Program's API

To restrict the user and system API symbols available to the Program container, use the PO_NO_API, PO_NO_SYSTEM_API, or the PO_NO_USER_API parse options. These parse options control the API that's imported into the Program container; by default all system symbols are imported and all user symbols declared public are imported; using the preceding parse options can override this behavior.

Note: System symbols that are imported into a Program container may not be accessible due to other parse restrictions; for example, if PO_NO_FILESYSTEM is set, then the ReadOnlyFile class cannot be used, even though the symbol is imported in the Program container.

A custom API can be provided to the embedded code by calling Program::importClass(), Program::importFunction(), and Program::importGlobalVariable() (among others) as in the following example:

Program pgm(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES|PO_NO_FILESYSTEM);
pgm.importClass("SecureFile");
pgm.importFunction("secure_hstat");

Import the User Code

Once the Program object has been created, code can then be imported into the object with the Program::parse() method.

Alternatively, to parse code from multiple sources into a Program object and maintain the source locations for that code, use the Program::parsePending() and Program::parseCommit() methods.

Note: All of the preceding methods have variants to accept a warning mask, which allows for a hash of warning information to be returned for potential warning logging and reporting.

For example:

# source_list is a list of hashes with "code" and "source_label" keys
Program sub get_program(list source_list) {
    # create the Program
    Program pgm(PO_NO_THREAD_CONTROL|PO_NO_PROCESS_CONTROL);

    # a list of warning information
    list wl = ();
 
    # add warnings to the list if they exist 
    code parse = sub (string code, string source_label) {
        *hash h = pgm.parsePending(code, source_label, WARN_DEFAULT|WARN_DUPLICATE_GLOBAL_VARS);
        if (h)
            wl += h;
    };

    # parse the code into the Program
    map parse($1.code, $1.source_label), code_list;

    # print warnings on stderr if "verbose" is set
    if (wl && verbose)
        map stderr.printf("WARNING: %s: %s: %s\n", get_ex_pos($1), $1.err, $1.desc), wl;

    return pgm;
}

Call the Embedded User Code

Once the code has been parsed and loaded into the Program object, then you can run it with Program::run(), or you can call functions with Program::callFunction() or Program::callFunctionArgs() as in the following example:

%new-style
%strict-args
%require-types
%enable-all-warnings

list source_list = (
    (
        "code": "string sub get_message() { return \"my message\"; }",
        "source_label": "my_code.q",
    ),
);
Program pgm = get_program(source_list);
string msg = pgm.callFunction("get_message");

Note: Program objects can be created and destroyed at runtime. Be aware that any objects created from a Program object that is destroyed will become invalid when the Program is destroyed. Object factories can be implemented in Program objects that persist at least as long as the lifetime of the objects created from them, or alternatively from user modules. User modules always persist for the entire lifetime of the process; it's not possible to unload or reload a user module once loaded (except in the special case of code injections which is meant for dependency injections and requires the PO_ALLOW_INJECTION parse option).

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.