v0.7.3
Security fixes for parsing untrusted case files. The parsers are written to reject malformed input with an error, never to crash, exhaust memory, or read or write files outside the ones named on the command line; this release closes several gaps in that model.
- A PowerWorld
.auxlegacyDATAheader whose field list closes before it opens (]before[) no longer panics on an inverted slice. It returns a read error, matching the guard the parenthesized form already carried. - OpenDSS
Redirect/Compile/Buscoordsno longer read arbitrary local files. Parsing from a string (parse_dss_str, and the C ABI and Python string entry points) disables filesystem includes entirely, so untrusted.dsstext cannot pull in a file such as/etc/passwdand echo its contents back through bus names. Parsing from a file (parse_dss_file) still follows includes, now confined to the case directory: an include that resolves outside that directory is refused with a warning, whether it climbs out with..or is an absolute path outside the directory. - OpenDSS
phases,windings, andwdgcounts are capped. A single small property could otherwise size a dense n by n conductor matrix or a per-winding vector into the gigabytes; an oversized value clamps to the supported maximum with a warning. - A PMD ENGINEERING impedance or admittance matrix dimension is capped the same way, so an array of thousands of empty rows no longer demands an n by n allocation.
- The matrix pipeline and the CLI
sensitivitiescommand sanitize the case name before it forms an output filename. A name like../../xor an absolute path can no longer steer a write outside the chosen output directory. - The PowerWorld
.pwdreader groups drawing records by tag in logarithmic time and bounds its substation identity search with a probe budget, so a crafted file cannot force quadratic work. - The OpenDSS include confinement holds for a case file parsed by bare filename: an empty case directory no longer passes every path as a prefix match, so
Redirect /etc/passwdinparse_dss_file("master.dss")is refused. A case directory that itself starts with..now confines rather than refusing everything under it. - OpenDSS includes are also checked after symlink resolution: a lexically contained include that is really a symlink out of the case directory is refused.
parse_raw_filenow confines includes exactly likeparse_dss_fileinstead of following them anywhere on disk. sanitize_stemfollows Windows filename rules (trailing dots trimmed, reserved device names likeconprefixed, length capped) and appends a short hash of the original name whenever sanitization changed it, so two distinct case names that sanitize alike (a/banda_b) cannot silently overwrite each other's files in a multi case export. The gridfm dataset writer routes its output directory through the same sanitizer.- A
DistNetworkarriving without reader caps (the model JSON C entry point deserializes one unchecked) can no longer force quadratic allocation out of linear-size input: the BMOPF writer caps the dense zero fill for an absent linecode/shunt matrix and the transformerx_scpair expansion at 64 conductors/windings, with a warning. - A
NetworkJSON bus id nearusize::MAXis rejected when 3-winding transformers are present, closing an integer overflow in the synthetic star bus allocation that could alias an existing bus. An oversized piecewise costncostclamps instead of overflowing during normalization and Surge export. - The Python
mcpextra pins the SDK below 2.0, which removed themcp.server.fastmcpmodule the server imports. - OpenDSS
like=splicing is capped per object. A self-referencing or mutually-referencing chain (Edit Load.a like=arepeated) otherwise doubled an object's property count each edit, so a few hundred bytes could exhaust memory; the splice is now refused with a warning past the cap. - The PMD writer no longer panics on a transformer with no windings, reachable from a PMD or BMOPF document with the winding array absent. The emergency rating default derives from the first winding only when one exists.
- A PSS/E transformer
CODfield of an extreme magnitude (which saturates toi64::MIN) no longer overflows when the control mode is decoded; it reads as a fixed ratio. - The PowerWorld
.auxreader routes a bus's ownBusNum/Numberthrough the same validation as every bus reference, so a fractional or out-of-range value is a read error instead of a silently truncated or saturated id. - A GOC3 bus uid of
bus_<usize::MAX>no longer overflows when its suffix is mapped to a bus id; it falls back to the 1-based document position. - The MCP server's allowed-roots write check follows a symbolic link in the output filename before deciding containment. A dangling link named as the output file, sitting inside an allowed root but pointing outside it, could otherwise pass the check while the write escaped the sandbox.
- Lifting a GOC3 time series into a package operating-point series binds the declared
time_periodsto theinterval_durationarray length (the same equality the SCOPF loader enforces). An oversizedtime_periodsthat does not match the data no longer drives an unbounded per-period allocation; the series is refused with a diagnostic and the package stays static. - The PMD writer caps the conductor count it expands into square matrices. A switch or voltage source terminal map is a linear model array, and a 35 KB case file naming thousands of terminals drove a 360 MB document and 1.5 GB of resident memory; the matrices now emit at 64 conductors with a warning while the terminal list itself stays faithful. Reachable from any distribution case file, not only the unchecked C entry point.
- The dist graph builder caps the transformer winding count feeding its pair expansion, so a
DistNetworkdeserialized without reader caps cannot turn a linear winding array into a quadratic edge list. - A degenerate model matrix (rows shorter than the row count) reads as zero in the PMD writer instead of panicking on an out of range index, matching the DSS writer.
sanitize_stemhashes a name that already carries the disambiguating suffix shape, so the suffixed and unsuffixed name spaces stay disjoint: a case cannot be named to impersonate another case's disambiguated stem, which took no search to construct because the suffix derives from a published hash. The suffix widens to 64 bits, and the DC OPF bundle writer routes its output directory through the same sanitizer instead of a weaker local copy.- The CLI refuses a conversion sidecar whose path is absolute or climbs out of the output directory, closing the join before a writer can reach it.
- Fuzz harnesses cover the distribution family (
dss,pmd_json). Both parse and then write the network back, since a reader cap that does not hold shows up in the consumer that sizes an allocation from it.
What's Changed
Full Changelog: v0.7.2...v0.7.3