linera-io/linera-protocol · error

Attempt to unflatten an invalid `char`

Error message

Attempt to unflatten an invalid `char`

What it means

A WIT `char` crosses the boundary as an i32 flat value holding the Unicode code point. Unflattening calls `char::from_u32(flat as u32)`, which returns None for values that are not Unicode scalar values — the surrogate range 0xD800-0xDFFF and anything above 0x10FFFF — and `.expect` panics. Receiving such a value means the guest produced a code point that Rust `char`s cannot represent, violating the WIT type contract.

Source

Thrown at linera-witty/src/primitive_types/simple_type.rs:71

                unflatten(flat)
            }
        }
    };
}

simple_type!(bool -> i32, 1, { |flat| flat != 0 });
simple_type!(i8 -> i32, 1);
simple_type!(i16 -> i32, 2);
simple_type!(i32 -> i32, 4);
simple_type!(i64 -> i64, 8);
simple_type!(u8 -> i32, 1);
simple_type!(u16 -> i32, 2);
simple_type!(u32 -> i32, 4);
simple_type!(u64 -> i64, 8);
simple_type!(f32 -> f32, 4);
simple_type!(f64 -> f64, 8);
simple_type!(char -> i32, 4,
    { |flat| char::from_u32(flat as u32).expect("Attempt to unflatten an invalid `char`") }
);

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Validate in the guest before passing/returning a char: `char::from_u32(v).ok_or(Error::InvalidCodePoint)`
  2. Fix surrogate handling: recombine UTF-16 surrogate pairs into one scalar value; never forward a lone surrogate
  3. If arbitrary u32 values must cross, declare the parameter as u32 in WIT and validate/convert to char on the host side

Example fix

// before (guest): forwards a raw code point unchecked
fn symbol(idx: u32) -> char {
    let code = table_lookup(idx); // may be 0xD800..=0xDFFF
    unsafe { char::from_u32_unchecked(code) } // host panics on unflatten
}

// after (guest): validate at the boundary
fn symbol(idx: u32) -> Result<char, InvalidChar> {
    char::from_u32(table_lookup(idx)).ok_or(InvalidChar)
}
Defensive patterns

Strategy: type-guard

Validate before calling

// guest: assert boundary inputs/outputs are valid scalar values
assert!(char::from_u32(code).is_some(), "invalid code point {code:#x}");

Type guard

fn is_valid_scalar(code: u32) -> bool { char::from_u32(code).is_some() }

Prevention

When it happens

Trigger: A guest passes 0xD800..=0xDFFF or >0x10FFFF where the WIT interface declares char; the guest builds its 'char' from raw u32 bits (e.g. UTF-16 surrogate pieces) without validation.

Common situations: Guests ported from C/JS that treat chars as plain 32-bit ints; incorrect UTF-16 surrogate-pair handling that forwards lone surrogates; fuzz tests feeding arbitrary u32 into char-typed entry points.

Related errors


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/4927ddbeaca0f7b2. Report an issue: GitHub.