disc: fix misleading bridge-degradation comment, add 04/3E regression test

The comment on the bridge-degradation branch in handle_read_error
claimed it matched the NOT_READY 04/3E sense signature, but
is_bridge_degradation() keys solely on a non-standard SCSI status byte
(anything that is not GOOD/CHECK CONDITION/TRANSPORT FAILURE) and
ignores sense_key/ASC/ASCQ. A real 04/3E bad-sector error arrives as
CHECK CONDITION (0x02), so it never took this branch — it falls through
to the generic NOT_READY retry. Rewrite the comment to describe the
status-byte condition the predicate actually detects, and drop the
parallel misleading note in the not_ready_err test helper.

Add a regression test asserting a NOT_READY 04/3E error is not
classified as bridge degradation and routes to the NOT_READY retry
(3 s pause) rather than the bridge cooldown (15 s pause).
This commit is contained in:
Matthew Jackson
2026-06-23 01:20:04 -07:00
parent a3987e67f2
commit f2c2ff0eb3
+62 -6
View File
@@ -544,11 +544,17 @@ pub fn handle_read_error(err: &Error, ctx: &mut ReadCtx) -> ReadAction {
return ReadAction::AbortPass;
}
// 2. Bridge degradation: NOT_READY with the well-known signature
// (sense_key=2, ASC=0x04, ASCQ=0x3E). Drive's bridge is in a
// semi-stuck state but typically recovers after a long cooldown.
// If we've exhausted our retry budget, fall through to the
// marginal/skip path below.
// 2. Bridge degradation: the SCSI status byte is non-standard —
// neither GOOD (0x00), CHECK CONDITION (0x02), nor TRANSPORT
// FAILURE (0xFF). The USB bridge firmware returns these bogus
// status bytes (e.g. 0x04, 0x05) with empty sense data when it
// enters a semi-stuck state preceding a crash. This is keyed on
// the status byte alone, NOT on sense_key/ASC/ASCQ — a real
// NOT_READY 04/3E bad-sector error arrives as CHECK CONDITION
// (0x02) and is handled by the generic NOT_READY branch below.
// The bridge typically recovers after a long cooldown; if we've
// exhausted our retry budget, fall through to the marginal/skip
// path below.
if err.is_bridge_degradation() && ctx.bridge_degradation_count < BRIDGE_DEGRADATION_MAX_RETRIES
{
ctx.bridge_degradation_count += 1;
@@ -1224,7 +1230,7 @@ mod tests {
sense: Some(ScsiSense {
sense_key: scsi::SENSE_KEY_NOT_READY,
asc: 0x04,
ascq: 0x00, // not 0x3E, so not the bridge-degradation signature
ascq: 0x00,
}),
}
}
@@ -1329,6 +1335,56 @@ mod tests {
);
}
/// The documented BU40N bad-sector signature: NOT_READY
/// (sense_key=2, ASC=0x04, ASCQ=0x3E) delivered as a CHECK CONDITION
/// (status 0x02). This is the case the old comment on the bridge
/// branch wrongly claimed `is_bridge_degradation` matched.
fn not_ready_04_3e_err() -> Error {
Error::DiscRead {
sector: 100,
status: Some(crate::scsi::SCSI_STATUS_CHECK_CONDITION),
sense: Some(ScsiSense {
sense_key: scsi::SENSE_KEY_NOT_READY,
asc: 0x04,
ascq: 0x3E,
}),
}
}
#[test]
fn not_ready_04_3e_does_not_take_bridge_branch() {
// Regression guard for the misleading-comment fix: the bridge
// branch keys on the *status byte* (non-standard, i.e. not
// GOOD/CHECK/TRANSPORT), NOT on the NOT_READY 04/3E sense. A real
// 04/3E bad-sector error arrives as CHECK CONDITION (0x02), so
// `is_bridge_degradation()` must be false for it, and it must
// route to the generic NOT_READY retry (3 s pause) rather than
// the bridge cooldown (15 s pause).
let err = not_ready_04_3e_err();
assert!(
!err.is_bridge_degradation(),
"04/3E arrives as CHECK CONDITION (0x02); it is not bridge degradation"
);
let mut ctx = ReadCtx::for_patch(1);
match handle_read_error(&err, &mut ctx) {
ReadAction::Retry { pause_secs } => {
assert_eq!(
pause_secs, NOT_READY_PAUSE_SECS,
"04/3E must use the generic NOT_READY pause, not the bridge cooldown"
);
assert_ne!(
pause_secs, BRIDGE_DEGRADATION_PAUSE_SECS,
"04/3E must not take the bridge-degradation branch"
);
// Confirm it really went through the NOT_READY path.
assert_eq!(ctx.not_ready_retries, 1);
assert_eq!(ctx.bridge_degradation_count, 0);
}
other => panic!("04/3E should Retry via the NOT_READY path, got {other:?}"),
}
}
#[test]
fn jump_multiplier_caps_and_jump_distance_stays_bounded() {
// CLAUDE.md damage-jump: multiplier doubles per jump but is