Repo: github.com/hq-opensource/predictive-control
Channel: GitHub Issue — label: security / vulnerability
Subject: [Responsible Disclosure] 3 security defects in predictive-control — CRITICAL + HIGH + MEDIUM
Hello,
My name is Dominik Blain, Co-Founder at QreativeLab (Gatineau, QC). I conduct formal verification analysis of open-source software using the Z3 SMT solver, AST analysis, and runtime verification.
I identified three defects in hq-opensource/predictive-control affecting grid constraint enforcement, schedule validation, and overload notification. Two are confirmed by runtime execution, one is formally proven via Z3 SMT solver.
I am disclosing these under a 90-day responsible disclosure window. I will not publish technical details publicly before that window expires or before a fix is confirmed — whichever comes first.
Finding HQ-001 — CRITICAL
File: src/cold_pickup_mpc/real_time/power_limit_mpc.py, line 146
CWE: CWE-704 — Incorrect Type Conversion
Type: Operational Defect (OD)
Vulnerable code:
security_limit = float(os.getenv("SECURITY_LIMIT", "0,5"))
Root cause:
The default value "0,5" uses French decimal notation (comma separator). Python's float() strictly requires a period separator. The call raises ValueError: could not convert string to float: '0,5' on every iteration when SECURITY_LIMIT is not explicitly set in the environment.
The ValueError propagates into the outer try/except block in _needs_curtailment(), which silently catches the exception and sleeps. Grid constraint enforcement is permanently disabled in any deployment without an explicit SECURITY_LIMIT environment variable.
Runtime verification (reproducible in 5 seconds):
>>> float("0,5")
ValueError: could not convert string to float: '0,5'
Impact:
_needs_curtailment() never returns True. Buildings can exceed configured power limits indefinitely without triggering the curtailment response. No error is logged at the constraint check level — the failure is silent.
Fix:
security_limit = float(os.getenv("SECURITY_LIMIT", "0.5"))
Finding HQ-002 — HIGH
File: src/cold_pickup_mpc/real_time/power_limit_mpc.py, line 304
CWE: CWE-670 — Always-Incorrect Control Flow Implementation
Type: Incorrect Reasoning (IR)
Vulnerable code:
last_limit = max(self.power_limit.keys()) # maximum timestamp
first_limit = min(self.power_limit.keys()) # minimum timestamp
if last_limit < timestamp < first_limit: # ← always False
return None
Root cause:
last_limit is by definition ≥ first_limit. The compound condition last_limit < timestamp < first_limit requires simultaneously that timestamp > last_limit and timestamp < first_limit, which is mathematically impossible when last_limit ≥ first_limit.
Formal Z3 proof:
Variables: last_t (Int), first_t (Int), ts (Int)
Constraint added: last_t >= first_t (invariant: max >= min)
Query: Exists ts such that last_t < ts < first_t
Result: UNSAT — no such ts exists
Conclusion: the out-of-range check is dead code
Impact:
Timestamps that fall outside the configured schedule window are never detected. The real-time controller (RtcMpc) may apply power limits when none should be in effect, or may continue operating outside the intended scheduling window without any boundary enforcement.
Fix:
if timestamp < first_limit or timestamp > last_limit:
return None
Finding HQ-004 — MEDIUM
File: src/cold_pickup_mpc/real_time/power_limit_mpc.py, line 279
CWE: CWE-476 — NULL Pointer Dereference
Type: Control-flow Issue (CI)
Vulnerable code:
def _trigger_notification(self):
total_power = self._get_total_consumption() # returns float | None
current_power_limit = self._get_current_power_limit(...) # returns float | None
if total_power > current_power_limit: # TypeError if either is None
self._send_overload_notification(...)
Root cause:
Both _get_total_consumption() and _get_current_power_limit() can return None on database query failure. The comparison None > float raises TypeError. The exception is caught by the outer loop's try/except, and the overload notification is never sent.
Runtime verification:
>>> None > 10.0
TypeError: '>' not supported between instances of 'NoneType' and 'float'
Impact:
Any transient database failure (connection timeout, pool exhaustion) silences all overload notifications for the duration of the failure. Operators receive no alert about buildings exceeding power limits.
Fix:
if total_power is not None and current_power_limit is not None:
if total_power > current_power_limit:
self._send_overload_notification(...)
Timeline
- 2026-04-02 — Initial disclosure sent
- 2026-07-01 — 90-day disclosure window expires (public disclosure if no response)
I am happy to provide additional technical context, Z3 proof scripts, or clarification on any of the above findings.
Thank you for maintaining this project as open source. It is precisely because it is public that we were able to identify and report these issues.
Dominik Blain
Co-Founder, QreativeLab
Gatineau, QC
security@qreativelab.io
https://cobalt-live.vercel.app
Repo: github.com/hq-opensource/predictive-control
Channel: GitHub Issue — label: security / vulnerability
Subject: [Responsible Disclosure] 3 security defects in predictive-control — CRITICAL + HIGH + MEDIUM
Hello,
My name is Dominik Blain, Co-Founder at QreativeLab (Gatineau, QC). I conduct formal verification analysis of open-source software using the Z3 SMT solver, AST analysis, and runtime verification.
I identified three defects in
hq-opensource/predictive-controlaffecting grid constraint enforcement, schedule validation, and overload notification. Two are confirmed by runtime execution, one is formally proven via Z3 SMT solver.I am disclosing these under a 90-day responsible disclosure window. I will not publish technical details publicly before that window expires or before a fix is confirmed — whichever comes first.
Finding HQ-001 — CRITICAL
File:
src/cold_pickup_mpc/real_time/power_limit_mpc.py, line 146CWE: CWE-704 — Incorrect Type Conversion
Type: Operational Defect (OD)
Vulnerable code:
Root cause:
The default value
"0,5"uses French decimal notation (comma separator). Python'sfloat()strictly requires a period separator. The call raisesValueError: could not convert string to float: '0,5'on every iteration whenSECURITY_LIMITis not explicitly set in the environment.The
ValueErrorpropagates into the outertry/exceptblock in_needs_curtailment(), which silently catches the exception and sleeps. Grid constraint enforcement is permanently disabled in any deployment without an explicitSECURITY_LIMITenvironment variable.Runtime verification (reproducible in 5 seconds):
Impact:
_needs_curtailment()never returnsTrue. Buildings can exceed configured power limits indefinitely without triggering the curtailment response. No error is logged at the constraint check level — the failure is silent.Fix:
Finding HQ-002 — HIGH
File:
src/cold_pickup_mpc/real_time/power_limit_mpc.py, line 304CWE: CWE-670 — Always-Incorrect Control Flow Implementation
Type: Incorrect Reasoning (IR)
Vulnerable code:
Root cause:
last_limitis by definition ≥first_limit. The compound conditionlast_limit < timestamp < first_limitrequires simultaneously thattimestamp > last_limitandtimestamp < first_limit, which is mathematically impossible whenlast_limit ≥ first_limit.Formal Z3 proof:
Impact:
Timestamps that fall outside the configured schedule window are never detected. The real-time controller (
RtcMpc) may apply power limits when none should be in effect, or may continue operating outside the intended scheduling window without any boundary enforcement.Fix:
Finding HQ-004 — MEDIUM
File:
src/cold_pickup_mpc/real_time/power_limit_mpc.py, line 279CWE: CWE-476 — NULL Pointer Dereference
Type: Control-flow Issue (CI)
Vulnerable code:
Root cause:
Both
_get_total_consumption()and_get_current_power_limit()can returnNoneon database query failure. The comparisonNone > floatraisesTypeError. The exception is caught by the outer loop'stry/except, and the overload notification is never sent.Runtime verification:
Impact:
Any transient database failure (connection timeout, pool exhaustion) silences all overload notifications for the duration of the failure. Operators receive no alert about buildings exceeding power limits.
Fix:
Timeline
I am happy to provide additional technical context, Z3 proof scripts, or clarification on any of the above findings.
Thank you for maintaining this project as open source. It is precisely because it is public that we were able to identify and report these issues.
Dominik Blain
Co-Founder, QreativeLab
Gatineau, QC
security@qreativelab.io
https://cobalt-live.vercel.app