diff --git a/ToDo.txt b/ToDo.txt index 6e0c6c2..22f9c42 100644 --- a/ToDo.txt +++ b/ToDo.txt @@ -1,19 +1,31 @@ Seed handling and input hardening (deferred from the B-list fixes) ===================================================================== -- Validate ranSeed in the GUI and startSequence: it must be an integer in a - safe range (e.g. -2^31 .. 2^31-1) if a fixed seed is requested. Blocks a - crafted share link from injecting a garbage seed value into the input deck. -- Derive per-scan-point seeds deterministically from the base seed and the +DONE - Validate ranSeed in the GUI and startSequence: it must be an integer in a + safe range (|v| <= 2^31-1, matching ISEED = IDINT(RI) in trimspNL.F). Enforced + in runSimulationSequence() (before any job is generated) AND in CreateInpFile() + (defense-in-depth), so a crafted share link or direct deck builder is blocked + with a "please change it" message. Non-numeric / NaN / non-integer / oversized + seeds no longer reach the engine. +DONE - Derive per-scan-point seeds deterministically from the base seed and the scan index, so a multi-point run is reproducible in any finish order - (currently: initialSeed + iScan, but only when the user set a seed). -- Warn when a shared configuration fixed the RanSeed: every scan point then - uses the same generator, which can produce statistically misleading results. -- Cap client-controlled magnitudes: numberProj (projectile count) and - valEnergy must be upper-bounded from share links before the engine runs, to - prevent an accidental or malicious "run forever / write enormous .rge" job. -- Consider persisting the effective per-point seed back into the *.seq output - metadata so results self-document their RNG state. + (currently: initialSeed + iScan, only when the user set a valid seed). +DONE - Cap valEnergy magnitude at 60000 eV (positive incidence mode and the + Maxwellian transmission mode |E0| = temperature). Stopping fits (KDEE1=4) are + documented valid to ~50 keV for helium (meaning-of-params.txt:177-185), so 60 + keV is the model's fitted ceiling. Applied to scanned energies and the base + value in runSimulationSequence() and CreateInpFile(). +- Warn when a shared configuration fixed the RanSeed: every scan point then uses + the same generator, which can produce statistically misleading results. +- Cap client-controlled numberProj magnitude: an unbounded projectile count (e.g. + 1e9) does not terminate within the 300 s watchdog and can flood scan points + (empirically verified: 1e9 did not finish in 15 s). GUI ProjNumberLimit() is + currently dead code (defined at TrimSPlib.js:3516, never wired to an event). +- Persisting the effective per-point seed: the seed already lives in each + archived *.inp (all three deck slots), so reproducibility does not require the + *.seq header change; extending *.seq would shift the 999(A20,1x) layer slots + and break getSequenceColumnInfo(). Surfacing it in run details instead is + deferred - not sure it is needed. Deferred follow-ups from the C-list CGI hardening ================================================= diff --git a/TrimSPlib.js b/TrimSPlib.js index 0e8e4dd..c698610 100644 --- a/TrimSPlib.js +++ b/TrimSPlib.js @@ -3634,6 +3634,16 @@ function setValues(content) { function CreateInpFile() { + // Gate client-controlled magnitudes here too so a crafted config can never + // reach the engine, regardless of which caller builds the deck. + var inpSeedError = validateSeedString(All["ranSeed"]); + if (inpSeedError) { + throw new Error(inpSeedError); + } + var inpEnergyError = validateEnergyValue(All["valEnergy"]); + if (inpEnergyError) { + throw new Error(inpEnergyError); + } // Create and return an input deck for the TRIM.SP-NL transport engine. // Takes object All as input for all relevant parameters @@ -3781,6 +3791,45 @@ var TRIMSP_WEB_MAX_CONCURRENCY = 4; // an effectively unlimited list of jobs and freeze the tab. var TRIMSP_MAX_SCAN_POINTS = 5000; +// Validated limits for client-owned input values. The Ziegler/ICRU stopping +// fits are only tuned up to ~50 keV for helium (KDEE1=4, see +// meaning-of-params.txt:177-185), so energies beyond 60 keV are outside the +// model's fitted validity. Negative energies select the Maxwellian initial +// distribution where |E0| is used as the ion temperature (trimspNL.F:806), so +// the same magnitude cap applies to both modes. +var TRIMSP_ENERGY_CAP_EV = 60000; + +// The engine casts the deck seed to a signed 32-bit integer +// (ISEED = IDINT(RI), trimspNL.F:818-827), so any magnitude beyond that is +// silently truncated. Restrict the seed to |v| <= 2^31-1. +var TRIMSP_SEED_MAX_ABS = 2147483647; + +// Returns an empty string when valid, otherwise a message that asks the user +// to change the offending value. +function validateEnergyValue(raw) { + var str = String(raw).trim(); + var value = Number(str); + if (str === "" || !Number.isFinite(value)) { + return "Energy must be numeric. Please change it."; + } + if (Math.abs(value) > TRIMSP_ENERGY_CAP_EV) { + return "Energy magnitude must not exceed 60000 eV (the stopping fits are not validated beyond ~50 keV). Please change it."; + } + return ""; +} + +function validateSeedString(raw) { + var str = String(raw).trim(); + if (str === "" || !/^[+-]?\d+$/.test(str)) { + return "Seed must be an integer. Please change it."; + } + var value = Number(str); + if (Math.abs(value) > TRIMSP_SEED_MAX_ABS) { + return "Seed is out of range (use an integer between -2147483648 and 2147483647). Please change it."; + } + return ""; +} + // Expand a From/Step/To range into a list of scan values. Both ascending and // descending steps are valid (e.g. scanning from high to low energy), so the // old "loop while value <= to" form is replaced by a count-based expansion @@ -3995,12 +4044,29 @@ async function runSimulationSequence() { return(0); } + // Cap the projectile energy magnitude before any job is generated. For an + // energy sweep the scan values are authoritative; for any other sweep the + // base valEnergy must still stay in the fitting range. + var evalues = (ScanAttrib === "valEnergy") ? SValues : [Number(All["valEnergy"])]; + for (var evIndex = 0; evIndex < evalues.length; evIndex++) { + var energyError = validateEnergyValue(evalues[evIndex]); + if (energyError) { + updateStatusBar({ run: energyError }); + return(0); + } + } + // Freeze the initial GUI state and expand the scan into independent jobs. // Each job gets its own input file name and a deterministic seed offset so // the results are reproducible even when runs finish out of order. const scanValues = SValues.slice(); const initialAll = Object.assign({}, All); const initialSeed = parseInt(initialAll["ranSeed"], 10); + const seedError = validateSeedString(initialAll["ranSeed"]); + if (seedError) { + updateStatusBar({ run: seedError }); + return(0); + } let jobs = []; for (let iScan = 0; iScan < scanValues.length; iScan++) {