diff --git a/vlib/v/ast/table.v b/vlib/v/ast/table.v index 4b1bca1acc8fbe..5879099fb66826 100644 --- a/vlib/v/ast/table.v +++ b/vlib/v/ast/table.v @@ -8,6 +8,12 @@ import v.cflag import v.util import v.token +const alias_unwrap_depth_cutoff_limit = 100 +const generic_inst_depth_cutoff_limit = 256 +const generic_fn_inst_cutoff_limit = 4_096 +const generic_inst_name_len_cutoff_limit = 8_192 +const max_postprocess_iterations_default = 100_000 + @[heap; minify] pub struct UsedFeatures { pub mut: @@ -86,6 +92,11 @@ pub mut: link_flag_segments []LinkFlagSegment redefined_fns []string fn_generic_types map[string][][]Type // for generic functions + generic_fn_inst_limit int = generic_fn_inst_cutoff_limit // wired from pref by builder; user-tunable: `-generic-fn-inst-limit` + generic_inst_name_len_limit int = generic_inst_name_len_cutoff_limit // `-generic-inst-name-len-limit` + generic_inst_depth_limit int = generic_inst_depth_cutoff_limit // `-generic-inst-depth-limit` + alias_unwrap_depth_limit int = alias_unwrap_depth_cutoff_limit // `-alias-unwrap-depth-limit` + max_postprocess_iterations int = max_postprocess_iterations_default // `-max-postprocess-iterations` structured_receiver_methods map[string][]Fn interfaces map[int]InterfaceDecl sumtypes map[int]SumTypeDecl @@ -1547,12 +1558,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 < t.alias_unwrap_depth_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 +2319,9 @@ pub fn (mut t Table) find_or_register_generic_inst(parent_typ Type, concrete_typ } } inst_name += ']' + if inst_name.len > t.generic_inst_name_len_limit { + t.panic('generic instantiation name limit ${t.generic_inst_name_len_limit} exceeded (override with `-generic-inst-name-len-limit`)') + } existing_idx := t.type_idxs[inst_name] if existing_idx > 0 { if t.type_symbols[existing_idx].kind == .placeholder { @@ -2409,6 +2425,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 > t.generic_fn_inst_limit { + t.panic('generic function instantiation limit ${t.generic_fn_inst_limit} exceeded (override with `-generic-fn-inst-limit`)') + } if types.len == 0 { return false } @@ -3016,8 +3035,14 @@ 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 { + return t.convert_generic_type_with_depth(generic_type, generic_names, to_types, 0) +} + +fn (mut t Table) convert_generic_type_with_depth(generic_type Type, generic_names []string, to_types []Type, depth int) ?Type { + if depth > t.generic_inst_depth_limit { + return none + } if generic_names.len != to_types.len { return none } @@ -3059,7 +3084,9 @@ pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []str match mut sym.info { Array { dims, elem_type := t.get_array_dims(sym.info) - if typ := t.convert_generic_type(elem_type, generic_names, to_types) { + if typ := t.convert_generic_type_with_depth(elem_type, generic_names, to_types, depth + + 1) + { idx := t.find_or_register_array_with_dims(typ, dims) if typ.has_flag(.generic) { return new_type(idx).derive_add_muls(generic_type).set_flag(.generic) @@ -3069,7 +3096,9 @@ pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []str } } ArrayFixed { - if typ := t.convert_generic_type(sym.info.elem_type, generic_names, to_types) { + if typ := t.convert_generic_type_with_depth(sym.info.elem_type, generic_names, + to_types, depth + 1) + { idx := t.find_or_register_array_fixed(typ, sym.info.size, None{}, false) if typ.has_flag(.generic) { return new_type(idx).derive_add_muls(generic_type).set_flag(.generic) @@ -3079,7 +3108,9 @@ pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []str } } Chan { - if typ := t.convert_generic_type(sym.info.elem_type, generic_names, to_types) { + if typ := t.convert_generic_type_with_depth(sym.info.elem_type, generic_names, + to_types, depth + 1) + { idx := t.find_or_register_chan(typ, typ.nr_muls() > 0) if typ.has_flag(.generic) { return new_type(idx).derive_add_muls(generic_type).set_flag(.generic) @@ -3089,7 +3120,9 @@ pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []str } } Thread { - if typ := t.convert_generic_type(sym.info.return_type, generic_names, to_types) { + if typ := t.convert_generic_type_with_depth(sym.info.return_type, generic_names, + to_types, depth + 1) + { idx := t.find_or_register_thread(typ) if typ.has_flag(.generic) { return new_type(idx).derive_add_muls(generic_type).set_flag(.generic) @@ -3106,11 +3139,13 @@ pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []str || t.generic_type_names(func.return_type).len > 0 || (return_type_sym.kind == .generic_inst && (return_type_sym.info as GenericInst).concrete_types.any(it.has_flag(.generic))) { - if typ := t.convert_generic_type(func.return_type, generic_names, to_types) { + if typ := t.convert_generic_type_with_depth(func.return_type, generic_names, + to_types, depth + 1) + { func.return_type = typ } else { - func.return_type = t.unwrap_generic_type_ex(func.return_type, generic_names, - to_types, true) + func.return_type = t.unwrap_generic_type_ex_with_depth(func.return_type, + generic_names, to_types, true, [], depth + 1) } if func.return_type.has_flag(.generic) || t.generic_type_names(func.return_type).len > 0 { @@ -3120,22 +3155,27 @@ pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []str func.params = func.params.clone() for mut param in func.params { orig_param_type := param.typ - if typ := t.convert_generic_param_type(param, generic_names, to_types) { + if typ := t.convert_generic_param_type_with_depth(param, generic_names, to_types, + + depth + 1) + { param.typ = typ } if t.sym(param.typ).kind == .placeholder { - param.typ = - t.unwrap_generic_type_ex(orig_param_type, generic_names, to_types, true) + param.typ = t.unwrap_generic_type_ex_with_depth(orig_param_type, generic_names, + to_types, true, [], depth + 1) } if param.typ.has_flag(.generic) || t.generic_type_names(param.typ).len > 0 { has_generic = true } if param.orig_typ.has_flag(.generic) || t.generic_type_names(param.orig_typ).len > 0 { - if otyp := t.convert_generic_type(param.orig_typ, generic_names, to_types) { + if otyp := t.convert_generic_type_with_depth(param.orig_typ, generic_names, + to_types, depth + 1) + { param.orig_typ = otyp } else { - param.orig_typ = t.unwrap_generic_type_ex(param.orig_typ, generic_names, - to_types, true) + param.orig_typ = t.unwrap_generic_type_ex_with_depth(param.orig_typ, + generic_names, to_types, true, [], depth + 1) } } } @@ -3159,7 +3199,10 @@ pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []str mut concrete_types := sym.info.concrete_types.clone() mut type_changed := false for i, concrete_type in concrete_types { - if typ := t.convert_generic_type(concrete_type, generic_names, to_types) { + if typ := t.convert_generic_type_with_depth(concrete_type, generic_names, to_types, + + depth + 1) + { concrete_types[i] = typ type_changed = true } @@ -3176,7 +3219,10 @@ pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []str mut types := []Type{} mut type_changed := false for ret_type in sym.info.types { - if typ := t.convert_generic_type(ret_type, generic_names, to_types) { + if typ := t.convert_generic_type_with_depth(ret_type, generic_names, to_types, + + depth + 1) + { types << typ type_changed = true } else { @@ -3196,11 +3242,16 @@ pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []str mut type_changed := false mut unwrapped_key_type := sym.info.key_type mut unwrapped_value_type := sym.info.value_type - if typ := t.convert_generic_type(sym.info.key_type, generic_names, to_types) { + if typ := t.convert_generic_type_with_depth(sym.info.key_type, generic_names, to_types, + + depth + 1) + { unwrapped_key_type = typ type_changed = true } - if typ := t.convert_generic_type(sym.info.value_type, generic_names, to_types) { + if typ := t.convert_generic_type_with_depth(sym.info.value_type, generic_names, + to_types, depth + 1) + { unwrapped_value_type = typ type_changed = true } @@ -3236,15 +3287,17 @@ pub fn (mut t Table) convert_generic_type(generic_type Type, generic_names []str if !t_typ.has_flag(.generic) { t_to_types << t_typ } else { - if tt := t.convert_generic_type(t_typ, generic_names, to_types) { + if tt := t.convert_generic_type_with_depth(t_typ, generic_names, + to_types, depth + 1) + { t_to_types << tt } } } } for i in 0 .. sym.info.generic_types.len { - if ct := t.convert_generic_type(sym.info.generic_types[i], t_generic_names, - t_to_types) + if ct := t.convert_generic_type_with_depth(sym.info.generic_types[i], + t_generic_names, t_to_types, depth + 1) { converted_types << ct gts := t.sym(ct) @@ -3373,13 +3426,17 @@ fn (mut t Table) lower_mut_param_type(typ Type, orig_typ ...Type) Type { } pub fn (mut t Table) convert_generic_param_type(param Param, generic_names []string, to_types []Type) ?Type { + return t.convert_generic_param_type_with_depth(param, generic_names, to_types, 0) +} + +fn (mut t Table) convert_generic_param_type_with_depth(param Param, generic_names []string, to_types []Type, depth int) ?Type { if param.is_mut && param.orig_typ != 0 && param.orig_typ.has_flag(.generic) && to_types.all(!it.has_flag(.generic)) { - if typ := t.convert_generic_type(param.orig_typ, generic_names, to_types) { + if typ := t.convert_generic_type_with_depth(param.orig_typ, generic_names, to_types, depth) { return t.lower_mut_param_type(typ, param.orig_typ) } } - return t.convert_generic_type(param.typ, generic_names, to_types) + return t.convert_generic_type_with_depth(param.typ, generic_names, to_types, depth) } // type_contains_placeholder returns true if the given type or any of its inner @@ -3413,12 +3470,17 @@ pub fn (t &Table) type_contains_placeholder(typ Type) bool { } pub fn (mut t Table) unwrap_generic_param_type(param Param, generic_names []string, concrete_types []Type) Type { + return t.unwrap_generic_param_type_with_depth(param, generic_names, concrete_types, 0) +} + +fn (mut t Table) unwrap_generic_param_type_with_depth(param Param, generic_names []string, concrete_types []Type, depth int) Type { if param.is_mut && param.orig_typ != 0 && param.orig_typ.has_flag(.generic) && concrete_types.all(!it.has_flag(.generic)) { - return t.lower_mut_param_type(t.unwrap_generic_type(param.orig_typ, generic_names, - concrete_types)) + return t.lower_mut_param_type(t.unwrap_generic_type_ex_with_depth(param.orig_typ, + generic_names, concrete_types, false, [], depth)) } - return t.unwrap_generic_type(param.typ, generic_names, concrete_types) + return t.unwrap_generic_type_ex_with_depth(param.typ, generic_names, concrete_types, false, [], + depth) } // convert_generic_expr_type resolves generic placeholders stored inside expression metadata. @@ -4009,15 +4071,21 @@ pub fn (mut t Table) unwrap_generic_type(typ Type, generic_names []string, concr // unwrap_generic_type_ex resolves generic symbols to concrete types and can recheck nested concrete fields. pub fn (mut t Table) unwrap_generic_type_ex(typ Type, generic_names []string, concrete_types []Type, recheck_concrete_types bool) Type { return t.unwrap_generic_type_ex_with_depth(typ, generic_names, concrete_types, - recheck_concrete_types, []string{}) + recheck_concrete_types, [], 0) } -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 { +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, depth int) Type { + if depth > t.generic_inst_depth_limit { + t.panic('generic instantiation depth limit ${t.generic_inst_depth_limit} exceeded (override with `-generic-inst-depth-limit`)') + } mut final_concrete_types := []Type{} mut fields := []StructField{} mut nrt := '' mut c_nrt := '' mut new_depth_guard := []string{} + if depth_guard.len > t.generic_inst_depth_limit { + t.panic('generic instantiation depth limit ${t.generic_inst_depth_limit} exceeded (override with `-generic-inst-depth-limit`)') + } type_idx := typ.idx() if type_idx == 0 || type_idx >= t.type_symbols.len { return typ @@ -4032,7 +4100,7 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str Array { dims, elem_type := t.get_array_dims(ts.info) unwrap_typ := t.unwrap_generic_type_ex_with_depth(elem_type, generic_names, - concrete_types, recheck_concrete_types, depth_guard) + concrete_types, recheck_concrete_types, depth_guard, depth + 1) idx := t.find_or_register_array_with_dims(unwrap_typ, dims) if idx <= 0 { return typ @@ -4041,7 +4109,7 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str } ArrayFixed { unwrap_typ := t.unwrap_generic_type_ex_with_depth(ts.info.elem_type, generic_names, - concrete_types, recheck_concrete_types, depth_guard) + concrete_types, recheck_concrete_types, depth_guard, depth + 1) idx := t.find_or_register_array_fixed(unwrap_typ, ts.info.size, None{}, false) if idx <= 0 { return typ @@ -4049,7 +4117,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, depth + 1) idx := t.find_or_register_chan(unwrap_typ, unwrap_typ.nr_muls() > 0) if idx <= 0 { return typ @@ -4058,7 +4127,7 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str } Thread { unwrap_typ := t.unwrap_generic_type_ex_with_depth(ts.info.return_type, generic_names, - concrete_types, recheck_concrete_types, depth_guard) + concrete_types, recheck_concrete_types, depth_guard, depth + 1) idx := t.find_or_register_thread(unwrap_typ) if idx <= 0 { return typ @@ -4067,9 +4136,9 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str } Map { unwrap_key_type := t.unwrap_generic_type_ex_with_depth(ts.info.key_type, generic_names, - concrete_types, recheck_concrete_types, depth_guard) + concrete_types, recheck_concrete_types, depth_guard, depth + 1) unwrap_value_type := t.unwrap_generic_type_ex_with_depth(ts.info.value_type, - generic_names, concrete_types, recheck_concrete_types, depth_guard) + generic_names, concrete_types, recheck_concrete_types, depth_guard, depth + 1) idx := t.find_or_register_map(unwrap_key_type, unwrap_value_type) if idx <= 0 { return typ @@ -4082,13 +4151,14 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str mut has_generic := false for i, param in unwrapped_fn.params { if param.typ.has_flag(.generic) || t.generic_type_names(param.typ).len > 0 { - unwrapped_fn.params[i].typ = t.unwrap_generic_param_type(param, generic_names, - concrete_types) + unwrapped_fn.params[i].typ = t.unwrap_generic_param_type_with_depth(param, + generic_names, concrete_types, depth + 1) 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, depth + + 1) } } if unwrapped_fn.return_type.has_flag(.generic) @@ -4096,7 +4166,7 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str || (unwrapped_fn.return_type.idx() > 0 && unwrapped_fn.return_type.idx() < t.type_symbols.len && t.sym(unwrapped_fn.return_type).kind == .generic_inst&& (t.sym(unwrapped_fn.return_type).info as GenericInst).concrete_types.any(it.has_flag(.generic))) { unwrapped_fn.return_type = t.unwrap_generic_type_ex_with_depth(unwrapped_fn.return_type, - generic_names, concrete_types, recheck_concrete_types, depth_guard) + generic_names, concrete_types, recheck_concrete_types, depth_guard, depth + 1) has_generic = true } if has_generic { @@ -4134,8 +4204,10 @@ 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, + + depth + 1) } } } @@ -4143,8 +4215,8 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str nrt = '${base_name}[' c_nrt = '${ts.cname}_T_' for i in 0 .. ts.info.generic_types.len { - if ct := t.convert_generic_type(ts.info.generic_types[i], t_generic_names, - t_concrete_types) + if ct := t.convert_generic_type_with_depth(ts.info.generic_types[i], + t_generic_names, t_concrete_types, depth + 1) { gts := t.sym(ct) if ct.is_ptr() { @@ -4180,15 +4252,15 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str for i in 0 .. fields.len { resolved_field_typ := t.unwrap_generic_type_ex_with_depth(fields[i].typ, t_generic_names, t_concrete_types, recheck_concrete_types, - new_depth_guard) + new_depth_guard, depth + 1) if resolved_field_typ != fields[i].typ { fields[i].typ = resolved_field_typ } } // update concrete types for i in 0 .. ts.info.generic_types.len { - if t_typ := t.convert_generic_type(ts.info.generic_types[i], - t_generic_names, t_concrete_types) + if t_typ := t.convert_generic_type_with_depth(ts.info.generic_types[i], + t_generic_names, t_concrete_types, depth + 1) { final_concrete_types << t_typ } @@ -4221,7 +4293,9 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str for i in 0 .. fields.len { orig_type := fields[i].typ resolved_field_typ := t.unwrap_generic_type_ex_with_depth(orig_type, - t_generic_names, t_concrete_types, recheck_concrete_types, new_depth_guard) + t_generic_names, t_concrete_types, recheck_concrete_types, new_depth_guard, + + depth + 1) if resolved_field_typ != orig_type { fields[i].typ = resolved_field_typ // Update type in `info.embeds`, if it's embed @@ -4254,8 +4328,8 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str } // update concrete types for i in 0 .. ts.info.generic_types.len { - if t_typ := t.convert_generic_type(ts.info.generic_types[i], t_generic_names, - t_concrete_types) + if t_typ := t.convert_generic_type_with_depth(ts.info.generic_types[i], + t_generic_names, t_concrete_types, depth + 1) { final_concrete_types << t_typ } @@ -4283,7 +4357,7 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str for i in 0 .. resolved_cts.len { if resolved_cts[i].has_flag(.generic) { new_ct := t.unwrap_generic_type_ex_with_depth(resolved_cts[i], generic_names, - concrete_types, recheck_concrete_types, depth_guard) + concrete_types, recheck_concrete_types, depth_guard, depth + 1) if new_ct != resolved_cts[i] { resolved_cts[i] = new_ct changed = true @@ -4339,10 +4413,10 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str || (sym.kind in [.struct, .sum_type, .interface] && sym.has_generic_type_info()) { if sym.kind in [.struct, .sum_type, .interface] { variants[i] = t.unwrap_generic_type_ex_with_depth(variants[i], gn_names, - final_concrete_types, false, new_depth_guard) + final_concrete_types, false, new_depth_guard, depth + 1) } else { - if t_typ := t.convert_generic_type(variants[i], gn_names, - final_concrete_types) + if t_typ := t.convert_generic_type_with_depth(variants[i], gn_names, + final_concrete_types, depth + 1) { variants[i] = t_typ } @@ -4390,13 +4464,15 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str concrete_types[..gn_names.len] } for mut method in imethods { - if unwrap_typ := t.convert_generic_type(method.return_type, gn_names, - iface_concrete) + if unwrap_typ := t.convert_generic_type_with_depth(method.return_type, gn_names, + iface_concrete, depth + 1) { method.return_type = unwrap_typ } for mut param in method.params { - if unwrap_typ := t.convert_generic_param_type(param, gn_names, iface_concrete) { + if unwrap_typ := t.convert_generic_param_type_with_depth(param, gn_names, + iface_concrete, depth + 1) + { param.typ = unwrap_typ } } @@ -4449,7 +4525,9 @@ fn (mut t Table) unwrap_generic_type_ex_with_depth(typ Type, generic_names []str } else { if typ.has_flag(.generic) { - if converted := t.convert_generic_type(typ, generic_names, concrete_types) { + if converted := t.convert_generic_type_with_depth(typ, generic_names, + concrete_types, depth + 1) + { return converted } } @@ -4887,8 +4965,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 > t.max_postprocess_iterations { + t.panic('generic_insts_to_concrete limit ${t.max_postprocess_iterations} exceeded (override with `-max-postprocess-iterations`)') + } info := sym.info as GenericInst if info.parent_idx <= 0 || info.parent_idx >= t.type_symbols.len { continue @@ -5040,6 +5123,38 @@ 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, while still + // processing the remaining generic instantiations. Variant and parent are + // compared module-aware, otherwise same-named types from different modules + // are misdetected as circular. + 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('<') + } + mut is_circular_sum_type := false + for variant in variants { + variant_sym := t.sym(variant) + if variant_sym.mod != parent.mod { + continue + } + mut sym_name := variant_sym.name.trim_string_left(variant_sym.mod + '.') + if sym_name.contains('[') { + sym_name = sym_name.all_before('[') + } else if sym_name.contains('<') { + sym_name = sym_name.all_before('<') + } + if sym_name == parent_name { + is_circular_sum_type = true + break + } + } + if is_circular_sum_type { + continue + } + 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, diff --git a/vlib/v/builder/builder.v b/vlib/v/builder/builder.v index 17c1d742686385..1a941d8a376a23 100644 --- a/vlib/v/builder/builder.v +++ b/vlib/v/builder/builder.v @@ -90,6 +90,11 @@ pub fn new_builder(pref_ &pref.Preferences) Builder { util.emanager.set_support_color(false) } table.pointer_size = if pref_.m64 && pref_.backend != .wasm { 8 } else { 4 } + table.generic_fn_inst_limit = pref_.generic_fn_inst_limit + table.generic_inst_name_len_limit = pref_.generic_inst_name_len_limit + table.generic_inst_depth_limit = pref_.generic_inst_depth_limit + table.alias_unwrap_depth_limit = pref_.alias_unwrap_depth_limit + table.max_postprocess_iterations = pref_.max_postprocess_iterations mut msvc := MsvcResult{} if pref_.ccompiler_type == .msvc || pref.cc_from_string(pref_.ccompiler) == .msvc { $if windows { diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 61a7c92af6f671..ac943b59a3276c 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -22,8 +22,6 @@ 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 - fn has_ascii_upper(s string) bool { for ch in s { if ch >= `A` && ch <= `Z` { @@ -771,7 +769,7 @@ pub fn (mut c Checker) check_files(ast_files []&ast.File) { // is needed when the generic type is auto inferred from the call argument. // we may have to loop several times, if there were more concrete types found. mut post_process_generic_fns_iterations := 0 - post_process_iterations_loop: for post_process_generic_fns_iterations <= generic_fn_postprocess_iterations_cutoff_limit { + post_process_iterations_loop: for post_process_generic_fns_iterations <= c.pref.generic_fn_postprocess_iters { $if trace_post_process_generic_fns_loop ? { eprintln('>>>>>>>>> recheck_generic_fns loop iteration: ${post_process_generic_fns_iterations}') } @@ -804,6 +802,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 == c.pref.generic_fn_postprocess_iters { + if c.file.generic_fns.len > 0 { + c.error('generic function post processing reached the cutoff limit of ${c.pref.generic_fn_postprocess_iters} 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 ${c.pref.generic_fn_postprocess_iters} 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 +1186,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 +1199,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 +1270,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 && sym.mod == c.file.mod.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{} diff --git a/vlib/v/help/build/build.txt b/vlib/v/help/build/build.txt index daf5f8ce9a4e71..c753f5772ac099 100644 --- a/vlib/v/help/build/build.txt +++ b/vlib/v/help/build/build.txt @@ -291,6 +291,24 @@ NB: the build flags are shared with the run command too: NB: this is still experimental, the rules for it will change, it may be dropped completely, or it may become the default. + Resource limits (guard rails against infinite generic / alias expansion in invalid code): + + -generic-fn-inst-limit + Max distinct concrete instantiations of a single generic fn. Default: 4096. + -generic-inst-name-len-limit + Max length of the generated name of a generic instantiation. Default: 8192. + -generic-inst-depth-limit + Max recursion depth while unwrapping generic types. Default: 256. + NB: values above 512 are clamped, since the guard must fire before the native stack overflows. + -alias-unwrap-depth-limit + Max unwrap iterations for chains/cycles of type aliases. Default: 100. + -generic-fn-postprocess-iters + Max post-processing re-check passes over generic fns, before bailing out + with an error about a probable infinite generic instantiation loop. Default: 128. + -max-postprocess-iterations + Max number of generic struct instantiations handled in one post-processing + sweep. Default: 100000. + For C-specific build flags, use `v help build-c`. For JS-specific build flags, use `v help build-js`. For Native-specific build flags, use `v help build-native`. diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index 172422fe319c52..f9770774d02d5d 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -256,9 +256,16 @@ pub mut: gc_set_by_flag bool // true when the compiler receives `-gc` assert_failure_mode AssertFailureMode // whether to call abort() or print_backtrace() after an assertion failure message_limit int = 200 // the maximum amount of warnings/errors/notices that will be accumulated - nofloat bool // for low level code, like kernels: replaces f32 with u32 and f64 with u64 - use_coroutines bool // experimental coroutines - fast_math bool // -fast-math will pass either -ffast-math or /fp:fast (for msvc) to the C backend + // resource limit settings (guard rails against infinite generic/alias expansion; see the defaults in vlib/v/ast/table.v): + generic_fn_inst_limit int = 4096 // Change with `-generic-fn-inst-limit` + generic_inst_name_len_limit int = 8192 // Change with `-generic-inst-name-len-limit` + generic_inst_depth_limit int = 256 // Change with `-generic-inst-depth-limit` (clamped to <= 512) + alias_unwrap_depth_limit int = 100 // Change with `-alias-unwrap-depth-limit` + generic_fn_postprocess_iters int = 128 // Change with `-generic-fn-postprocess-iters` + max_postprocess_iterations int = 100_000 // Change with `-max-postprocess-iterations` + nofloat bool // for low level code, like kernels: replaces f32 with u32 and f64 with u64 + use_coroutines bool // experimental coroutines + fast_math bool // -fast-math will pass either -ffast-math or /fp:fast (for msvc) to the C backend // checker settings: checker_match_exhaustive_cutoff_limit int = 12 thread_stack_size int = 8388608 // Change with `-thread-stack-size 4194304`. The final default is adjusted in fill_with_defaults() based on the target architecture. @@ -1091,6 +1098,40 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin cmdline.option(args[i..], arg, '10').int() i++ } + '-generic-fn-inst-limit' { + res.generic_fn_inst_limit = cmdline.option(args[i..], arg, + res.generic_fn_inst_limit.str()).int() + i++ + } + '-generic-inst-name-len-limit' { + res.generic_inst_name_len_limit = cmdline.option(args[i..], arg, + res.generic_inst_name_len_limit.str()).int() + i++ + } + '-generic-inst-depth-limit' { + res.generic_inst_depth_limit = cmdline.option(args[i..], arg, + res.generic_inst_depth_limit.str()).int() + if res.generic_inst_depth_limit > 512 { + eprintln('warning: `-generic-inst-depth-limit` clamped to 512; higher values can segfault the compiler before the guard fires') + res.generic_inst_depth_limit = 512 + } + i++ + } + '-alias-unwrap-depth-limit' { + res.alias_unwrap_depth_limit = cmdline.option(args[i..], arg, + res.alias_unwrap_depth_limit.str()).int() + i++ + } + '-generic-fn-postprocess-iters' { + res.generic_fn_postprocess_iters = cmdline.option(args[i..], arg, + res.generic_fn_postprocess_iters.str()).int() + i++ + } + '-max-postprocess-iterations' { + res.max_postprocess_iterations = cmdline.option(args[i..], arg, + res.max_postprocess_iterations.str()).int() + i++ + } '-o', '-output' { raw_out_name := cmdline.option(args[i..], arg, '') res.out_name_is_dir = raw_out_name.ends_with('/') || raw_out_name.ends_with('\\') diff --git a/vlib/v/tests/recursion_cutoff_flags_test.v b/vlib/v/tests/recursion_cutoff_flags_test.v new file mode 100644 index 00000000000000..fdc3378fe1df58 --- /dev/null +++ b/vlib/v/tests/recursion_cutoff_flags_test.v @@ -0,0 +1,197 @@ +// Tests for the user-tunable recursion / cutoff guard limits +// (defaults live in vlib/v/ast/table.v, wiring in vlib/v/pref/pref.v). +// +// Every guard can be overridden with a compiler flag, e.g. +// `v -generic-fn-postprocess-iters 512 run file.v`. +module main + +import os + +const vexe = @VEXE + +const work_dir = os.join_path(os.vtmp_dir(), 'recursion_cutoff_flags_test') + +const bug_recursive_generic_fn = 'fn foo[T]() { + foo[[]T]() +} + +fn main() { + foo[int]() +} +' + +const bug_recursive_generic_method = 'struct Box[T] { + val T +} + +fn (b Box[T]) foo() { + Box[Box[T]]{}.foo() +} + +fn main() { + Box[int]{}.foo() +} +' + +const bug_nested_generic_struct = 'struct Box[T] { + Box[Box[T]] +} + +fn main() { + b := Box[int]{} + println(b) +} +' + +const bug_circular_sum_type = 'type MySum[T] = T | MySum[MySum[T]] + +fn main() { + mut x := MySum[int](0) + println(x) +} +' + +const bug_recursive_fn_alias = 'type Func[T] = fn (f Func[[]T]) T + +fn main() { + _ := Func[int](none) +} +' + +const valid_fn_instantiations = 'fn id[T](x T) T { + return x +} + +fn main() { + assert id(1) == 1 + assert id(i64(2)) == i64(2) + assert id(u8(3)) == u8(3) + assert id(`a`) == `a` + assert id(f32(1.5)) == f32(1.5) + assert id(f64(2.5)) == f64(2.5) + println(12345) +} +' + +const valid_struct_instantiations = "struct Box[T] { +pub: + val T +} + +fn main() { + a := Box[int]{100} + b := Box[string]{'str'} + c := Box[f64]{1.5} + d := Box[bool]{true} + e := Box[rune]{`z`} + f := Box[u64]{42} + assert a.val == 100 + assert b.val == 'str' + assert c.val == 1.5 + assert d.val + assert e.val == `z` + assert f.val == 42 + println('struct insts ok') +} +" + +fn compile_and_run(file_name string, contents string, args string) os.Result { + os.mkdir_all(work_dir) or { panic(err) } + path := os.join_path(work_dir, file_name) + os.write_file(path, contents) or { panic(err) } + defer { + os.rm(path) or {} + } + return os.execute('${os.quoted_path(vexe)} ${args} run ${os.quoted_path(path)} 2>&1') +} + +fn test_recursive_generic_fn_caught_by_default_cutoff() { + res := compile_and_run('bug_recursive_generic_fn.v', bug_recursive_generic_fn, '') + assert res.exit_code == 1, res.output + assert res.output.contains('cutoff limit of 128 iterations'), res.output +} + +fn test_recursive_generic_method_caught_by_cutoff_flag() { + // NB: the default of 128 is intentionally not used here. The method/struct + // expansion variant consumes memory so aggressively, that on + // memory-constrained machines the process can get OOM-killed before the + // 128th pass. A small explicit limit keeps the failure fast and deterministic, + // and doubles as a check that the flag is honored for this code path too. + res := compile_and_run('bug_recursive_generic_method.v', bug_recursive_generic_method, + '-generic-fn-postprocess-iters 16') + assert res.exit_code == 1, res.output + assert res.output.contains('cutoff limit of 16 iterations'), res.output +} + +fn test_generic_fn_postprocess_iters_flag_is_respected() { + res := compile_and_run('bug_recursive_generic_fn_flag.v', bug_recursive_generic_fn, + '-generic-fn-postprocess-iters 16') + assert res.exit_code == 1, res.output + // the reported limit must be the one passed on the command line: + assert res.output.contains('cutoff limit of 16 iterations'), res.output +} + +fn test_circular_sum_type_is_rejected() { + res := compile_and_run('bug_circular_sum_type.v', bug_circular_sum_type, '') + assert res.exit_code == 1, res.output + assert res.output.contains('sum type cannot hold itself'), res.output +} + +fn test_nested_generic_struct_hits_depth_limit_by_default() { + res := compile_and_run('bug_nested_generic_struct.v', bug_nested_generic_struct, '') + assert res.exit_code == 1, res.output + assert res.output.contains('generic instantiation depth limit 256 exceeded'), res.output +} + +fn test_depth_limit_flag_is_respected() { + res := compile_and_run('bug_nested_generic_struct_flag.v', bug_nested_generic_struct, + '-generic-inst-depth-limit 8') + assert res.exit_code == 1, res.output + assert res.output.contains('generic instantiation depth limit 8 exceeded'), res.output +} + +fn test_depth_limit_flag_is_clamped_to_a_safe_maximum() { + res := compile_and_run('bug_nested_generic_struct_clamp.v', bug_nested_generic_struct, + '-generic-inst-depth-limit 1000') + assert res.exit_code == 1, res.output + // values above 512 are clamped, the reported limit must be 512: + assert res.output.contains('generic instantiation depth limit 512 exceeded'), res.output +} + +fn test_recursive_fn_alias_hits_depth_limit_by_default() { + res := compile_and_run('bug_recursive_fn_alias.v', bug_recursive_fn_alias, '') + assert res.exit_code == 1, res.output + assert res.output.contains('generic instantiation depth limit 256 exceeded'), res.output +} + +fn test_fn_instantiation_limit_flag_is_respected() { + res := compile_and_run('valid_fn_instantiations_flag.v', valid_fn_instantiations, + '-generic-fn-inst-limit 4') + assert res.exit_code == 1, res.output + assert res.output.contains('generic function instantiation limit 4 exceeded'), res.output +} + +fn test_max_postprocess_iterations_flag_is_respected() { + res := compile_and_run('valid_struct_instantiations_flag.v', valid_struct_instantiations, + '-max-postprocess-iterations 5') + assert res.exit_code == 1, res.output + assert res.output.contains('generic_insts_to_concrete limit 5 exceeded'), res.output +} + +fn test_valid_code_compiles_with_default_limits() { + r1 := compile_and_run('valid_fn_instantiations.v', valid_fn_instantiations, '') + assert r1.exit_code == 0, r1.output + assert r1.output.contains('12345'), r1.output + r2 := compile_and_run('valid_struct_instantiations.v', valid_struct_instantiations, '') + assert r2.exit_code == 0, r2.output +} + +fn test_flags_are_documented_in_help() { + res := os.execute('${os.quoted_path(vexe)} help build') + assert res.exit_code == 0, res.output + assert res.output.contains('-generic-fn-inst-limit'), res.output + assert res.output.contains('-generic-inst-depth-limit'), res.output + assert res.output.contains('-generic-fn-postprocess-iters'), res.output + assert res.output.contains('-alias-unwrap-depth-limit'), res.output + assert res.output.contains('-max-postprocess-iterations'), res.output +}