Prevent canonicalizing is_changed

This commit is contained in:
xmarre 2026-03-15 16:14:23 +01:00
parent dbed5a1b52
commit f1d91a4c8c
2 changed files with 61 additions and 1 deletions

View File

@ -67,6 +67,22 @@ _MAX_SIGNATURE_CONTAINER_VISITS = 10_000
_FAILED_SIGNATURE = object()
def _shallow_is_changed_signature(value):
"""Sanitize execution-time `is_changed` values without deep recursion."""
value_type = type(value)
if value_type in _PRIMITIVE_SIGNATURE_TYPES:
return value
if value_type is list or value_type is tuple:
try:
items = tuple(value)
except RuntimeError:
return Unhashable()
if all(type(item) in _PRIMITIVE_SIGNATURE_TYPES for item in items):
container_tag = "is_changed_list" if value_type is list else "is_changed_tuple"
return (container_tag, items)
return Unhashable()
def _primitive_signature_sort_key(obj):
"""Return a deterministic ordering key for primitive signature values."""
obj_type = type(obj)
@ -429,7 +445,7 @@ class CacheKeySetInputSignature(CacheKeySet):
node = dynprompt.get_node(node_id)
class_type = node["class_type"]
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
signature = [class_type, await self.is_changed_cache.get(node_id)]
signature = [class_type, _shallow_is_changed_signature(await self.is_changed_cache.get(node_id))]
if self.include_node_id_in_input() or (hasattr(class_def, "NOT_IDEMPOTENT") and class_def.NOT_IDEMPOTENT) or include_unique_id_in_input(class_type):
signature.append(node_id)
inputs = node["inputs"]

View File

@ -309,3 +309,47 @@ def test_get_node_signature_returns_top_level_unhashable_for_tainted_signature(c
signature = asyncio.run(key_set.get_node_signature(dynprompt, "node"))
assert isinstance(signature, caching.Unhashable)
def test_shallow_is_changed_signature_accepts_primitive_lists(caching_module):
"""Primitive-only `is_changed` lists should stay hashable without deep descent."""
caching, _ = caching_module
sanitized = caching._shallow_is_changed_signature([1, "two", None, True])
assert sanitized == ("is_changed_list", (1, "two", None, True))
def test_shallow_is_changed_signature_fails_closed_on_nested_containers(caching_module):
"""Nested containers from `is_changed` should be rejected immediately."""
caching, _ = caching_module
sanitized = caching._shallow_is_changed_signature([1, ["nested"]])
assert isinstance(sanitized, caching.Unhashable)
def test_get_immediate_node_signature_marks_recursive_is_changed_unhashable(caching_module, monkeypatch):
"""Recursive `is_changed` payloads should be cut off before signature canonicalization."""
caching, nodes_module = caching_module
monkeypatch.setitem(nodes_module.NODE_CLASS_MAPPINGS, "UnitTestNode", _DummyNode)
is_changed_value = []
is_changed_value.append(is_changed_value)
dynprompt = _FakeDynPrompt(
{
"node": {
"class_type": "UnitTestNode",
"inputs": {"value": 5},
}
}
)
key_set = caching.CacheKeySetInputSignature(
dynprompt,
["node"],
_FakeIsChangedCache({"node": is_changed_value}),
)
signature = asyncio.run(key_set.get_immediate_node_signature(dynprompt, "node", {}))
assert isinstance(signature[1], caching.Unhashable)