From ddc9c99fda9b46d4a1e3d8fe674a4395bb311d96 Mon Sep 17 00:00:00 2001 From: Richard Wheeler Date: Mon, 22 Jun 2026 22:44:35 -0400 Subject: [PATCH] builder: fix infinite loop in find_module_path for projects at a Windows drive root (#27473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * builder: fix infinite loop in find_module_path for projects at a Windows drive root When a V project is located directly under a drive root (e.g. `S:\proj`), `find_module_path`'s "anchor to the outermost enclosing v.mod" loop never terminated, so building the project (including `make`/`makev.bat`) hung forever at 100% CPU. `os.dir("S:\proj")` returns the bare drive `"S:"`, and inside `get_by_folder` `os.real_path("S:")` resolves a bare drive letter to that drive's *current directory* (back into the project), so the walked folder never moved up while the loop's guard compared the unresolved `"S:"` against the full path and never broke. Only climb when the resolved parent v.mod is a strict ancestor of the current folder. This handles both the fixpoint case (project at a drive root) and the multi-step oscillation case (a project nested under such a drive-root project). Fixes #27472 Co-Authored-By: WOZCODE * vmod, builder: fix Windows drive-root infinite loop in module lookup On Windows, os.dir('C:\project') returns 'C:' (bare drive letter with no trailing slash). os.dir('C:') returns '.' and os.dir('.') returns '.' — a fixed point that the existing '/' stop condition in traverse never catches, causing the upward vmod search to burn 255 iterations instead of stopping cleanly. Any caller of get_by_folder/get_by_file was exposed (checker, util, vdoc, vpm), not just builder. Fix traverse (vmod.v) to break as soon as os.dir stops making progress (next == cfolder). This is the correct general stop condition and subsumes the Windows drive-root, Unix root, and '.' cases in one check. Add is_strict_ancestor in builder.find_module_path as a belt-and- suspenders guard: stop climbing the outermost-vmod chain whenever the cache returns a candidate that is not a strict path ancestor of the current folder. Implemented by delegating to the existing comparable_real_path + path_is_at_or_inside helpers already in the file. Co-Authored-By: WOZCODE * builder: fix struct field alignment to pass v fmt -verify Co-Authored-By: WOZCODE --------- Co-authored-by: Richard Wheeler <18647491+PythonWillRule@users.noreply.github.com> Co-authored-by: WOZCODE --- vlib/v/builder/builder.v | 35 ++++++++++++++++++++++++----------- vlib/v/vmod/vmod.v | 6 +++++- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/vlib/v/builder/builder.v b/vlib/v/builder/builder.v index 02c2445de..0e81c4bfb 100644 --- a/vlib/v/builder/builder.v +++ b/vlib/v/builder/builder.v @@ -51,17 +51,14 @@ pub mut: table &ast.Table = unsafe { nil } ccoptions CcompilerOptions // Note: changes in mod `builtin` force invalidation of every other .v file - mod_invalidates_paths map[string][]string // changes in mod `os`, invalidate only .v files, that do `import os` - mod_invalidates_mods map[string][]string // changes in mod `os`, force invalidation of mods, that do `import os` - path_invalidates_mods map[string][]string // changes in a .v file from `os`, invalidates `os` - crun_cache_keys []string // target executable + top level source files; filled in by Builder.should_rebuild - executable_exists bool // if the executable already exists, don't remove new executable after `v run` - str_args string // for parallel_cc mode only, to know which cc args to use (like -I etc) - last_cc_cmd string // the most recently executed C compiler command; reused to regenerate a #line annotated report - disable_flto bool - // thirdparty_header_mtimes memoizes the newest header mtime under a - // thirdparty module root (see thirdparty_deps_mtime), so the recursive scan - // runs once per module instead of once per compiled object. + mod_invalidates_paths map[string][]string // changes in mod `os`, invalidate only .v files, that do `import os` + mod_invalidates_mods map[string][]string // changes in mod `os`, force invalidation of mods, that do `import os` + path_invalidates_mods map[string][]string // changes in a .v file from `os`, invalidates `os` + crun_cache_keys []string // target executable + top level source files; filled in by Builder.should_rebuild + executable_exists bool // if the executable already exists, don't remove new executable after `v run` + str_args string // for parallel_cc mode only, to know which cc args to use (like -I etc) + last_cc_cmd string // the most recently executed C compiler command; reused to regenerate a #line annotated report + disable_flto bool thirdparty_header_mtimes map[string]i64 } @@ -970,6 +967,13 @@ pub fn (b &Builder) find_module_path(mod string, fpath string) !string { if parent_loc.vmod_file == '' { break } + // On Windows, os.dir('C:\project') returns 'C:' (no trailing slash), + // and os.real_path('C:') resolves to the drive's current directory — + // which may equal importer_vmod_folder, producing an infinite loop. + // Stop climbing whenever the candidate is not a strict path ancestor. + if !is_strict_ancestor(parent_loc.vmod_folder, importer_vmod_folder) { + break + } importer_vmod_folder = parent_loc.vmod_folder } } @@ -1307,3 +1311,12 @@ pub fn (mut b Builder) show_parsed_files() { println(p.path) } } + +fn is_strict_ancestor(ancestor string, descendant string) bool { + if ancestor == '' || descendant == '' { + return false + } + a := comparable_real_path(ancestor) + d := comparable_real_path(descendant) + return a != d && path_is_at_or_inside(d, a) +} diff --git a/vlib/v/vmod/vmod.v b/vlib/v/vmod/vmod.v index 328cdd678..fc2a5cefb 100644 --- a/vlib/v/vmod/vmod.v +++ b/vlib/v/vmod/vmod.v @@ -144,7 +144,11 @@ fn (mut mcache ModFileCacher) traverse(mfolder string) ([]string, ModFileAndFold if mcache.check_for_stop(files) { break } - cfolder = os.dir(cfolder) + next := os.dir(cfolder) + if next == cfolder { + break + } + cfolder = next folders_so_far << cfolder levels++ } -- 2.39.5