Skip to content

Commit 50b508e

Browse files
committed
fix(hardening): defensive JSON.parse in undo restore() + interval-handle cleanup + parameter_memory DB row guards
3 CRIT defects (would crash entire LiteGraph canvas on corrupted widget data): - js/spline_mask_tracker.js: restore() now try/catches malformed snapshots, validates shape - js/points_bbox_editor.js: same defensive pattern in restore() - js/spline_mask_editor.js: same defensive pattern in restore() 2 HIGH memory leaks (interval kept polling after page unload): - js/mec_complexity_hud.js: setInterval handle stored + cleared on beforeunload - js/mec_token_counter.js: setInterval handle stored + cleared on beforeunload 2 HIGH DB hazards (one corrupted row would abort entire history query): - nodes/parameter_memory.py: try/catch json.loads in _get_history and _diff_runs; malformed rows are now skipped with a warning instead of crashing the node.
1 parent aa3b979 commit 50b508e

6 files changed

Lines changed: 61 additions & 14 deletions

File tree

js/mec_complexity_hud.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,12 @@ app.registerExtension({
165165
_update();
166166

167167
// Periodic safety refresh — in case some custom op mutates the graph
168-
// without going through the hooked methods.
169-
setInterval(_update, 2000);
168+
// without going through the hooked methods. Store the handle so the
169+
// interval can be torn down (e.g. on hot-reload of the extension or
170+
// when the page navigates away).
171+
const _t = setInterval(_update, 2000);
172+
window.addEventListener("beforeunload", () => clearInterval(_t), { once: true });
173+
window.__MEC_COMPLEXITY_HUD_INTERVAL = _t;
170174

171175
console.log("[MEC.ComplexityHUD] Loaded.");
172176
},

js/mec_token_counter.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,10 @@ app.registerExtension({
175175
async setup() {
176176
_injectStyle();
177177
_scanAll();
178-
setInterval(_scanAll, 4000); // catch nodes added by graph load
178+
// Store the handle so the interval can be torn down on page unload.
179+
const _t = setInterval(_scanAll, 4000); // catch nodes added by graph load
180+
window.addEventListener("beforeunload", () => clearInterval(_t), { once: true });
181+
window.__MEC_TOKEN_COUNTER_INTERVAL = _t;
179182
console.log("[MEC.TokenCounter] Loaded.");
180183
},
181184
});

js/points_bbox_editor.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,15 @@ class Editor {
111111
if (this.undoStack.length > HISTORY_LIMIT) this.undoStack.shift();
112112
this.redoStack.length = 0;
113113
}
114-
restore(s) { const o = JSON.parse(s); this.points = o.p || []; this.bboxes = o.b || []; }
114+
restore(s) {
115+
// Defensive: corrupted snapshot would crash entire canvas.
116+
let o;
117+
try { o = JSON.parse(s); }
118+
catch (e) { console.warn("[MEC.PointsBBoxEditor] restore: malformed snapshot, ignored", e); return; }
119+
if (!o) return;
120+
this.points = Array.isArray(o.p) ? o.p : [];
121+
this.bboxes = Array.isArray(o.b) ? o.b : [];
122+
}
115123
undo() {
116124
if (!this.undoStack.length) return false;
117125
this.redoStack.push(this.snapshot());

js/spline_mask_editor.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,13 @@ class Editor {
9393
this.redoStack.length = 0;
9494
}
9595
restore(s) {
96-
const o = JSON.parse(s);
97-
this.shapes = o.s || [];
98-
this.active = o.a ?? -1;
96+
// Defensive: corrupted snapshot would crash entire canvas.
97+
let o;
98+
try { o = JSON.parse(s); }
99+
catch (e) { console.warn("[MEC.SplineMaskEditor] restore: malformed snapshot, ignored", e); return; }
100+
if (!o) return;
101+
this.shapes = Array.isArray(o.s) ? o.s : [];
102+
this.active = Number.isFinite(o.a) ? o.a : -1;
99103
}
100104
undo() {
101105
if (!this.undoStack.length) return false;

js/spline_mask_tracker.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,21 @@ class TrackerState {
108108
this.redoStack.length = 0;
109109
}
110110
restore(s) {
111-
const o = JSON.parse(s);
112-
this.keyframes = new Map(o.k.map(([f, pts]) =>
113-
[Number(f), pts.map(p => ({ x: +p.x, y: +p.y }))]
114-
));
115-
this.curFrame = o.f ?? 0;
111+
// Guard: undo/redo snapshots are produced by snapshot() so they
112+
// SHOULD be valid JSON, but a corrupted widget value or a runaway
113+
// dev-tools manipulation would crash the entire LiteGraph canvas.
114+
let o;
115+
try { o = JSON.parse(s); }
116+
catch (e) { console.warn("[MEC.SplineMaskTracker] restore: malformed snapshot, ignored", e); return; }
117+
if (!o || !Array.isArray(o.k)) return;
118+
try {
119+
this.keyframes = new Map(o.k.map(([f, pts]) =>
120+
[Number(f), (pts || []).map(p => ({ x: +p.x, y: +p.y }))]
121+
));
122+
this.curFrame = Number(o.f) || 0;
123+
} catch (e) {
124+
console.warn("[MEC.SplineMaskTracker] restore: malformed snapshot shape, ignored", e);
125+
}
116126
}
117127
undo() {
118128
if (!this.undoStack.length) return false;

nodes/parameter_memory.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,20 @@ def _query_history(node_class: str = "", last_n_runs: int = 10) -> list[dict]:
141141

142142
results = []
143143
for r in rows:
144+
# Defensive: a corrupted delta_json row would otherwise abort the
145+
# entire history query. Skip the bad row and log it.
146+
try:
147+
delta = json.loads(r[5])
148+
except (json.JSONDecodeError, TypeError) as e:
149+
logger.warning(
150+
f"[MEC] param_history: dropping row ts={r[0]} run_id={r[1]} "
151+
f"node_id={r[2]} with malformed delta_json ({e})"
152+
)
153+
continue
144154
results.append({
145155
"ts": r[0], "run_id": r[1], "node_id": r[2],
146156
"node_title": r[3], "node_class": r[4],
147-
"delta": json.loads(r[5]),
157+
"delta": delta,
148158
})
149159
return results
150160
except Exception as e:
@@ -165,9 +175,17 @@ def _get_run_deltas(rid):
165175
).fetchall()
166176
nodes = {}
167177
for r in rows:
178+
try:
179+
delta = json.loads(r[3])
180+
except (json.JSONDecodeError, TypeError) as e:
181+
logger.warning(
182+
f"[MEC] _diff_runs: dropping node_id={r[0]} with "
183+
f"malformed delta_json ({e})"
184+
)
185+
continue
168186
nodes[r[0]] = {
169187
"node_id": r[0], "node_title": r[1],
170-
"node_class": r[2], "delta": json.loads(r[3]),
188+
"node_class": r[2], "delta": delta,
171189
}
172190
return nodes
173191

0 commit comments

Comments
 (0)