nautechsystems/nautilus_trader · critical

Failed to convert C string to UTF-8

Error message

Failed to convert C string to UTF-8

What it means

uuid4_from_cstr in crates/core/src/ffi/uuid.rs:51 builds a UUID4 from a C string pointer, converting the CStr to &str with to_str().expect("Failed to convert C string to UTF-8") inside abort_on_panic (so the process aborts, not just unwinds). It panics when the pointed-to bytes are not valid UTF-8; note the abort_on_panic wrapper makes this failure harsher than the similar panics in ffi/string.rs.

Source

Thrown at crates/core/src/ffi/uuid.rs:51

    abort_on_panic(UUID4::new)
}

/// Returns a [`UUID4`] from C string pointer.
///
/// # Safety
///
/// Assumes `ptr` is a valid C string pointer.
///
/// # Panics
///
/// Panics if `ptr` cannot be cast to a valid C string.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn uuid4_from_cstr(ptr: *const c_char) -> UUID4 {
    abort_on_panic(|| {
        assert!(!ptr.is_null(), "`ptr` was NULL");
        // SAFETY: Caller guarantees ptr is valid per function contract
        let cstr = unsafe { CStr::from_ptr(ptr) };
        let value = cstr.to_str().expect("Failed to convert C string to UTF-8");
        UUID4::from(value)
    })
}

/// Return a borrowed *null-terminated* UTF-8 C string representing `uuid`.
///
/// The pointer remains valid for as long as the input `UUID4` reference lives - callers **must
/// not** attempt to free it.
#[unsafe(no_mangle)]
pub extern "C" fn uuid4_to_cstr(uuid: &UUID4) -> *const c_char {
    abort_on_panic(|| uuid.to_cstr().as_ptr())
}

/// Compare two UUID values, returning `1` when they are equal and `0` otherwise.
#[unsafe(no_mangle)]
pub extern "C" fn uuid4_eq(lhs: &UUID4, rhs: &UUID4) -> u8 {
    abort_on_panic(|| u8::from(lhs == rhs))
}

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Serialize UUIDs as canonical lowercase hex strings (e.g. Python str(uuid.uuid4())) before crossing the boundary.
  2. Never pass raw 16-byte UUID binaries to this API — convert to the hex string representation first.
  3. Validate the string matches ^[0-9a-fA-F-]{36}$ on the producer side before the call.
  4. Because this aborts the process, add producer-side unit tests covering every ID emission path to keep bad bytes from reaching the FFI boundary.

Example fix

// before (Python producer)
struct.pack("16s", uuid.uuid4().bytes)      # raw binary, not UTF-8 text
// after
str(uuid.uuid4()).encode("utf-8")           # '550e8400-e29b-41d4-...' hex string
Defensive patterns

Strategy: validation

Validate before calling

# Python producer
u = str(uuid.uuid4())
import re
assert re.fullmatch(r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", u)
send_to_ffi(u.encode("utf-8"))

Type guard

def is_uuid4_text(value: bytes) -> bool:
    import re, string
    try:
        s = value.decode("utf-8")
    except UnicodeDecodeError:
        return False
    return bool(re.fullmatch(r"[0-9a-fA-F-]{36}", s))

Prevention

When it happens

Trigger: Calling uuid4_from_cstr with a pointer to non-UTF-8 bytes: a binary/misinterpreted UUID representation (raw 16 bytes instead of the hex string form), legacy-encoded text, or truncated multi-byte data.

Common situations: Passing a UUID as raw 16-byte binary instead of its canonical 36-char hyphenated hex string; an ID field produced by a non-UTF-8 system; buffer corruption between producer and FFI call.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/aa7e6d8a74ad8d4b. Report an issue: GitHub.