v.ast, v.checker: fix infinite loops and segfaults in generic instantiation and cyclic aliases - #28005
v.ast, v.checker: fix infinite loops and segfaults in generic instantiation and cyclic aliases#28005tailsmails wants to merge 15 commits into
Conversation
Limit the depth of alias resolution to prevent infinite loops.
Add a depth limit check for generic instantiation.
Reduced the cutoff limit for generic function post-processing iterations from 1000 to 50. Updated loop variables to be mutable for variants in sum type declarations.
Added tests for nested generic functions, structs, type aliases, and sum types.
|
|
I have also found a 5th related segmentation fault in convert_generic_type with this recursive alias pattern, and I will address it in a separate PR soon.. |
|
Update: Found another related segfault while testing this branch. It happens when resolving recursive generic function pointer aliases like this: type Func[T] = fn (f Func[[]T]) T
fn main() {
_ := Func[int](none)
}On master, this crashes with a segmentation fault in What was going wrong: The fix: |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab9edcefea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if t.fn_generic_types[fn_name].len > generic_fn_inst_cutoff_limit { | ||
| t.panic('generic function instantiation limit exceeded') |
There was a problem hiding this comment.
Replace the total generic-instantiation cap with cycle detection
When a valid program specializes one generic function for 257 distinct concrete type lists, the map entry reaches length 257 and every subsequent call to register_fn_concrete_types—even a duplicate that would otherwise return false—panics here. The number of finite specializations is not evidence of recursive expansion, and generated serializers or dispatch code can legitimately exceed this threshold, so this turns valid compilation into a compiler crash; guard actual recursive growth instead of the accumulated specialization count.
Useful? React with 👍 / 👎.
| if inst_name.len > generic_inst_name_len_cutoff_limit { | ||
| t.panic('generic instantiation name limit exceeded') |
There was a problem hiding this comment.
Allow finite generic instance names longer than 1 KiB
A finite generic instantiation can exceed 1,024 bytes without containing a cycle—for example, a generated generic with many concrete parameters or nested qualified type names—yet it now unconditionally panics here. Because V does not otherwise impose this identifier-length restriction, valid source that previously compiled now crashes; recursion should be bounded by the instantiation graph rather than the rendered name length.
Useful? React with 👍 / 👎.
| const generic_fn_cutoff_limit_per_fn = 10_000 // how many times post_process_generic_fns, can visit the same function before bailing out | ||
|
|
||
| const generic_fn_postprocess_iterations_cutoff_limit = 1_000_000 | ||
| const generic_fn_postprocess_iterations_cutoff_limit = 50 |
There was a problem hiding this comment.
Preserve finite generic chains beyond 50 rechecks
A valid chain of more than roughly 50 generic functions can require more than 50 passes when declarations are ordered from the innermost function to the outermost and only the outermost function is initially instantiated: each pass discovers the concrete type for a function that was already visited earlier in that pass. This fixed cutoff therefore emits an infinite-instantiation error for a finite chain that previously converged; use repeated-state/no-progress detection or a limit that does not reject ordinary finite propagation.
Useful? React with 👍 / 👎.
|
I tweaked the constants a bit.
|
Raise generic post-process limit to 256The old limit of 50 was too tight for complex nested generics, leading to false-positive loop errors on valid code. At the same time, we can't set it too high because actual infinite loops will hang the compiler and spike CPU usage. |
I think maybe 128 is safer than 256 |
What this PR does and why
This PR fixes multiple infinite loop hangs (causing 100% CPU usage) and segmentation faults (caused by unbounded recursion stack overflow) during type checking of recursive generics, deeply nested types, and cyclic type aliases.
The fixes introduce standard recursion guards and cutoff limits (similar to how limits are guarded in other compilers) to prevent compiler hangs and crashes, while ensuring complex, valid user code still compiles smoothly.
Changes and Fixes
v.checker (Generic Function Loop Limit):
generic_fn_postprocess_iterations_cutoff_limitfrom1,000,000to a safer50. This prevents permanent compiler hangs and high CPU utilization when resolving circular/recursive generic functions.v.ast (Generic Struct Instantiation Depth):
generic_inst_depth_cutoff_limit = 256inunwrap_generic_type_ex_with_depthto stop unbounded recursion and prevent stack overflows (resulting in segfaults) for deeply nested generic structs.v.ast (Type Alias Loop Limit):
forloop infully_unaliased_typeusingalias_unwrap_depth_cutoff_limit = 100to prevent infinite loops and hangs on circular type aliases.v.checker (Generic Sum Type Validation):
[and<) from the symbol name insum_type_declbefore comparing it to the node name, ensuring that generic sum types cannot hold themselves and bypass circular reference verification.Tests
Add a regression test file verifying that deeply nested (yet valid) generic structures, nested generic functions, and type alias chains still compile and evaluate correctly under the new limits: