Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions vlib/v/compiler_tests/scoped_monomorphize_closure_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import os

const scoped_monomorph_vexe = @VEXE
const scoped_monomorph_tests_dir = os.dir(@FILE)
const scoped_monomorph_v3_dir = os.dir(scoped_monomorph_tests_dir)
const scoped_monomorph_vlib_dir = os.dir(scoped_monomorph_v3_dir)
const scoped_monomorph_v3_src = os.join_path(scoped_monomorph_v3_dir, 'v.v')

$if windows {
const scoped_monomorph_bin_suffix = '.exe'
} $else {
const scoped_monomorph_bin_suffix = ''
}

fn scoped_monomorph_v3_bin_path() string {
return os.join_path(os.temp_dir(), 'v3_scoped_monomorphize_closure_test${scoped_monomorph_bin_suffix}')
}

fn scoped_monomorph_v3_bin() string {
bin := scoped_monomorph_v3_bin_path()
if os.exists(bin) {
return bin
}
// `-prealloc` is what enables `scope_parallel_workers` and the scoped
// monomorphize path, matching how the distributed compiler is built.
Comment thread
Jengro777 marked this conversation as resolved.
build := os.execute('${os.quoted_path(scoped_monomorph_vexe)} -gc none -prealloc -path "${scoped_monomorph_vlib_dir}|@vlib|@vmodules" -o ${os.quoted_path(bin)} ${os.quoted_path(scoped_monomorph_v3_src)}')
assert build.exit_code == 0, build.output
return bin
}

fn testsuite_begin() {
os.rm(scoped_monomorph_v3_bin_path()) or {}
}

// Compiler builds use `-prealloc`, and the memory-bounded monomorphize path (the
// fix for vlang/v#28564) runs for every non-empty specialization batch there.
// That path used to give two different lifted closures the same `__anon_fn_N`
// name - the module-keyed signature table then mixed their signatures up and the
// generated C did not compile - and merged specialization arguments as shallow
// `[]string` copies that still pointed into the released worker arena, so a later
// pass read freed arguments (bogus `unknown function` diagnostics or a crash).
// This is the small `veb` program from vlang/v#28489, which exercises closures
// lifted while specializing a generic helper.
fn test_scoped_monomorphize_keeps_closure_signatures_and_args() {
v3_bin := scoped_monomorph_v3_bin()
dir := os.join_path(os.temp_dir(), 'v3_scoped_monomorphize_closure')
os.rmdir_all(dir) or {}
os.mkdir_all(dir) or { panic(err) }
defer {
os.rmdir_all(dir) or {}
}
os.write_file(os.join_path(dir, 'main.v'), "module main

import veb

pub struct Ctx {
veb.Context
}

pub struct ModelApp {
veb.Middleware[Ctx]
veb.Controller
}

pub struct Item {
ModelApp
}

pub struct MainApp {
veb.Middleware[Ctx]
veb.Controller
}

fn mw() veb.MiddlewareOptions[Ctx] {
return veb.MiddlewareOptions[Ctx]{
handler: fn (mut ctx Ctx) bool {
return true
}
}
}

fn (mut app MainApp) common_middleware[T](mut ctrl T) {
ctrl.use(mw())
}

fn (mut app MainApp) register_routes_no_auth[T, U](mut ctrl T, url_path string) {
app.common_middleware[T](mut ctrl)
app.register_controller[T, U](url_path, mut ctrl) or { panic(err) }
ctrl.route_use('/item/*', veb.encode_auto[Ctx]())
}

fn main() {
mut app := &MainApp{}
app.register_routes_no_auth[Item, Ctx](mut &Item{}, '/item')
veb.run_at[MainApp, Ctx](mut app, port: 9001) or { panic(err) }
}
") or { panic(err) }
out := os.join_path(dir, 'app${scoped_monomorph_bin_suffix}')
compile := os.execute('${os.quoted_path(v3_bin)} -nocache -o ${os.quoted_path(out)} ${os.quoted_path(dir)}')
assert compile.exit_code == 0, compile.output
assert !compile.output.contains('C compilation failed'), compile.output
assert os.is_file(out), 'the compile produced no binary'
}
18 changes: 13 additions & 5 deletions vlib/v/transform/monomorphize.v
Original file line number Diff line number Diff line change
Expand Up @@ -588,13 +588,21 @@ fn (mut t Transformer) request_generic_fn_specialization(decl GenericFnDecl, arg
}

fn (mut t Transformer) record_monomorph_cache_spec(key string, decl_key string, module_name string, args []string) {
if key.len == 0 || decl_key.len == 0 || key in t.monomorph_cache_specs {
if key.len == 0 || decl_key.len == 0 {
return
}
t.monomorph_cache_specs[key] = MonomorphCacheSpec{
decl_key: decl_key
module: module_name
args: args.clone()
// The strings must not reference a worker arena: a scoped batch releases its
// arenas right after the merge, and a later pass re-seeds its specializations
// from this cache. Re-record instead of skipping an existing key so the last
// (parent-arena) copy wins over one a worker recorded for the same spec.
mut owned_args := []string{cap: args.len}
for arg in args {
owned_args << arg.clone()
}
t.monomorph_cache_specs[key.clone()] = MonomorphCacheSpec{
decl_key: decl_key.clone()
module: module_name.clone()
args: owned_args
}
}

Expand Down
21 changes: 21 additions & 0 deletions vlib/v/transform/monomorphize_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -674,3 +674,24 @@ fn test_free_generic_map_suffix_preserves_qualified_value_type() {
decoded := generic_type_arg_from_suffix_with_containers(suffix)
assert decoded == 'map[string]binary.St'
}

// The monomorph cache outlives the worker arena that produced an entry: a scoped
// batch records a spec, releases its arena and a later pass re-seeds from this
// table (vlang/v#28489). Recording has to own key, module, declaration key and
// every argument, and a re-recorded key must keep the last copy instead of being
// skipped.
fn test_record_monomorph_cache_spec_replaces_existing_entry() {
mut a := flat.FlatAst.new()
mut tc := types.TypeChecker.new(&a)
mut t := new_transformer(mut a, &tc, map[string]bool{})

t.record_monomorph_cache_spec('main.f_T_int', 'main.f', 'main', ['int'])
t.record_monomorph_cache_spec('main.f_T_int', 'main.f', 'main', ['iam.Token'])
spec := t.monomorph_cache_specs['main.f_T_int'] or {
assert false, 'the re-recorded spec disappeared'
return
}
assert spec.args == ['iam.Token']
assert spec.decl_key == 'main.f'
assert spec.module == 'main'
}
27 changes: 25 additions & 2 deletions vlib/v/transform/transform_parallel_notd_v3_no_parallel.v
Original file line number Diff line number Diff line change
Expand Up @@ -1412,7 +1412,10 @@ fn (mut t Transformer) run_parallel_monomorphize_specs(specs []PendingGenericFnS
for name in w.generic_specialization_args_log {
spec_args := w.generic_specialization_args[name] or { continue }
if name !in t.generic_specialization_args {
t.generic_specialization_args[name.clone()] = spec_args.clone()
// Deep-copy the elements: the worker's copy of this array is
// backed by its scratch arena, which is released after the
// merge (a later pass re-seeds from these recorded args).
t.generic_specialization_args[name.clone()] = clone_monomorph_specialization_args(spec_args)
}
}
// Every emitted worker specialization is registered by the master below.
Expand Down Expand Up @@ -1567,6 +1570,21 @@ fn (mut t Transformer) run_scoped_monomorphize_specs(specs []PendingGenericFnSpe
roots << root
emitted_specs << spec
}
// The emitted specs can carry worker-owned argument strings. Copy key and
// arguments while the worker scope is still alive, but with that scope
// suspended so the copies land in the arena the merge loops below use:
// reading the worker strings after `transform_worker_scope_leave()` would
// be a use-after-free (vlang/v#28489).
mut owned_emitted_specs := []PendingGenericFnSpec{cap: emitted_specs.len}
worker_scope_state := transform_stage_scope_suspend(scope)
for spec in emitted_specs {
owned_emitted_specs << PendingGenericFnSpec{
decl: spec.decl
args: clone_monomorph_specialization_args(spec.args)
key: spec.key.clone()
}
}
transform_stage_scope_resume(scope, worker_scope_state)
w.worker_scope = scope
transform_worker_scope_leave(scope)

Expand Down Expand Up @@ -1594,8 +1612,13 @@ fn (mut t Transformer) run_scoped_monomorphize_specs(specs []PendingGenericFnSpe
}
t.request_generic_fn_specialization(pending.decl, owned_args)
}
for idx, spec in emitted_specs {
for idx, spec in owned_emitted_specs {
root := flat.NodeId(int(roots[idx]) + node_shift)
// Re-record the spec from the master: `record_monomorph_cache_spec`
// deep-copies the argument strings into the arena that is current here
// (the parent), while the worker's copies die with `scope`.
t.record_monomorph_cache_spec(spec.key.clone(), spec.decl.key, spec.decl.module,
spec.args)
if !t.generic_specialization_registered(spec.decl, spec.args) {
value := specialized_generic_fn_value(spec.decl.node.value, spec.args)
t.register_specialized_fn_signature_value(spec.decl, value, spec.args)
Expand Down