From 0f86ffd90989d2243cc2f24f7648ca419fc0d1aa Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:26:47 +0300 Subject: [PATCH] fix: .gguf listing walks by folder identity (sibling cycles, root links, case); dangling links not listed (attacker round 14) Co-Authored-By: Claude Sonnet 5.5 --- modules/loaders/gguf_support.py | 48 ++++++++++++++++------ modules/loaders/tests/test_gguf_support.py | 12 ++++++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/modules/loaders/gguf_support.py b/modules/loaders/gguf_support.py index a6d6606c..1f4becd5 100644 --- a/modules/loaders/gguf_support.py +++ b/modules/loaders/gguf_support.py @@ -65,6 +65,37 @@ def has_gguf_magic(path: str) -> bool: # ── finding .gguf files ─────────────────────────────────────────────────────── +def _walk(root: str, limit: int = 5000, depth: int = 12): + """Every .gguf under `root` as a relative name, following links. + + A folder is not entered again while it is one of the folders above it (same device and inode, so case, roots and + cycles through a sibling are all caught), and the walk stops after `limit` folders: a link to a whole disk is slow, not endless. + A link to a sibling folder is not a loop; it is a second name for the same files. + """ + seen = [0] + + def rec(path, rel, above): + seen[0] += 1 + try: + entries = sorted(os.scandir(path), key=lambda e: e.name) + except OSError: + return + for e in entries: + try: + if e.is_dir(): + st = os.stat(e.path) + key = (st.st_dev, st.st_ino) + if key not in above and len(above) < depth and seen[0] < limit: + yield from rec(e.path, rel + e.name + "/", above | {key}) + elif is_gguf(e.name) and e.is_file(): # is_file: a dangling link is not a model + yield rel + e.name + except OSError: + continue + + st = os.stat(root) + yield from rec(root, "", frozenset({(st.st_dev, st.st_ino)})) + + def gguf_names(folder: str) -> list[str]: """`.gguf` files under a ComfyUI model folder, relative-named like get_filename_list(). @@ -90,17 +121,10 @@ def gguf_names(folder: str) -> list[str]: for root in roots: if not root or not os.path.isdir(root): continue - for dirpath, dirs, files in os.walk(root, followlinks=True): - here = os.path.realpath(dirpath) + os.sep - # a link back to this folder or one above it is a loop; a link to a sibling folder is just a second name - dirs[:] = [d for d in dirs if not here.startswith(os.path.realpath(os.path.join(dirpath, d)) + os.sep)] - for f in files: - if not is_gguf(f): - continue - rel = os.path.relpath(os.path.join(dirpath, f), root) - if rel not in seen: - seen.add(rel) - out.append(rel) + for rel in _walk(root): + if rel not in seen: + seen.add(rel) + out.append(rel) return sorted(out) @@ -324,7 +348,7 @@ def _load_native(path: str, pack: bool = False) -> tuple[dict, dict, str]: # type the package knows -- do not reimplement it per quant type. try: arr = gguf.quants.dequantize(tensor.data, qtype) - except NotImplementedError as e: + except (NotImplementedError, ValueError) as e: raise RuntimeError(f"{os.path.basename(path)}: tensor {name!r} uses a storage type the gguf package cannot " f"dequantize ({e}).") from e quantized += 1 diff --git a/modules/loaders/tests/test_gguf_support.py b/modules/loaders/tests/test_gguf_support.py index 501f3b37..541ae728 100644 --- a/modules/loaders/tests/test_gguf_support.py +++ b/modules/loaders/tests/test_gguf_support.py @@ -389,3 +389,15 @@ def test_a_symlinked_folder_aliasing_a_sibling_is_listed_under_both_names(fake_f (enc / "current").symlink_to(enc / "gemma") names = gguf_support.gguf_names("text_encoders") assert "current/g-Q5.gguf" in names and "gemma/g-Q5.gguf" in names + + +def test_cycles_through_a_sibling_folder_and_dangling_links_do_not_flood_the_listing(fake_folders): + enc = fake_folders / "text_encoders" + other = fake_folders / "other" + other.mkdir() + (enc / "p0").symlink_to(other) + (enc / "p1").symlink_to(other) + (other / "q").symlink_to(enc) + (enc / "broken.gguf").symlink_to(fake_folders / "missing.gguf") + assert gguf_support.gguf_names("text_encoders") == ["gemma-Q5_K.gguf"] + assert "broken.gguf" not in gguf_support.gguf_names("text_encoders")