-
Notifications
You must be signed in to change notification settings - Fork 15
Harden grid loading: whole-grid validation, solver-output checks, trust-boundary docs #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
133e262
227b86c
2e18a3e
2d653cf
f9ea702
6564101
39027ae
e0daf36
d324994
0bafd6d
fec1f9e
528719e
bf66552
0248125
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| .. _security: | ||
|
|
||
| Security and trust boundaries | ||
| ============================= | ||
|
|
||
| This page describes what lightsim2grid does, and does **not**, protect you from | ||
| when you load a grid or a solver that you did not produce yourself. It is meant | ||
| to help you decide which inputs are safe to accept from a third party. | ||
|
|
||
| The short version | ||
| ----------------- | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
| :widths: 30 70 | ||
|
|
||
| * - Channel | ||
| - Trust required | ||
| * - Unpickling a grid (``pickle.load`` / ``joblib`` / ...) | ||
| - **Trusted input only.** Loading a pickle can execute arbitrary Python | ||
| code *by design* — this is a property of Python's ``pickle`` module, not | ||
| of lightsim2grid. Never unpickle a grid (or anything else) that came from | ||
| an untrusted source. | ||
| * - Loading a solver plugin (:func:`lightsim2grid.load_algorithm_plugin`) | ||
| - **Trusted code only.** A plugin is a native shared library that runs | ||
| in-process; once loaded it can do anything the host process can. Only | ||
| load plugins you built or otherwise trust. | ||
| * - Loading a grid from the fast binary format (``save_binary`` / | ||
| ``load_binary``) | ||
| - **Hardened against untrusted input.** See below. | ||
| * - Building a grid from a source file (pandapower, pypowsybl, matpower, | ||
| powermodels) | ||
| - **Validated on load.** See below. | ||
|
|
||
| Pickle and plugins are trusted-input-only by design | ||
| --------------------------------------------------- | ||
|
|
||
| Unpickling arbitrary data and loading a native plugin are both, fundamentally, | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unpickling arbitrary data, loading native plugin or using a grid2op environment are all fundamentally etc |
||
| ways of running code chosen by whoever produced the input. No amount of | ||
| validation inside lightsim2grid changes that, so the only safe rule is: **do not | ||
| load a pickle or a plugin from a source you do not trust.** This is the same | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add "load a pickle or plugin or call grid2op.make" |
||
| posture as ``numpy``, ``pandas`` (``read_pickle``), ``torch.load`` and any other | ||
| library that supports pickling. | ||
|
|
||
| The version string embedded in a pickled grid is a *compatibility* check (it | ||
| refuses to load a grid saved by a different lightsim2grid version); it is **not** | ||
| a security control. | ||
|
|
||
| The binary format is the channel to use for untrusted data | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer a terminology like "least dangerous", I don't want people to think it's a good idea to load untrusted data (regardless of the protection made here) |
||
| ---------------------------------------------------------- | ||
|
|
||
| The fast binary format (:ref:`binary-serialization`) was designed as a safe, | ||
| additive alternative to pickle. Loading a binary file never executes code from | ||
| the file, and it is validated at two levels: | ||
|
|
||
| * **Byte level.** The reader checks the file's magic number, format version and | ||
| object type, validates every length field against the actual file size | ||
| *before* allocating (so a corrupted size cannot trigger a huge allocation), | ||
| and requires the file to be fully consumed. Malformed, truncated or corrupted | ||
| bytes raise ``RuntimeError``. | ||
| * **Semantic level.** After the bytes are read back into a grid, ``check_grid()`` | ||
| runs automatically (see below): a file that is byte-wise well-formed but | ||
| *inconsistent* — for example one with an out-of-range bus id — raises a clean | ||
| exception instead of leaving the grid in a state that would cause an | ||
| out-of-bounds memory access on the next powerflow. | ||
|
|
||
| ``check_grid()``: whole-grid consistency validation | ||
| --------------------------------------------------- | ||
|
|
||
| :py:meth:`LSGrid.check_grid` (also available as ``GridModel.check_grid``) verifies that a grid | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gridmodel is now called LSGrid. It is deprecated |
||
| is internally consistent and safe to run a powerflow on. It checks that every | ||
| index the grid carries is in range — the bus id of every element, the substation | ||
| id and the position in the topology vector (both optional), and the generator | ||
| slack and remote-regulated bus references. It raises ``IndexError`` (an | ||
| out-of-range index) or ``RuntimeError`` (a structural inconsistency), and returns | ||
| ``None`` for a consistent grid. | ||
|
|
||
| You normally do not need to call it yourself: | ||
|
|
||
| * it runs automatically when a grid is loaded from a pickle or the binary format | ||
| (via ``set_state``); | ||
| * every grid loader (``init_from_pandapower``, ``init_from_pypowsybl``, | ||
| ``init_from_matpower``, ``init_from_powermodels``) calls it before returning. | ||
|
|
||
| It is exposed so you can validate a grid you built or modified by hand. It runs | ||
| in time proportional to the number of elements in the grid, so it is cheap | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So it is relatively cheap |
||
| compared to a powerflow. | ||
|
|
||
| .. note:: | ||
|
|
||
| Release wheels are compiled with ``-O3 -DNDEBUG``, which removes Eigen's and | ||
| the standard library's own bounds checks. ``check_grid()`` (and the size / | ||
| finiteness check applied to the voltages a solver returns) are deliberately | ||
| *not* gated behind that flag: they always run, because they are what turns a | ||
| malformed input into a clean exception rather than an out-of-bounds access. | ||
|
|
||
| A note for solver-plugin authors | ||
| --------------------------------- | ||
|
|
||
| A solver plugin is trusted code, so lightsim2grid does not try to sandbox it. | ||
| It does, however, sanity-check the voltages your ``compute_pf`` returns before | ||
| using them: if you report convergence but return a voltage vector of the wrong | ||
| size, the powerflow raises ``RuntimeError`` (this would otherwise be an | ||
| out-of-bounds read); if you return non-finite values, the solve is reported as | ||
| non-converged rather than propagating ``NaN`` / ``Inf`` into the results. Writing | ||
| a correct plugin therefore means returning ``V`` / ``Va`` / ``Vm`` sized to the | ||
| solver's bus count, and reporting non-convergence yourself when appropriate. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # Copyright (c) 2020-2026, RTE (https://www.rte-france.com) | ||
| # See AUTHORS.txt | ||
| # This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. | ||
| # If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, | ||
| # you can obtain one at http://mozilla.org/MPL/2.0/. | ||
| # SPDX-License-Identifier: MPL-2.0 | ||
| # This file is part of LightSim2grid, LightSim2grid implements a c++ backend targeting the Grid2Op platform. | ||
|
|
||
| """ | ||
| Tests for ``LSGrid.check_grid()`` (the whole-grid consistency validator) from | ||
| Python. | ||
|
|
||
| ``check_grid()`` range-checks every index the grid carries (bus / substation / | ||
| topology-vector) and the finiteness of the physical inputs; it is called | ||
| automatically when a grid is loaded (pickle / binary format, via ``set_state``) | ||
| and by every grid loader (pandapower / pypowsybl / matpower / powermodels). The | ||
| deep ``set_state`` poison cases are covered by the C++ suite (test_check_grid.cpp, | ||
| also run under ASan/UBSan/valgrind); here we check the Python-visible behaviour: | ||
|
|
||
| * a real grid loaded from pandapower passes (the loader calls check_grid, so a | ||
| false positive would make the loader itself raise); | ||
| * check_grid() is reachable from Python and rejects an inconsistent | ||
| topology-vector with a clean exception (``IndexError`` for an out-of-range | ||
| index -- C++ ``std::out_of_range`` --, ``RuntimeError`` for a duplicate -- | ||
| C++ ``std::runtime_error``), never a crash. | ||
| """ | ||
|
|
||
| import unittest | ||
| import warnings | ||
| import numpy as np | ||
|
|
||
| import pandapower.networks as pn | ||
| from lightsim2grid.network import init_from_pandapower | ||
|
|
||
|
|
||
| # an id far above any element count, but still inside the C `int` range | ||
| BIG = 10 ** 6 | ||
|
|
||
|
|
||
| class TestCheckGrid(unittest.TestCase): | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you do same type of tests for substation id (***_to_subid) |
||
| def setUp(self): | ||
| with warnings.catch_warnings(): | ||
| warnings.filterwarnings("ignore") | ||
| net = pn.case14() | ||
| self.grid = init_from_pandapower(net) # the loader calls check_grid() | ||
| self.n_load = len(self.grid.get_loads()) | ||
| self.assertGreater(self.n_load, 1) # case14 has several loads | ||
|
|
||
| def test_loader_accepts_valid_grid(self): | ||
| # init_from_pandapower already ran check_grid() in setUp without raising; | ||
| # calling it again explicitly must also succeed and return None. | ||
| self.assertIsNone(self.grid.check_grid()) | ||
|
|
||
| def test_duplicated_pos_topo_vect_is_rejected(self): | ||
| # put every load at the same position in the topology vector: the collected | ||
| # pos_topo_vect entries can then no longer be a permutation of [0, dim_topo). | ||
| self.grid.set_load_pos_topo_vect(np.zeros(self.n_load, dtype=np.int32)) | ||
| with self.assertRaises(RuntimeError): | ||
| self.grid.check_grid() | ||
|
|
||
| def test_out_of_range_pos_topo_vect_is_rejected(self): | ||
| # distinct positions, but one is far out of range. | ||
| pos = np.arange(self.n_load, dtype=np.int32) | ||
| pos[-1] = BIG | ||
| self.grid.set_load_pos_topo_vect(pos) | ||
| with self.assertRaises(IndexError): | ||
| self.grid.check_grid() | ||
|
|
||
| def test_negative_pos_topo_vect_is_rejected(self): | ||
| pos = np.arange(self.n_load, dtype=np.int32) | ||
| pos[0] = -1 | ||
| self.grid.set_load_pos_topo_vect(pos) | ||
| with self.assertRaises(IndexError): | ||
| self.grid.check_grid() | ||
|
|
||
| def test_pickle_roundtrip_of_valid_grid(self): | ||
| # pickling goes through set_state on load, which runs check_grid(); a valid | ||
| # grid must round-trip cleanly. | ||
| import pickle | ||
| restored = pickle.loads(pickle.dumps(self.grid)) | ||
| self.assertIsNone(restored.check_grid()) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also add (pandapower, pypowsybl, matpower, power models, including grid2op environment)