-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
v.ast, v.checker: fix infinite loops and segfaults in generic instantiation and cyclic aliases #28005
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
v.ast, v.checker: fix infinite loops and segfaults in generic instantiation and cyclic aliases #28005
Changes from 13 commits
bfc956b
cc93f15
342bfe6
b9ebd72
f9ccc1b
cef9581
7a859cc
ca75f9a
43d8c4f
28924a8
be03e72
58e0fa6
ab9edce
489bc06
a936e3d
81a5f51
5a3c4b2
6879480
dc18b0b
b23be70
3723a7e
32fe5d7
1e3635c
8d9b055
243db77
f926618
fa81479
643052f
067d1ec
dc3b065
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,13 @@ import v.cflag | |
| import v.util | ||
| import v.token | ||
|
|
||
| const alias_unwrap_depth_cutoff_limit = 100 | ||
| const generic_inst_depth_cutoff_limit = 256 | ||
| const struct_fields_depth_cutoff_limit = 100 | ||
| const generic_fn_inst_cutoff_limit = 256 | ||
| const generic_inst_name_len_cutoff_limit = 1_024 | ||
| const max_postprocess_iterations = 100_000 | ||
|
|
||
| @[heap; minify] | ||
| pub struct UsedFeatures { | ||
| pub mut: | ||
|
|
@@ -97,6 +104,8 @@ pub mut: | |
| panic_handler FnPanicHandler = default_table_panic_handler | ||
| panic_userdata voidptr = unsafe { nil } // can be used to pass arbitrary data to panic_handler; | ||
| panic_npanics int | ||
| convert_generic_type_depth int | ||
| unwrap_generic_type_depth int | ||
| cur_fn &FnDecl = unsafe { nil } // previously stored in Checker.cur_fn and Gen.cur_fn | ||
| cur_lambda &LambdaExpr = unsafe { nil } // current lambda node | ||
| cur_concrete_types []Type // current concrete types, e.g. [int, string] | ||
|
|
@@ -1547,12 +1556,14 @@ pub fn (t &Table) are_payloads_alias_compatible(a Type, b Type) bool { | |
| pub fn (t &Table) fully_unaliased_type(typ Type) Type { | ||
| mut unaliased := typ | ||
| mut extra_flags := u32(typ) & 0xff00_0000 | ||
| for { | ||
| mut depth := 0 | ||
| for depth < alias_unwrap_depth_cutoff_limit { | ||
| sym := t.sym(unaliased) | ||
| if sym.info is Alias { | ||
| parent_typ := sym.info.parent_type | ||
| unaliased = Type(u32(parent_typ.set_nr_muls(parent_typ.nr_muls() + unaliased.nr_muls())) | extra_flags) | ||
| extra_flags |= u32(unaliased) & 0xff00_0000 | ||
| depth++ | ||
| continue | ||
| } | ||
| return unaliased | ||
|
|
@@ -2306,6 +2317,9 @@ pub fn (mut t Table) find_or_register_generic_inst(parent_typ Type, concrete_typ | |
| } | ||
| } | ||
| inst_name += ']' | ||
| if inst_name.len > generic_inst_name_len_cutoff_limit { | ||
| t.panic('generic instantiation name limit exceeded') | ||
| } | ||
| existing_idx := t.type_idxs[inst_name] | ||
| if existing_idx > 0 { | ||
| if t.type_symbols[existing_idx].kind == .placeholder { | ||
|
|
@@ -2409,6 +2423,9 @@ pub fn (mut t Table) register_fn_generic_types(fn_name string) { | |
| } | ||
|
|
||
| pub fn (mut t Table) register_fn_concrete_types(fn_name string, types []Type) bool { | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 Useful? React with 👍 / 👎. |
||
| } | ||
| if types.len == 0 { | ||
| return false | ||
| } | ||
|
|
@@ -3016,8 +3033,15 @@ pub fn (mut t Table) convert_generic_static_type_name(fn_name string, generic_na | |
| return void_type, fn_name | ||
| } | ||
|
|
||
| // convert_generic_type convert generics to real types (T => int) or other generics type. | ||
| pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []string, to_types []Type) ?Type { | ||
| t.convert_generic_type_depth++ | ||
| if t.convert_generic_type_depth > generic_inst_depth_cutoff_limit { | ||
| t.convert_generic_type_depth-- | ||
| return none | ||
| } | ||
| defer { | ||
| t.convert_generic_type_depth-- | ||
| } | ||
| if generic_names.len != to_types.len { | ||
| return none | ||
| } | ||
|
|
@@ -4013,11 +4037,22 @@ pub fn (mut t Table) unwrap_generic_type_ex(typ Type, generic_names []string, co | |
| } | ||
|
|
||
| fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []string, concrete_types []Type, recheck_concrete_types bool, depth_guard []string) Type { | ||
| t.unwrap_generic_type_depth++ | ||
| if t.unwrap_generic_type_depth > generic_inst_depth_cutoff_limit { | ||
| t.unwrap_generic_type_depth-- | ||
| t.panic('generic instantiation depth limit exceeded') | ||
| } | ||
| defer { | ||
| t.unwrap_generic_type_depth-- | ||
| } | ||
| mut final_concrete_types := []Type{} | ||
| mut fields := []StructField{} | ||
| mut nrt := '' | ||
| mut c_nrt := '' | ||
| mut new_depth_guard := []string{} | ||
| if depth_guard.len > generic_inst_depth_cutoff_limit { | ||
| t.panic('generic instantiation depth limit exceeded') | ||
| } | ||
| type_idx := typ.idx() | ||
| if type_idx == 0 || type_idx >= t.type_symbols.len { | ||
| return typ | ||
|
|
@@ -4049,7 +4084,8 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str | |
| return new_type(idx).derive_add_muls(typ).clear_flag(.generic) | ||
| } | ||
| Chan { | ||
| unwrap_typ := t.unwrap_generic_type(ts.info.elem_type, generic_names, concrete_types) | ||
| unwrap_typ := t.unwrap_generic_type_ex_with_depth(ts.info.elem_type, generic_names, | ||
| concrete_types, recheck_concrete_types, depth_guard) | ||
| idx := t.find_or_register_chan(unwrap_typ, unwrap_typ.nr_muls() > 0) | ||
| if idx <= 0 { | ||
| return typ | ||
|
|
@@ -4087,8 +4123,8 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str | |
| has_generic = true | ||
| } | ||
| if param.orig_typ.has_flag(.generic) || t.generic_type_names(param.orig_typ).len > 0 { | ||
| unwrapped_fn.params[i].orig_typ = t.unwrap_generic_type(param.orig_typ, | ||
| generic_names, concrete_types) | ||
| unwrapped_fn.params[i].orig_typ = t.unwrap_generic_type_ex_with_depth(param.orig_typ, | ||
| generic_names, concrete_types, recheck_concrete_types, depth_guard) | ||
| } | ||
| } | ||
| if unwrapped_fn.return_type.has_flag(.generic) | ||
|
|
@@ -4134,8 +4170,8 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str | |
| if !t_typ.has_flag(.generic) { | ||
| t_concrete_types << t_typ | ||
| } else { | ||
| t_concrete_types << t.unwrap_generic_type(t_typ, generic_names, | ||
| concrete_types) | ||
| t_concrete_types << t.unwrap_generic_type_ex_with_depth(t_typ, | ||
| generic_names, concrete_types, recheck_concrete_types, depth_guard) | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -4887,8 +4923,13 @@ fn (mut t Table) specialize_generic_fn_type_methods(parent_type Type, mut concre | |
|
|
||
| // generic struct instantiations to concrete types | ||
| pub fn (mut t Table) generic_insts_to_concrete() { | ||
| mut cnt := 0 | ||
| for mut sym in t.type_symbols { | ||
| if sym.kind == .generic_inst { | ||
| cnt++ | ||
| if cnt > max_postprocess_iterations { | ||
| t.panic('generic_insts_to_concrete limit exceeded') | ||
| } | ||
| info := sym.info as GenericInst | ||
| if info.parent_idx <= 0 || info.parent_idx >= t.type_symbols.len { | ||
| continue | ||
|
|
@@ -5040,6 +5081,29 @@ pub fn (mut t Table) generic_insts_to_concrete() { | |
| if parent_info.generic_types.len == info.concrete_types.len { | ||
| mut fields := parent_info.fields.clone() | ||
| mut variants := parent_info.variants.clone() | ||
|
|
||
| // Prevent circular sum types from causing infinite loops | ||
| for variant in variants { | ||
| mut sym_name := t.sym(variant).name.trim_string_left( | ||
| t.sym(variant).mod + '.') | ||
| if sym_name.contains('[') { | ||
| sym_name = sym_name.all_before('[') | ||
| } else if sym_name.contains('<') { | ||
| sym_name = sym_name.all_before('<') | ||
| } | ||
|
|
||
| mut parent_name := parent.name.trim_string_left(parent.mod + '.') | ||
| if parent_name.contains('[') { | ||
| parent_name = parent_name.all_before('[') | ||
| } else if parent_name.contains('<') { | ||
| parent_name = parent_name.all_before('<') | ||
| } | ||
|
|
||
| if sym_name == parent_name { | ||
| return | ||
| } | ||
| } | ||
|
|
||
| generic_names := t.get_generic_names(parent_info.generic_types) | ||
| for i in 0 .. fields.len { | ||
| if t_typ := t.convert_generic_type(fields[i].typ, generic_names, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,7 @@ const type_level_cutoff_limit = 40 // it is very rarely deeper than 4 | |
| const iface_level_cutoff_limit = 100 | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎. |
||
|
|
||
| fn has_ascii_upper(s string) bool { | ||
| for ch in s { | ||
|
|
@@ -804,6 +804,15 @@ pub fn (mut c Checker) check_files(ast_files []&ast.File) { | |
| if !c.need_recheck_generic_fns { | ||
| break | ||
| } | ||
| if post_process_generic_fns_iterations == generic_fn_postprocess_iterations_cutoff_limit { | ||
| if c.file.generic_fns.len > 0 { | ||
| c.error('generic function post processing reached the cutoff limit of ${generic_fn_postprocess_iterations_cutoff_limit} iterations, probably due to an infinite generic instantiation loop', | ||
| c.file.generic_fns[0].pos) | ||
| } else { | ||
| c.error('generic function post processing reached the cutoff limit of ${generic_fn_postprocess_iterations_cutoff_limit} iterations, probably due to an infinite generic instantiation loop', token.Pos{}) | ||
| } | ||
| break | ||
| } | ||
| c.need_recheck_generic_fns = false | ||
| post_process_generic_fns_iterations++ | ||
| } | ||
|
|
@@ -1179,7 +1188,7 @@ fn (mut c Checker) fn_type_decl(mut node ast.FnTypeDecl) { | |
| fn (mut c Checker) sum_type_decl(mut node ast.SumTypeDecl) { | ||
| c.check_valid_pascal_case(node.name, 'sum type', node.pos) | ||
| if c.pref.is_vls && c.pref.linfo.method == .definition { | ||
| for variant in node.variants { | ||
| for mut variant in node.variants { | ||
| if c.vls_is_the_node(variant.pos) { | ||
| typ_str := c.table.type_to_str(variant.typ) | ||
| if np := c.name_pos_gotodef(typ_str) { | ||
|
|
@@ -1192,7 +1201,7 @@ fn (mut c Checker) sum_type_decl(mut node ast.SumTypeDecl) { | |
| } | ||
| } | ||
| mut names_used := []string{} | ||
| for variant in node.variants { | ||
| for mut variant in node.variants { | ||
| c.ensure_type_exists(variant.typ, variant.pos) | ||
| sym := c.table.sym(variant.typ) | ||
| if variant.typ.is_ptr() || (sym.info is ast.Alias && sym.info.parent_type.is_ptr()) { | ||
|
|
@@ -1263,8 +1272,16 @@ and use a reference to the sum type instead: `var := &${node.name}(${variant_nam | |
| } | ||
| c.check_any_type(variant.typ, sym, variant.pos) | ||
|
|
||
| if sym.name.trim_string_left(sym.mod + '.') == node.name { | ||
| mut clean_sym_name := sym.name.trim_string_left(sym.mod + '.') | ||
| if clean_sym_name.contains('[') { | ||
| clean_sym_name = clean_sym_name.all_before('[') | ||
| } else if clean_sym_name.contains('<') { | ||
| clean_sym_name = clean_sym_name.all_before('<') | ||
| } | ||
| if clean_sym_name == node.name { | ||
| c.error('sum type cannot hold itself', variant.pos) | ||
| variant.typ = ast.void_type | ||
| continue | ||
| } else if sym.kind == .sum_type && sym.info is ast.SumType { | ||
| // Check for circular references through other sum types | ||
| mut visited := map[int]bool{} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| fn generic_level_1[T](val T) T { | ||
| return val | ||
| } | ||
|
|
||
| fn generic_level_2[T](val T) T { | ||
| return generic_level_1[T](val) | ||
| } | ||
|
|
||
| fn generic_level_3[T](val T) T { | ||
| return generic_level_2[T](val) | ||
| } | ||
|
|
||
| fn generic_level_4[T](val T) T { | ||
| return generic_level_3[T](val) | ||
| } | ||
|
|
||
| fn test_valid_nested_generic_functions() { | ||
| res_int := generic_level_4[int](42) | ||
| assert res_int == 42 | ||
|
|
||
| res_str := generic_level_4[string]('Vlang') | ||
| assert res_str == 'Vlang' | ||
| } | ||
|
|
||
| struct Box[T] { | ||
| pub: | ||
| val T | ||
| } | ||
|
|
||
| fn test_valid_nested_generic_structs() { | ||
| b1 := Box[int]{ | ||
| val: 100 | ||
| } | ||
| b2 := Box[Box[int]]{ | ||
| val: b1 | ||
| } | ||
| b3 := Box[Box[Box[int]]]{ | ||
| val: b2 | ||
| } | ||
| b4 := Box[Box[Box[Box[int]]]]{ | ||
| val: b3 | ||
| } | ||
|
|
||
| assert b4.val.val.val.val == 100 | ||
| } | ||
|
|
||
| type Alias1 = int | ||
| type Alias2 = Alias1 | ||
| type Alias3 = Alias2 | ||
| type Alias4 = Alias3 | ||
| type Alias5 = Alias4 | ||
|
|
||
| fn test_valid_type_alias_chain() { | ||
| mut num := Alias5(10) | ||
| assert num == Alias5(10) | ||
|
|
||
| num += Alias5(20) | ||
| assert num == Alias5(30) | ||
| } | ||
|
|
||
| struct Some[T] { | ||
| pub: | ||
| val T | ||
| } | ||
|
|
||
| struct None {} | ||
|
|
||
| type MyOption[T] = None | Some[T] | ||
| type ComplexResult[T, E] = E | Some[T] | ||
|
|
||
| fn test_valid_generic_sum_types() { | ||
| opt_some := MyOption[int](Some[int]{ | ||
| val: 99 | ||
| }) | ||
| if opt_some is Some[int] { | ||
| assert opt_some.val == 99 | ||
| } else { | ||
| assert false | ||
| } | ||
|
|
||
| opt_none := MyOption[string](None{}) | ||
| if opt_none is None { | ||
| assert true | ||
| } else { | ||
| assert false | ||
| } | ||
|
|
||
| res := ComplexResult[int, string](Some[int]{ | ||
| val: 500 | ||
| }) | ||
| if res is Some[int] { | ||
| assert res.val == 500 | ||
| } else { | ||
| assert false | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.