How to Fix: Off-by-One Enum discriminant validation in component adapter
Off-by-One Enum Discriminant Validation in a Component Adapter
An enum discriminant that slips one value past its valid range can corrupt the contract between components, and this issue is exactly that: the adapter accepts or mishandles a discriminant equal to the enum length instead of trapping it as invalid.
Table of Contents
In the reported case, the component defines an enum with three cases:
(type $enum (enum "case0" "case1" "case2"))
That means the only valid discriminants are 0, 1, and 2. If a producer returns 3, the component adapter must reject it immediately. Accepting that value is an off-by-one validation bug.
Understanding the Root Cause
This bug usually appears when adapter validation uses the wrong upper-bound comparison. For an enum with n cases, the valid range is:
0 <= discriminant < n
But buggy logic often checks:
0 <= discriminant <= n
That single inclusive comparison allows the first invalid value through. In this issue, the enum has 3 members, so:
- Valid: 0, 1, 2
- Invalid: 3
If the adapter tests discriminant <= case_count instead of discriminant < case_count, discriminant 3 is incorrectly treated as valid.
At the implementation level, this often happens in one of these places:
- The lifting/lowering adapter path converts a raw integer into an enum variant.
- A helper method validates a decoded discriminant using an inclusive bound.
- A length-derived maximum is interpreted as the last valid index instead of the total count.
Conceptually, the difference is simple:
case_count = 3
last_valid_index = 2
invalid_first_index = 3
Using case_count as though it were the last valid index creates the exact failure shown by the test.
Step-by-Step Solution
The fix is to validate enum discriminants using an exclusive upper bound and trap on any out-of-range value before materializing the variant.
1. Locate the enum validation path
Find the code in the component adapter responsible for decoding or validating enum values. It may appear in logic that handles canonical ABI conversion, variant decoding, or adapter lowering/lifting.
Look for patterns like:
if discriminant > case_count {
trap()
}
or:
if discriminant <= case_count {
accept()
}
Both are suspicious in an enum validator.
2. Replace inclusive validation with exclusive validation
The correct logic is:
if discriminant >= case_count {
trap()
}
In Rust-style pseudocode:
fn validate_enum_discriminant(discriminant: u32, case_count: u32) -> Result<(), Trap> {
if discriminant >= case_count {
return Err(Trap::InvalidEnumDiscriminant {
discriminant,
case_count,
});
}
Ok(())
}
If the existing code computes a max index, make sure it does not accidentally underflow when the enum is empty in generic code paths:
fn validate_enum_discriminant(discriminant: u32, case_count: u32) -> Result<(), Trap> {
if case_count == 0 {
return Err(Trap::InvalidTypeState);
}
if discriminant >= case_count {
return Err(Trap::InvalidEnumDiscriminant {
discriminant,
case_count,
});
}
Ok(())
}
Even if empty enums are impossible by type construction, this style keeps helper code safe and explicit.
3. Ensure trapping happens before adaptation continues
Do not defer the failure until later pattern matching or array access. The adapter should reject the invalid value at the boundary:
let disc = read_discriminant_from_component()?;
validate_enum_discriminant(disc, enum_cases.len() as u32)?;
let case = &enum_cases[disc as usize];
This ordering matters. If indexing or branching happens first, the runtime may panic, misroute execution, or produce a confusing error instead of the required trap.
4. Add a regression test for the exact boundary
Create a test that returns the first invalid discriminant: exactly the enum case count.
(assert_trap
(component
(type $enum (enum "case0" "case1" "case2"))
;; Returns invalid discriminant 3 (valid range: 0-2)
(component $producer
;; producer definition here
)
;; adapter wiring here
)
"invalid enum discriminant")
Also add a positive boundary test to prove the highest valid value still succeeds:
(assert_return
(component
(type $enum (enum "case0" "case1" "case2"))
;; Returns valid discriminant 2
(component $producer
;; producer definition here
)
))
Together, these tests verify both sides of the boundary.
5. Audit similar validators
If this bug exists for enums, it may also exist in related variant discriminant or case-index validation logic. Search for comparisons involving counts and indexes:
<=
>
len() as u32
case_count
variant_count
Then normalize them to the correct rule:
valid index => index < count
invalid index => index >= count
6. Example patch shape
Here is a compact before-and-after example.
// Before
if discriminant > case_count {
return Err(trap_invalid_enum(discriminant));
}
// After
if discriminant >= case_count {
return Err(trap_invalid_enum(discriminant));
}
If the code currently branches by direct lookup, make it explicit:
// Better
let case_count = cases.len() as u32;
if discriminant >= case_count {
return Err(trap_invalid_enum(discriminant));
}
let selected = &cases[discriminant as usize];
Common Edge Cases
- Highest valid discriminant rejected: After fixing the bug, verify that n – 1 still works. A rushed patch can accidentally change the condition to discriminant + 1 >= count or introduce conversion issues.
- Signed vs unsigned conversion: If discriminants are decoded into signed integers first, negative values may wrap or cast incorrectly. Validate after decoding into a consistent unsigned representation.
- Variant and enum confusion: Some runtimes share helpers between enum and variant handling. Confirm that payload-carrying variants use the same correct discriminant bounds.
- Late failure instead of trap: If invalid values are used in indexing before validation, the runtime may panic with an unrelated error. The adapter must fail at the boundary with a deterministic trap.
- Mismatched canonical ABI assumptions: If one component encodes discriminants differently than the adapter expects, a valid-looking integer may still map incorrectly. Validate the ABI path end to end.
FAQ
Why is discriminant 3 invalid for a 3-case enum?
Because enum cases are zero-indexed. For three cases, the valid discriminants are 0, 1, and 2. The value 3 equals the case count, not a valid case index.
Should the adapter trap or coerce the invalid discriminant?
It should trap. Coercing an invalid discriminant would violate the component contract and could hide producer bugs or lead to incorrect behavior downstream.
How do I know the fix is complete?
Add regression tests for both boundaries: n – 1 must succeed and n must trap. Then audit all related discriminant validation paths, especially shared helpers used by enums and variants.
For maintainers, the practical rule is simple: whenever a component adapter validates a discriminant derived from a type with count members, the only correct check is discriminant < count. Anything else risks exactly this off-by-one bug.