Skip to content

Fixes #XXXXX - Fix RHSM OS TOCTOU race during concurrent registration#11059

Open
pablomh wants to merge 1 commit into
theforeman:developfrom
pablomh:fix/rhsm-os-toctou-race
Open

Fixes #XXXXX - Fix RHSM OS TOCTOU race during concurrent registration#11059
pablomh wants to merge 1 commit into
theforeman:developfrom
pablomh:fix/rhsm-os-toctou-race

Conversation

@pablomh

@pablomh pablomh commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Under concurrent bulk registration, create_or_find_by in RhsmFactParser#operatingsystem can return an unpersisted Operatingsystem object when the model-level uniqueness validation catches a duplicate committed between the initial find_by_attributes lookup and the create attempt
  • Rails create_or_find_by only rescues RecordNotUnique (DB constraint), not RecordInvalid (model validation). Assigning the invalid object to the host silently sets operatingsystem_id=NULL, causing step 3 POST /register to fail with 422 "Must provide an operating system"
  • Replace create_or_find_by with create! + rescue for both RecordInvalid and RecordNotUnique, retrying the find on either. Re-raise if the retry also returns nil (genuine validation failure, not a race)

Root cause

Operatingsystem has three model-level uniqueness validations (name scoped to major/minor, description, and title). During the first registration burst when the OS record does not yet exist:

  1. Thread A: find_by_attributes → nil, create_or_find_by → creates OS (id=4), commits
  2. Thread B: find_by_attributes → nil (Thread A not yet committed), create_or_find_by → internal create runs model validations → SELECT finds OS (Thread A now committed) → validation fails → returns unpersisted object (id=nil) → no RecordNotUnique raisedcreate_or_find_by returns the invalid object
  3. host.operatingsystem = invalid_oshost.operatingsystem_id = nil
  4. host.save persists architecture_id but operatingsystem_id stays NULL
  5. Step 3 POST /registerinitial_configuration_template → 422

Test plan

  • test_operatingsystem_returns_existing_os_after_record_not_unique — DB constraint race path
  • test_operatingsystem_returns_existing_os_after_uniqueness_validation_conflict — model validation race path (the confirmed production failure)
  • test_operatingsystem_raises_non_race_validation_failure — genuine validation errors propagate instead of being silently swallowed

🤖 Generated with Claude Code

Under concurrent bulk registration, create_or_find_by in
RhsmFactParser#operatingsystem can return an unpersisted
Operatingsystem object when the model-level uniqueness validation
catches a duplicate that was committed between the initial
find_by_attributes lookup and the create attempt.

Rails create_or_find_by only rescues ActiveRecord::RecordNotUnique
(DB constraint), not ActiveRecord::RecordInvalid (model validation).
When the model validation fires first, create returns an invalid object
with id=nil. Assigning this to the host silently sets
operatingsystem_id=NULL, which causes the subsequent POST /register
to fail with 422 Must provide an operating system.

Replace create_or_find_by with create! wrapped in a rescue for both
RecordInvalid and RecordNotUnique. On either failure, retry the
find_by_attributes lookup. If the retry also returns nil (genuine
validation failure, not a race), re-raise the original exception.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant