influxdata/influxdb · error · anyhow::Error

plugin code contains null bytes

Error message

plugin code contains null bytes

What it means

load_plugin_function converts plugin source code to a CString for py.run(); CString::new fails if the string contains interior NUL bytes, and the error is wrapped with 'plugin code contains null bytes'. Embedded NULs would truncate the Python source, so they are rejected outright.

Solutions

  1. Verify the plugin file is valid UTF-8 Python source (file command / reading as text) and not a binary or .pyc.
  2. Strip or reject NUL bytes before loading: if '\x00' in code: raise/sanitize.
  3. Re-download or re-deploy the plugin from a trusted source to rule out corruption.

Example fix

# before
code = open(path, 'rb').read()
# after
code = open(path, 'r', encoding='utf-8').read()
assert '\x00' not in code, 'plugin source contains NUL bytes'
Defensive patterns

Strategy: validation

Validate before calling

if '\x00' in code:
    raise ValueError('plugin source contains NUL bytes')  # reject before load

Try / catch

# caller-side
catch the anyhow error whose context is 'plugin code contains null bytes' and skip/redeploy the plugin

Prevention

When it happens

Trigger: Calling load_plugin_function (directly or via execute_wal_flush_trigger / execute_schedule_trigger / execute_request_trigger) with plugin code read from a binary file, a corrupt download, or a string that accidentally includes '\x00'.

Common situations: Pointing the plugin loader at a compiled .pyc or binary file instead of source; file corruption during transfer; building code strings via concatenation that introduces NUL bytes.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of influxdata/influxdb@06200ef96b (2026-09-19). Data as JSON: /api/errors/6510aef825152816. Report an issue: GitHub.

Appendix: source

Thrown at influxdb3_py_api/src/system_py.rs:292

/// For multi-file plugins (when `plugin_root` is `Some`), imports the module and retrieves
/// the function. For single-file plugins, executes the code in an isolated namespace and
/// retrieves the function.
fn load_plugin_function<'py>(
    py: Python<'py>,
    code: &str,
    plugin_root: Option<&Path>,
    call_site: &str,
    missing_fn_error: ExecutePluginError,
) -> Result<Bound<'py, PyAny>, ExecutePluginError> {
    if let Some(root_path) = plugin_root {
        load_function_from_module(py, root_path, call_site, missing_fn_error)
    } else {
        // Create isolated globals for this plugin execution. Without this, single-file
        // plugins would share __main__'s namespace and could overwrite each other's
        // function definitions during concurrent execution.
        let globals = PyDict::new(py);
        let code = CString::new(code)
            .map_err(|e| anyhow::Error::new(e).context("plugin code contains null bytes"))?;
        let call_site = CString::new(call_site)
            .map_err(|e| anyhow::Error::new(e).context("call site contains null bytes"))?;
        py.run(&code, Some(&globals), None)
            .map_err(anyhow::Error::from)?;
        py.eval(&call_site, Some(&globals), None)
            .map_err(|_| missing_fn_error)
    }
}

/// Serializes temporary `sys.path` mutation during multi-file plugin imports.
static SYS_PATH_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));

/// True when `err` is a `ModuleNotFoundError` for `module_name` itself, so no
/// plugin code has run and re-importing is side-effect free.
fn is_module_not_found(py: Python<'_>, err: &PyErr, module_name: &str) -> bool {
    err.is_instance_of::<PyModuleNotFoundError>(py)
        && err
            .value(py)

View on GitHub (pinned to 06200ef96b)