From 5cada693eabbea5fca1ee0711f7d435bbab8c244 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 13 Feb 2026 20:37:08 +0100 Subject: [PATCH 1/2] did some fixes: sql placeholder mismatch, more flexible db version handling, unsafe eval --- mpqp/local_storage/load.py | 73 +++++++++++++++++++++++++++++-------- mpqp/local_storage/save.py | 8 ++-- mpqp/local_storage/setup.py | 18 ++++++--- 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/mpqp/local_storage/load.py b/mpqp/local_storage/load.py index 1128ba98..998c09c7 100644 --- a/mpqp/local_storage/load.py +++ b/mpqp/local_storage/load.py @@ -2,17 +2,59 @@ storage. In the process, they are converted to MPQP objects (:class:`~mpqp.execution.job.Job` and :class:`~mpqp.execution.result.Result`).""" -# TODO: put DB specific errors here ? - from __future__ import annotations -from typing import Optional +from typing import Any, Optional from mpqp.all import * from mpqp.local_storage.queries import * from mpqp.local_storage.setup import DictDB +def _build_safe_namespace() -> dict[str, Any]: + """Build a restricted namespace containing only the MPQP symbols needed + to deserialize objects stored in the local database. This prevents + arbitrary code execution from tampered database content.""" + import numpy as np + from numpy import array, complex64, float64 # noqa: F401 + + import mpqp.all as _all + + safe_ns: dict[str, Any] = {"__builtins__": {}} + # Pull in all public symbols from mpqp.all + for name in dir(_all): + if not name.startswith("_"): + safe_ns[name] = getattr(_all, name) + # Add numpy helpers commonly found in serialized repr() strings + safe_ns.update( + { + "np": np, + "array": np.array, + "complex64": np.complex64, + "float64": np.float64, + } + ) + return safe_ns + + +_SAFE_NAMESPACE: dict[str, Any] | None = None + + +def _safe_eval(expr: str) -> Any: + """Evaluate an expression in a restricted namespace that only contains + known MPQP symbols. Raises ``ValueError`` on failure.""" + global _SAFE_NAMESPACE + if _SAFE_NAMESPACE is None: + _SAFE_NAMESPACE = _build_safe_namespace() + try: + return eval(expr, _SAFE_NAMESPACE) # noqa: S307 + except Exception as e: + raise ValueError( + f"Failed to safely deserialize from local storage: {e!r}\n" + f"Expression was: {expr[:200]!r}" + ) from e + + def jobs_local_storage_to_mpqp(jobs: Optional[list[DictDB] | DictDB]) -> list[Job]: """Convert a dictionary or list of dictionaries representing jobs into MPQP Job objects. @@ -30,27 +72,26 @@ def jobs_local_storage_to_mpqp(jobs: Optional[list[DictDB] | DictDB]) -> list[Jo """ if jobs is None: return [] - from numpy import array, complex64 # pyright: ignore[reportUnusedImport] jobs_mpqp = [] if isinstance(jobs, dict): - measure = eval(eval(jobs['measure'])) if jobs['measure'] is not None else None + measure = _safe_eval(_safe_eval(jobs['measure'])) if jobs['measure'] is not None else None jobs_mpqp.append( Job( - eval("JobType." + jobs['type']), - eval(eval(jobs['circuit'])), - eval(jobs['device']), + _safe_eval("JobType." + jobs['type']), + _safe_eval(_safe_eval(jobs['circuit'])), + _safe_eval(jobs['device']), measure, ) ) else: for job in jobs: - measure = eval(eval(job['measure'])) if job['measure'] is not None else None + measure = _safe_eval(_safe_eval(job['measure'])) if job['measure'] is not None else None jobs_mpqp.append( Job( - eval("JobType." + job['type']), - eval(eval(job['circuit'])), - eval(job['device']), + _safe_eval("JobType." + job['type']), + _safe_eval(_safe_eval(job['circuit'])), + _safe_eval(job['device']), measure, ) ) @@ -83,28 +124,28 @@ def results_local_storage_to_mpqp( return [] results_mpqp = [] if isinstance(results, dict): - error = eval(eval(results['error'])) if results['error'] is not None else None + error = _safe_eval(_safe_eval(results['error'])) if results['error'] is not None else None job = fetch_jobs_with_id(results['job_id']) if len(job) == 0: raise ValueError("Job not found for result, can not be instantiated.") results_mpqp.append( Result( jobs_local_storage_to_mpqp(job)[0], - eval(eval(results['data'])), + _safe_eval(_safe_eval(results['data'])), error, results['shots'], ) ) else: for result in results: - error = None if result['error'] is None else eval(eval(result['error'])) + error = None if result['error'] is None else _safe_eval(_safe_eval(result['error'])) job = fetch_jobs_with_id(result['job_id']) if len(job) == 0: raise ValueError("Job not found for result, can not be instantiated.") results_mpqp.append( Result( jobs_local_storage_to_mpqp(job)[0], - eval(eval(result['data'])), + _safe_eval(_safe_eval(result['data'])), error, result['shots'], ) diff --git a/mpqp/local_storage/save.py b/mpqp/local_storage/save.py index 9fb7761b..a58a30f4 100644 --- a/mpqp/local_storage/save.py +++ b/mpqp/local_storage/save.py @@ -47,7 +47,7 @@ def insert_jobs(jobs: Job | list[Job]) -> list[int]: cursor.execute( ''' INSERT INTO jobs (type, circuit, device, measure, remote_id, status) - VALUES (?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?) ''', ( job.job_type.name, @@ -71,14 +71,16 @@ def insert_jobs(jobs: Job | list[Job]) -> list[int]: cursor.execute( ''' - INSERT INTO jobs (type, circuit, device, measure) - VALUES (?, ?, ?, ?) + INSERT INTO jobs (type, circuit, device, measure, remote_id, status) + VALUES (?, ?, ?, ?, ?, ?) ''', ( jobs.job_type.name, circuit_json, str(jobs.device), measure_json, + str(jobs.id), + str(jobs.status), ), ) id = cursor.lastrowid diff --git a/mpqp/local_storage/setup.py b/mpqp/local_storage/setup.py index b486e18e..4c136861 100644 --- a/mpqp/local_storage/setup.py +++ b/mpqp/local_storage/setup.py @@ -49,12 +49,20 @@ def wrapper(*args: Any, **kwargs: dict[str, Any]) -> T: db_version = get_database_version() if db_version != DATABASE_VERSION: - raise RuntimeError( - f"""\ -Database version {db_version} is outdated. Current supported version: {DATABASE_VERSION}. -Automated migration is not yet supported, please contact library authors to get\ - help for the migration.""" + import shutil + from warnings import warn + + db_path = get_env_variable("DB_PATH") + backup_path = db_path + f".v{db_version}.bak" + shutil.copy2(db_path, backup_path) + warn( + f"Database version {db_version} is outdated (current: " + f"{DATABASE_VERSION}). The old database has been backed up to " + f"'{backup_path}' and a fresh database will be created.", + stacklevel=2, ) + Path(db_path).unlink() + setup_local_storage(db_path) return func(*args, **kwargs) From 9c8d5495fdd79a7f9429390b7450b7c2f7cddaa1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 16 Feb 2026 09:32:48 +0000 Subject: [PATCH 2/2] chore: Files formated --- mpqp/local_storage/load.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/mpqp/local_storage/load.py b/mpqp/local_storage/load.py index 7691ef46..14b174ac 100644 --- a/mpqp/local_storage/load.py +++ b/mpqp/local_storage/load.py @@ -74,14 +74,18 @@ def jobs_local_storage_to_mpqp(jobs: Optional[list[DictDB] | DictDB]) -> list[Jo return [] from numpy import ( - array, # pyright: ignore[reportUnusedImport] - complex64, # pyright: ignore[reportUnusedImport] - complex128, # pyright: ignore[reportUnusedImport] - ) + array, # pyright: ignore[reportUnusedImport] + complex64, # pyright: ignore[reportUnusedImport] + complex128, # pyright: ignore[reportUnusedImport] + ) jobs_mpqp = [] if isinstance(jobs, dict): - measure = _safe_eval(_safe_eval(jobs['measure'])) if jobs['measure'] is not None else None + measure = ( + _safe_eval(_safe_eval(jobs['measure'])) + if jobs['measure'] is not None + else None + ) jobs_mpqp.append( Job( _safe_eval("JobType." + jobs['type']), @@ -92,7 +96,11 @@ def jobs_local_storage_to_mpqp(jobs: Optional[list[DictDB] | DictDB]) -> list[Jo ) else: for job in jobs: - measure = _safe_eval(_safe_eval(job['measure'])) if job['measure'] is not None else None + measure = ( + _safe_eval(_safe_eval(job['measure'])) + if job['measure'] is not None + else None + ) jobs_mpqp.append( Job( _safe_eval("JobType." + job['type']), @@ -105,7 +113,6 @@ def jobs_local_storage_to_mpqp(jobs: Optional[list[DictDB] | DictDB]) -> list[Jo return jobs_mpqp - def results_local_storage_to_mpqp( results: Optional[list[DictDB] | DictDB], ) -> list[Result]: @@ -131,7 +138,11 @@ def results_local_storage_to_mpqp( return [] results_mpqp = [] if isinstance(results, dict): - error = _safe_eval(_safe_eval(results['error'])) if results['error'] is not None else None + error = ( + _safe_eval(_safe_eval(results['error'])) + if results['error'] is not None + else None + ) job = fetch_jobs_with_id(results['job_id']) if len(job) == 0: raise ValueError("Job not found for result, can not be instantiated.") @@ -145,7 +156,11 @@ def results_local_storage_to_mpqp( ) else: for result in results: - error = None if result['error'] is None else _safe_eval(_safe_eval(result['error'])) + error = ( + None + if result['error'] is None + else _safe_eval(_safe_eval(result['error'])) + ) job = fetch_jobs_with_id(result['job_id']) if len(job) == 0: raise ValueError("Job not found for result, can not be instantiated.")