Skip to content

register_handler auto-export heuristic uses naive substring match — string literals containing "export" break handler registration #39

@simongdavies

Description

@simongdavies

Bug Description

JsRuntime::register_handler() in lib.rs L146-L149 uses a raw .contains("export") check to decide whether to auto-append export { handler };:

let handler_script = if !handler_script.contains("export") {
    format!("{}\nexport {{ handler }};", handler_script)
} else {
    handler_script
};

This is a plain substring match against the entire script source, including string literals, comments, and variable names. If the handler script contains the word "export" anywhere — even inside a string, a comment, or an identifier like exportData — the auto-export is skipped, and the subsequent module.get("handler") call on line 166 fails because the module has no handler export.

Reproduction

// This handler WORKS — no "export" anywhere
const script_ok = `
function handler(event) {
    return { result: "hello" };
}
`;

// This handler FAILS — "export" appears in a string literal
const script_broken = `
function handler(event) {
    const xml = '<config mode="export">value</config>';
    return { result: xml };
}
`;

// This handler FAILS — "export" appears in a comment
const script_comment = `
function handler(event) {
    // TODO: export this data to CSV
    return { result: 42 };
}
`;

// This handler FAILS — "export" appears in a variable name
const script_ident = `
function handler(event) {
    const exportPath = "/tmp/out.csv";
    return { result: exportPath };
}
`;

In all three broken cases, the runtime sees "export" in the source, assumes the developer has already written an ES export statement, skips auto-appending export { handler };, and the handler module has no exports → runtime error.

Impact

This silently breaks handler registration for any script whose content (not just its module structure) happens to contain the substring "export". The failure mode is confusing — the error comes from module.get("handler") failing, not from anything obviously related to exports.

For downstream consumers that generate handler scripts programmatically (e.g. wrapping LLM-generated code inside a function handler(event) { ... } body), this is especially problematic because the script content is arbitrary and frequently contains strings with "export" in them.

Suggested Fix

Since register_handler is specifically documented as taking "a JavaScript module that exports a function named handler", the safest fix is to always append the export and let QuickJS handle duplicates:

// Option A: Always append — QuickJS will ignore a duplicate export
//           of the same binding if one already exists in the source.
let handler_script = format!("{}\nexport {{ handler }};", handler_script);

If QuickJS rejects duplicate exports, an alternative is to use a more precise heuristic:

// Option B: Regex that matches actual export syntax, not substrings
let has_export = regex::Regex::new(r"\bexport\s+(default\s+|async\s+)?(function|class|const|let|var|\{|\*)")
    .unwrap()
    .is_match(&handler_script);

let handler_script = if !has_export {
    format!("{}\nexport {{ handler }};", handler_script)
} else {
    handler_script
};

Environment

Crate: hyperlight-js-runtime
File: lib.rs, register_handler() method, line 146
Hyperlight version: 0.12.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions