From 7d76a4447e382db4c5fac15618920f7a69fa9b35 Mon Sep 17 00:00:00 2001 From: xmarre Date: Sat, 14 Mar 2026 02:36:40 +0100 Subject: [PATCH 1/2] Sanitize execution cache inputs --- comfy_execution/caching.py | 51 +++++++++++++++++++++++++++------- comfy_execution/graph_utils.py | 11 ++++++-- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/comfy_execution/caching.py b/comfy_execution/caching.py index 78212bde3..53d8e5bdb 100644 --- a/comfy_execution/caching.py +++ b/comfy_execution/caching.py @@ -51,17 +51,48 @@ class Unhashable: def __init__(self): self.value = float("NaN") -def to_hashable(obj): - # So that we don't infinitely recurse since frozenset and tuples - # are Sequences. - if isinstance(obj, (int, float, str, bool, bytes, type(None))): + +def _sanitize_signature_input(obj, depth=0, max_depth=32): + if depth >= max_depth: + return Unhashable() + + obj_type = type(obj) + if obj_type in (int, float, str, bool, bytes, type(None)): return obj - elif isinstance(obj, Mapping): - return frozenset([(to_hashable(k), to_hashable(v)) for k, v in sorted(obj.items())]) - elif isinstance(obj, Sequence): - return frozenset(zip(itertools.count(), [to_hashable(i) for i in obj])) + elif obj_type is dict: + sanitized = [] + for key in sorted(obj.keys(), key=lambda x: (type(x).__module__, type(x).__qualname__, repr(x))): + sanitized.append((_sanitize_signature_input(key, depth + 1, max_depth), + _sanitize_signature_input(obj[key], depth + 1, max_depth))) + return tuple(sanitized) + elif obj_type in (list, tuple): + return tuple(_sanitize_signature_input(item, depth + 1, max_depth) for item in obj) + elif obj_type in (set, frozenset): + sanitized_items = [_sanitize_signature_input(item, depth + 1, max_depth) for item in obj] + sanitized_items.sort(key=repr) + return tuple(sanitized_items) + else: + # Execution-cache signatures should be built from prompt-safe values. + # If a custom node injects a runtime object here, mark it unhashable so + # the node won't reuse stale cache entries across runs, but do not walk + # the foreign object and risk crashing on custom container semantics. + return Unhashable() + +def to_hashable(obj): + # Restrict recursion to plain built-in containers. Some custom nodes insert + # runtime objects into prompt inputs for dynamic graph paths; walking those + # objects as generic Mappings / Sequences is unsafe and can destabilize the + # cache signature builder. + obj_type = type(obj) + if obj_type in (int, float, str, bool, bytes, type(None)): + return obj + elif obj_type is dict: + return frozenset([(to_hashable(k), to_hashable(v)) for k, v in sorted(obj.items(), key=lambda kv: repr(kv[0]))]) + elif obj_type in (list, tuple): + return frozenset(zip(itertools.count(), [to_hashable(i) for i in obj])) + elif obj_type in (set, frozenset): + return frozenset([to_hashable(i) for i in obj]) else: - # TODO - Support other objects like tensors? return Unhashable() class CacheKeySetID(CacheKeySet): @@ -123,7 +154,7 @@ class CacheKeySetInputSignature(CacheKeySet): ancestor_index = ancestor_order_mapping[ancestor_id] signature.append((key,("ANCESTOR", ancestor_index, ancestor_socket))) else: - signature.append((key, inputs[key])) + signature.append((key, _sanitize_signature_input(inputs[key]))) return signature # This function returns a list of all ancestors of the given node. The order of the list is diff --git a/comfy_execution/graph_utils.py b/comfy_execution/graph_utils.py index 496d2c634..75e4bc522 100644 --- a/comfy_execution/graph_utils.py +++ b/comfy_execution/graph_utils.py @@ -1,11 +1,16 @@ def is_link(obj): - if not isinstance(obj, list): + # Prompt links produced by the frontend / GraphBuilder are plain Python + # lists in the form [node_id, output_index]. Some custom-node paths can + # inject foreign runtime objects into prompt inputs during on-prompt graph + # rewriting or subgraph construction. Be strict here so cache signature + # building never tries to treat list-like proxy objects as links. + if type(obj) is not list: return False if len(obj) != 2: return False - if not isinstance(obj[0], str): + if type(obj[0]) is not str: return False - if not isinstance(obj[1], int) and not isinstance(obj[1], float): + if type(obj[1]) not in (int, float): return False return True From 2adde5a0e1f6abbb19c8aa4f3a71b93c7d47abf6 Mon Sep 17 00:00:00 2001 From: xmarre Date: Sat, 14 Mar 2026 06:36:06 +0100 Subject: [PATCH 2/2] Keep container types in sanitizer --- comfy_execution/caching.py | 77 +++++++++++++++++++++++++++------- comfy_execution/graph_utils.py | 2 +- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/comfy_execution/caching.py b/comfy_execution/caching.py index 53d8e5bdb..d4bd2dee1 100644 --- a/comfy_execution/caching.py +++ b/comfy_execution/caching.py @@ -1,7 +1,6 @@ import asyncio import bisect import gc -import itertools import psutil import time import torch @@ -52,6 +51,37 @@ class Unhashable: self.value = float("NaN") +def _sanitized_sort_key(obj, depth=0, max_depth=32): + if depth >= max_depth: + return ("MAX_DEPTH",) + + obj_type = type(obj) + if obj_type is Unhashable: + return ("UNHASHABLE",) + elif obj_type in (int, float, str, bool, bytes, type(None)): + return (obj_type.__module__, obj_type.__qualname__, repr(obj)) + elif obj_type is dict: + items = [ + ( + _sanitized_sort_key(k, depth + 1, max_depth), + _sanitized_sort_key(v, depth + 1, max_depth), + ) + for k, v in obj.items() + ] + items.sort() + return ("dict", tuple(items)) + elif obj_type is list: + return ("list", tuple(_sanitized_sort_key(i, depth + 1, max_depth) for i in obj)) + elif obj_type is tuple: + return ("tuple", tuple(_sanitized_sort_key(i, depth + 1, max_depth) for i in obj)) + elif obj_type is set: + return ("set", tuple(sorted(_sanitized_sort_key(i, depth + 1, max_depth) for i in obj))) + elif obj_type is frozenset: + return ("frozenset", tuple(sorted(_sanitized_sort_key(i, depth + 1, max_depth) for i in obj))) + else: + return (obj_type.__module__, obj_type.__qualname__, "OPAQUE") + + def _sanitize_signature_input(obj, depth=0, max_depth=32): if depth >= max_depth: return Unhashable() @@ -60,17 +90,28 @@ def _sanitize_signature_input(obj, depth=0, max_depth=32): if obj_type in (int, float, str, bool, bytes, type(None)): return obj elif obj_type is dict: - sanitized = [] - for key in sorted(obj.keys(), key=lambda x: (type(x).__module__, type(x).__qualname__, repr(x))): - sanitized.append((_sanitize_signature_input(key, depth + 1, max_depth), - _sanitize_signature_input(obj[key], depth + 1, max_depth))) - return tuple(sanitized) - elif obj_type in (list, tuple): + sanitized_items = [ + ( + _sanitize_signature_input(key, depth + 1, max_depth), + _sanitize_signature_input(value, depth + 1, max_depth), + ) + for key, value in obj.items() + ] + sanitized_items.sort( + key=lambda kv: ( + _sanitized_sort_key(kv[0], depth + 1, max_depth), + _sanitized_sort_key(kv[1], depth + 1, max_depth), + ) + ) + return {key: value for key, value in sanitized_items} + elif obj_type is list: + return [_sanitize_signature_input(item, depth + 1, max_depth) for item in obj] + elif obj_type is tuple: return tuple(_sanitize_signature_input(item, depth + 1, max_depth) for item in obj) - elif obj_type in (set, frozenset): - sanitized_items = [_sanitize_signature_input(item, depth + 1, max_depth) for item in obj] - sanitized_items.sort(key=repr) - return tuple(sanitized_items) + elif obj_type is set: + return {_sanitize_signature_input(item, depth + 1, max_depth) for item in obj} + elif obj_type is frozenset: + return frozenset(_sanitize_signature_input(item, depth + 1, max_depth) for item in obj) else: # Execution-cache signatures should be built from prompt-safe values. # If a custom node injects a runtime object here, mark it unhashable so @@ -87,11 +128,15 @@ def to_hashable(obj): if obj_type in (int, float, str, bool, bytes, type(None)): return obj elif obj_type is dict: - return frozenset([(to_hashable(k), to_hashable(v)) for k, v in sorted(obj.items(), key=lambda kv: repr(kv[0]))]) - elif obj_type in (list, tuple): - return frozenset(zip(itertools.count(), [to_hashable(i) for i in obj])) - elif obj_type in (set, frozenset): - return frozenset([to_hashable(i) for i in obj]) + return ("dict", frozenset((to_hashable(k), to_hashable(v)) for k, v in obj.items())) + elif obj_type is list: + return ("list", tuple(to_hashable(i) for i in obj)) + elif obj_type is tuple: + return ("tuple", tuple(to_hashable(i) for i in obj)) + elif obj_type is set: + return ("set", frozenset(to_hashable(i) for i in obj)) + elif obj_type is frozenset: + return ("frozenset", frozenset(to_hashable(i) for i in obj)) else: return Unhashable() diff --git a/comfy_execution/graph_utils.py b/comfy_execution/graph_utils.py index 75e4bc522..28f53ad95 100644 --- a/comfy_execution/graph_utils.py +++ b/comfy_execution/graph_utils.py @@ -10,7 +10,7 @@ def is_link(obj): return False if type(obj[0]) is not str: return False - if type(obj[1]) not in (int, float): + if type(obj[1]) is not int: return False return True