Skip to content

security: validate observed-score equating controls without caller coercion #685

Description

@seonghobae

Public validation defect

Protected main a7be9eb8ca0b807364173f8471fc300e3cf9350f performs caller-controlled Python conversion before package-owned validation in the observed-score equating boundary:

  • _infer_k(..., k, ...) returns int(k) for explicit score ceilings;
  • equate_observed_scores(..., method=...) passes method=str(method) to Rust and stringifies it again when constructing EquateResult.

A caller-defined __int__, __str__, or __repr__ can therefore execute or raise arbitrary exceptions before the finite equating-control vocabulary and score-ceiling domain are established. This is a public API error-surface/reliability boundary; it does not imply a defect in the Rust equating arithmetic.

Bounded test-first slice

Start from exact protected main and preserve all accepted numerical semantics.

  1. Add a fail-first public regression using an otherwise valid tiny X/Y score pair and a hostile non-string method object whose __str__/__repr__ raise. The package must reject it with a stable non-reflective ValueError before compiled equating work.
  2. Add a fail-first regression for explicit k_x/k_y using a hostile non-integer object whose __int__/__index__/__repr__ raise. Reject before Rust with a package-owned finite score-ceiling error.
  3. Preserve built-in string method identities and documented aliases exactly; do not silently invent a new method vocabulary. Preserve ordinary Python and genuine NumPy integer ceilings in the documented non-negative domain.
  4. Reject booleans, fractional/non-integer ceilings, negative ceilings, and unsupported/non-string methods without reflecting caller content or executing conversion/representation hooks.
  5. Validate semantic controls before expensive/result-producing Rust work. Tests must install a sentinel compiled-core callable and prove rejected controls never reach it.
  6. Preserve accepted-input score arrays, inferred-ceiling behavior, Rust-owned mean/linear/equipercentile equations, result values, and caller-array immutability.
  7. Keep this first slice limited to equate_observed_scores plus its shared explicit-ceiling helper. NEAT method/anchor_kind/w1, loglinear degree and kernel controls require separately bounded follow-ups after this public helper is accepted; do not silently widen.
  8. After product GREEN add concise doctoring grounded in Python 3.14's supported built-in/data-model conversion semantics plus the existing Kolen & Brennan (2014) scientific basis, an authoritative changelog fragment, exact changed-production statement/branch coverage and complete public docs; deterministically render/check CHANGELOG.md before Ready.
  9. Require one unchanged final head with focused/full Python, Rust/PyO3/package/GPU-existing/fuzz/Security/SAST/current-head review and repository governance evidence.

Writer/dependency boundary

Fresh open-PR search found no active PR changing python/fast_mlsirm/equating.py; #663 is a distinct fixed-anchor IRT linking Rust-ownership test and currently changes only its linking ownership regression. Refetch exact head/main/target blob/open writer evidence before every mutation and abort if another equating writer appears. Canonical cross-cutting documentation remains #604/#621.

Scientific boundary

This patch changes validation order and error ownership only. All observed-score equating arithmetic remains Rust-owned. It must not alter the estimand, continuization, score-distribution handling, or interpretation, and it makes no validity/equating-design claim beyond the existing documented contract.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions