Skip to content

Unknown param-file keys silently become PARAM_* env vars — a mistyped or invented spend control caps nothing #526

Description

@scttfrdmn

Severity: medium — turns a class of typos and wrong guesses into silent misconfiguration

Any param-file key the sweep parser does not recognise becomes a PARAM_<key> environment
variable, with no warning. That is the correct behaviour for intentional user parameters —
it is how a sweep passes values to a workload — but it means the parser can never say "that
isn't a thing." A typo, a renamed key, or a plausible-sounding key that simply does not exist
all produce a launch that looks completely healthy and does something other than what the
file says.

cmd/sweep.go:188-190:

default:
    // All unknown fields become parameters (PARAM_* env vars)
    config.Parameters[key] = fmt.Sprintf("%v", val)

Concrete failures this allows, all of which launch and bill normally:

written in the param file what the user believes what happens
cost_limit: 8 a per-instance $8 cap PARAM_cost_limit=8; no cap (#525)
budget: 50 a sweep budget PARAM_budget=50; nothing
max_concurrent: 3 a concurrency cap PARAM_max_concurrent=3; nothing (it is a CLI flag)
instance_types: [...] several types PARAM_instance_types; every row on the CLI default type
ttl_hours: 4 a TTL (misremembering ttl:) PARAM_ttl_hours=4; no TTL
on-complete: terminate hyphen instead of underscore PARAM_on-complete; instance never terminates

The last two are the ones that cost money: the user wrote a lifecycle bound, the parser
accepted the file without complaint, and the instance has no bound.

Why file this separately from #525

So it isn't lost inside it. #525 is about two specific flags; this is the mechanism that
makes every future spend-control key fail the same way. Fixing it turns a class of silent
misconfigurations into loud errors, and it is the precondition for #525 option (b)
("set cost_limit: per row") being a real fix rather than a relocation of the problem.

Expected behaviour

PARAM_* passthrough must stay — it is the feature. What is missing is a way to distinguish
"a parameter for my workload" from "a spawn setting I got wrong." Options, roughly in order
of preference:

  1. A reserved-name denylist that errors. Reject keys that are near-misses for real spawn
    controls or CLI-only flags — cost_limit, budget, max_concurrent, instance_types,
    ttl_hours, on-complete, and any recognised key written with hyphens instead of
    underscores. Error message should name the right spelling or say the control is CLI-only.
    Cheap, and it catches every row in the table above.
  2. An explicit namespace for passthrough, e.g. params:/env: sub-map, with unknown
    top-level keys erroring. Cleanest, but breaking; needs a deprecation path.
  3. At minimum, log every PARAM_* derived from an unrecognised key at launch time, so
    PARAM_cost_limit=8 appears in the output where a user might notice it. Weakest — it is
    still a warning nobody reads — but strictly better than silence and non-breaking.

(1) plus (3) together would be a good landing spot: loud failure on the known-dangerous
names, visible listing for the rest.

Note the related trap on the same surface: unknown keys at the top level of a param file
(outside defaults:/grid:/params:) are dropped entirely — spawn's own
examples/simple-params.yaml puts region:/instance_type:/ami: there, where they are
discarded. That is arguably a separate bug, but it is the same "unrecognised input accepted
in silence" pattern and probably wants the same fix.

Regression test to add

given  a param file with `cost_limit: 8` under defaults:
when   the params are parsed
then   parsing FAILS with an error naming cost_limit as unsupported
       (and NOT a config carrying Parameters["cost_limit"])

given  a param file with `my_workload_arg: 42`
when   the params are parsed
then   parsing succeeds and Parameters["my_workload_arg"] == "42"

The second case is as important as the first: the fix must not break intentional
passthrough, which is the whole point of the default: branch.

Cross-references

Found while building a GROMACS benchmark sweep for the cost-to-result project (recorded
there as finding 15 in docs/platform-findings.md). A draft matrix in that project had
cost_limit: 8 under defaults: with the comment "the ENFORCED cap"; the parser accepted
it, and it capped nothing. That project's harness/check_matrix.py now carries a hardcoded
denylist of exactly these keys — a check that belongs upstream, in the parser, not in each
consumer that happens to have been burned.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:launchlaunch path: instance-type, AMI, user-data, runbugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions