Skip to content

fix: rating service now fails closed on NaN/Inf values#311

Open
aryash45 wants to merge 2 commits into
AOSSIE-Org:mainfrom
aryash45:fix/rating-service-fail-closed
Open

fix: rating service now fails closed on NaN/Inf values#311
aryash45 wants to merge 2 commits into
AOSSIE-Org:mainfrom
aryash45:fix/rating-service-fail-closed

Conversation

@aryash45

@aryash45 aryash45 commented Feb 9, 2026

Copy link
Copy Markdown

Closes #298

  • Added validatePlayerMetrics() that returns error instead of resetting
  • Updated sanitizePlayerStats() to return error on invalid values
  • Fixed unrelated sprintf bug in debatevsbot.go

Summary by CodeRabbit

  • Bug Fixes
    • Improved rating system validation to detect and reject invalid player metrics and return clear errors, preventing bad data from being persisted.
    • Fixed an extraneous placeholder in the opening-statement template so generated opening statements render cleanly.

- Added validatePlayerMetrics() that returns error instead of resetting
- Updated sanitizePlayerStats() to return error on invalid values
- Fixed unrelated sprintf bug in debatevsbot.go
@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Removed an unused format placeholder from the debate opening-statement template and introduced stricter validation and error propagation in the rating service to detect NaN/Inf and invalid RD/volatility, causing early failure instead of silently resetting or correcting player ratings.

Changes

Cohort / File(s) Summary
Template Cleanup
backend/services/debatevsbot.go
Removed a standalone unused format placeholder ("%s") from the Opening Statement phase template.
Rating Validation & Error Handling
backend/services/rating_service.go
Changed sanitizePlayerStats to return errors; added validatePlayerMetrics to detect NaN/Inf and invalid RD/volatility; updated UpdateRatings and updateUserRating to call and propagate these validations, failing early instead of applying silent fallbacks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through code and found a stray "%s" gone,
Then chased the NaNs until the midnight dawn.
No more silent resets, no quiet score decline,
Errors now trumpet when the numbers misalign.
Snacks, carrots, and checks — the ratings stay fine! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately captures the main change: converting NaN/Inf error handling in the rating service from failing open to failing closed, which aligns with the primary objective.
Linked Issues check ✅ Passed The PR directly addresses issue #298 by implementing error-returning validation instead of silent fallback resets, ensuring the rating service now fails closed on NaN/Inf values as required.
Out of Scope Changes check ✅ Passed The PR includes a minor unrelated fix (removing unused sprintf placeholder in debatevsbot.go), which is explicitly mentioned as unrelated but is a small, low-impact change that does not detract from the main objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/services/rating_service.go (1)

72-100: ⚠️ Potential issue | 🟠 Major

sanitizePlayerMetrics closure silently resets values, making validatePlayerMetrics unreachable for those conditions.

The closure on lines 72–94 still performs silent fail-open resets (NaN/Inf → default, RD ≤ 0 → default, Volatility ≤ 0 → default). Since sanitizePlayerStats (lines 64–69) already catches NaN/Inf and returns an error, the closure is dead code for those cases. However, for RD <= 0 (which sanitizePlayerStats does not check), the closure silently resets it before validatePlayerMetrics ever runs, so the validation on line 95–100 can never trigger for that condition. This contradicts the fail-closed objective from issue #298.

The closure should be removed. Let validatePlayerMetrics be the single validation gate, and add the RD <= 0 check to sanitizePlayerStats if you want to catch it early:

Proposed fix
 	ratingSystem.UpdateMatch(userPlayer, opponentPlayer, outcome, debateTime)
 	if err := sanitizePlayerStats(userPlayer, preUserRating, preUserRD); err != nil {
 		return nil, nil, fmt.Errorf("user stats validation failed: %w", err)
 	}
 	if err := sanitizePlayerStats(opponentPlayer, preOpponentRating, preOpponentRD); err != nil {
 		return nil, nil, fmt.Errorf("opponent stats validation failed: %w", err)
 	}
 
-	// Sanitize rating outputs to avoid NaN/Inf values
-	sanitizePlayerMetrics := func(player *rating.Player) {
-		initialRating := ratingSystem.Config.InitialRating
-		initialRD := ratingSystem.Config.InitialRD
-		initialVol := ratingSystem.Config.InitialVol
-
-		if math.IsNaN(player.Rating) || math.IsInf(player.Rating, 0) {
-			player.Rating = initialRating
-		}
-		if math.IsNaN(player.RD) || math.IsInf(player.RD, 0) {
-			player.RD = initialRD
-		}
-		if math.IsNaN(player.Volatility) || math.IsInf(player.Volatility, 0) {
-			player.Volatility = initialVol
-		}
-		if player.RD <= 0 {
-			player.RD = initialRD
-		}
-		if player.Volatility <= 0 {
-			player.Volatility = initialVol
-		}
-	}
-	sanitizePlayerMetrics(userPlayer)
-	sanitizePlayerMetrics(opponentPlayer)
 	if err := validatePlayerMetrics(userPlayer); err != nil {
 		return nil, nil, fmt.Errorf("user Rating calculation error: %w", err)
 	}
🤖 Fix all issues with AI agents
In `@backend/services/rating_service.go`:
- Around line 222-235: sanitizePlayerStats currently declares unused parameters
fallbackRating and fallbackRD; remove these parameters from the
sanitizePlayerStats signature and implementation (keep function name
sanitizePlayerStats and behavior that validates Rating, RD, Volatility, and sets
LastUpdate) and then update every call site that passed the magic numbers (e.g.,
1200.0, 350.0) to call sanitizePlayerStats(player) instead; ensure imports/uses
still compile after the signature change and delete the now-unused variables at
those call sites so no fallback magic numbers remain.
🧹 Nitpick comments (2)
backend/services/rating_service.go (2)

175-194: validatePlayerMetrics largely duplicates sanitizePlayerStats.

Both functions check NaN/Inf on Rating, RD, and Volatility, and both check Volatility ≤ 0. The only addition here is RD <= 0. Consider consolidating into a single validation function to avoid maintaining two nearly identical check lists that can drift apart.


205-220: Defensive re-validation is reasonable, but note the magic numbers.

1200.0 and 350.0 on line 207 are presumably InitialRating and InitialRD — but since those parameters go unused, this is doubly confusing. Once the unused params are removed per the earlier suggestion, this call site simplifies naturally.

Comment thread backend/services/rating_service.go Outdated
- Removed fallbackRating and fallbackRD parameters (were unused)
- Updated all call sites to use simplified signature
- No more magic numbers (1200.0, 350.0) in code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/services/rating_service.go (1)

72-94: ⚠️ Potential issue | 🔴 Critical

The old sanitizePlayerMetrics closure is fail-open code that directly contradicts the PR objective.

This closure silently resets NaN/Inf/non-positive values to initial defaults — exactly the behavior issue #298 describes and this PR is supposed to eliminate. Because it runs after sanitizePlayerStats (which now correctly errors out on NaN/Inf) but before validatePlayerMetrics, it masks any remaining invalid values (e.g., negative RD), making validatePlayerMetrics on lines 95-100 a no-op for those cases.

Remove this entire closure and its two call sites. The fail-closed checks are already handled by sanitizePlayerStats (lines 64-69) and validatePlayerMetrics (lines 95-100).

Proposed fix
 	if err := sanitizePlayerStats(opponentPlayer); err != nil {
 		return nil, nil, fmt.Errorf("opponent stats validation failed: %w", err)
 	}
 
-	// Sanitize rating outputs to avoid NaN/Inf values
-	sanitizePlayerMetrics := func(player *rating.Player) {
-		initialRating := ratingSystem.Config.InitialRating
-		initialRD := ratingSystem.Config.InitialRD
-		initialVol := ratingSystem.Config.InitialVol
-
-		if math.IsNaN(player.Rating) || math.IsInf(player.Rating, 0) {
-			player.Rating = initialRating
-		}
-		if math.IsNaN(player.RD) || math.IsInf(player.RD, 0) {
-			player.RD = initialRD
-		}
-		if math.IsNaN(player.Volatility) || math.IsInf(player.Volatility, 0) {
-			player.Volatility = initialVol
-		}
-		if player.RD <= 0 {
-			player.RD = initialRD
-		}
-		if player.Volatility <= 0 {
-			player.Volatility = initialVol
-		}
-	}
-	sanitizePlayerMetrics(userPlayer)
-	sanitizePlayerMetrics(opponentPlayer)
 	if err := validatePlayerMetrics(userPlayer); err != nil {
🧹 Nitpick comments (1)
backend/services/rating_service.go (1)

64-69: Redundant triple-validation of the same player data — consider consolidating.

Each player's metrics are validated up to three times through the same flow: sanitizePlayerStats (line 64/67), validatePlayerMetrics (line 95/98), then sanitizePlayerStats again in updateUserRating (line 207). The two functions (sanitizePlayerStats and validatePlayerMetrics) perform nearly identical NaN/Inf checks with slightly different coverage (see RD <= 0 gap above).

Consider merging these into a single validation function to reduce duplication and eliminate the risk of the two diverging over time. The LastUpdate zero-value fix from sanitizePlayerStats can remain as a separate concern or a flag.

Also applies to: 95-100, 207-209

@aryash45

Copy link
Copy Markdown
Author

@Zahnentferner please review my pr

@aryash45

Copy link
Copy Markdown
Author

@bhavik-mangla please review this pr

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Rating Reset Exploit via NaN Sanitiization

1 participant