From f2e6fddee9c41f11dc83ff73fc6750a34d0e7630 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:26:25 -0500 Subject: [PATCH] Harden Rivian angle and bridge safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep Rivian angle commands below 4 m/s², exercise the common angle-safety coverage, and prevent generic firmware management from overwriting unverified external Black Pandas. Original safety work based on PR #90 by TonyJOM (Anthony Orta). Co-authored-by: TonyJOM --- .../opendbc/car/rivian/tests/test_rivian.py | 6 +++++- opendbc_repo/opendbc/car/rivian/values.py | 6 +++++- .../opendbc/safety/tests/test_rivian.py | 16 +++++++++++----- selfdrive/pandad/rivian_long_flasher.py | 16 +++++++++------- .../pandad/tests/test_rivian_long_flasher.py | 19 ++++++++++++++++++- starpilot/common/starpilot_utilities.py | 13 ++++++++++--- 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/opendbc_repo/opendbc/car/rivian/tests/test_rivian.py b/opendbc_repo/opendbc/car/rivian/tests/test_rivian.py index 93769abf4..55b953d5e 100644 --- a/opendbc_repo/opendbc/car/rivian/tests/test_rivian.py +++ b/opendbc_repo/opendbc/car/rivian/tests/test_rivian.py @@ -14,7 +14,8 @@ from opendbc.car.rivian.fingerprints import FW_VERSIONS from opendbc.car.rivian.interface import CarInterface from opendbc.car.rivian.toi_controller import (TOI_ACK_FRAMES, TOI_MAX_ANGLE_FRAMES, TOI_RECOVERY_TIMEOUT_FRAMES, ToiController, ToiState) -from opendbc.car.rivian.values import CAR, FW_QUERY_CONFIG, WMI, ModelLine, ModelYear, RivianFlags, RivianSafetyFlags +from opendbc.car.rivian.values import (CAR, FW_QUERY_CONFIG, MAX_ALLOWED_LATERAL_ACCEL, CarControllerParams, WMI, + ModelLine, ModelYear, RivianFlags, RivianSafetyFlags) class TestRivian: @@ -173,6 +174,9 @@ class TestRivian: assert not controller.angle_saturated + def test_angle_command_envelope_is_below_product_lateral_accel_limit(self): + assert 0.0 < CarControllerParams.ANGLE_LIMITS.MAX_LATERAL_ACCEL <= MAX_ALLOWED_LATERAL_ACCEL + def test_angle_saturation_param_is_seeded_and_edge_written(self): writes = [] controller = CarController.__new__(CarController) diff --git a/opendbc_repo/opendbc/car/rivian/values.py b/opendbc_repo/opendbc/car/rivian/values.py index 521eddecb..33903b802 100644 --- a/opendbc_repo/opendbc/car/rivian/values.py +++ b/opendbc_repo/opendbc/car/rivian/values.py @@ -113,6 +113,7 @@ GEAR_MAP = { } AVERAGE_ROAD_ROLL = 0.06 # ~3.4 degrees, conservative banked-road allowance for angle safety +MAX_ALLOWED_LATERAL_ACCEL = 4.0 # m/s^2; hard product requirement for Rivian angle control class CarControllerParams: @@ -139,7 +140,10 @@ class CarControllerParams: 500, ([], []), ([], []), - MAX_LATERAL_ACCEL=ISO_LATERAL_ACCEL + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL), + # Keep the vehicle-model command envelope below the product's absolute + # lateral-acceleration ceiling, including the road-roll allowance. + MAX_LATERAL_ACCEL=min(MAX_ALLOWED_LATERAL_ACCEL, + ISO_LATERAL_ACCEL + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL)), MAX_LATERAL_JERK=3.0 + (ACCELERATION_DUE_TO_GRAVITY * AVERAGE_ROAD_ROLL), MAX_ANGLE_RATE=2.5, ) diff --git a/opendbc_repo/opendbc/safety/tests/test_rivian.py b/opendbc_repo/opendbc/safety/tests/test_rivian.py index 32fc22372..37f0f53c2 100755 --- a/opendbc_repo/opendbc/safety/tests/test_rivian.py +++ b/opendbc_repo/opendbc/safety/tests/test_rivian.py @@ -6,7 +6,7 @@ from opendbc.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness from opendbc.car.lateral import get_max_angle_delta_vm, get_max_angle_vm from opendbc.car.structs import CarParams from opendbc.car.vehicle_model import VehicleModel -from opendbc.car.rivian.values import CarControllerParams +from opendbc.car.rivian.values import CarControllerParams, MAX_ALLOWED_LATERAL_ACCEL from opendbc.safety.tests.libsafety import libsafety_py import opendbc.safety.tests.common as common from opendbc.safety.tests.common import CANPackerSafety @@ -168,8 +168,14 @@ class TestRivianAngleSafetyBase(TestRivianSafetyBase, common.AngleSteeringSafety return self.packer.make_can_msg_safety("EPAS_AdasStatus", 0, {"EPAS_InternalSas": angle}) def test_angle_cmd_when_enabled(self): - # The VM-based lateral acceleration and jerk limits are exercised below. - pass + # The VM-based lateral acceleration and jerk limits are exercised by the + # dedicated tests below; keep this common-suite hook explicit rather than + # silently skipping the enabled-angle coverage. + self._check_lateral_accel_limit() + self._check_lateral_jerk_limit() + + def test_product_lateral_accel_limit(self): + self.assertLessEqual(CarControllerParams.ANGLE_LIMITS.MAX_LATERAL_ACCEL, MAX_ALLOWED_LATERAL_ACCEL) def _can_to_deg(self, can_value): return can_value / self.DEG_TO_CAN @@ -180,7 +186,7 @@ class TestRivianAngleSafetyBase(TestRivianSafetyBase, common.AngleSteeringSafety stored = round(speed_kph_can / 3.6 * 1000) return max(stored / 1000.0 - 1.0, 1.0) - def test_lateral_accel_limit(self): + def _check_lateral_accel_limit(self): for speed in np.linspace(0, 40, 100): speed = max(self._round_speed(speed), 1) for sign in (-1, 1): @@ -198,7 +204,7 @@ class TestRivianAngleSafetyBase(TestRivianSafetyBase, common.AngleSteeringSafety should_tx = above_can > self.STEER_ANGLE_MAX * self.DEG_TO_CAN self.assertEqual(should_tx, self._tx(self._angle_cmd_msg(above_deg, True))) - def test_lateral_jerk_limit(self): + def _check_lateral_jerk_limit(self): for speed in np.linspace(0, 40, 100): speed = max(self._round_speed(speed), 1) for sign in (-1, 1): diff --git a/selfdrive/pandad/rivian_long_flasher.py b/selfdrive/pandad/rivian_long_flasher.py index 9722ebb54..9f348e823 100644 --- a/selfdrive/pandad/rivian_long_flasher.py +++ b/selfdrive/pandad/rivian_long_flasher.py @@ -77,13 +77,13 @@ def _flash_panda(panda: Panda) -> None: def is_rivian_bridge_panda(panda: Panda, rivian: bool | None = None) -> bool: if panda.is_internal() or panda.get_type() != Panda.HW_TYPE_BLACK: return False - if rivian is None: - rivian = _is_rivian() - if rivian or panda.bootstub: - return rivian + # A cached Rivian CarParams record is not sufficient evidence to identify a + # bridge. An arbitrary external Black Panda may be connected at the same + # time, and flashing it would permanently replace its firmware. Only the + # signed bridge image can positively identify the hardware. try: expected_signature = Panda.get_signature_from_firmware(FW_PATH) - return panda.get_signature() == expected_signature + return not panda.bootstub and panda.get_signature() == expected_signature except Exception: return False @@ -108,12 +108,14 @@ def prepare_rivian_bridge(panda_serials: list[str]) -> set[str]: continue bridge_confirmed = is_rivian_bridge_panda(panda, rivian) - if not bridge_confirmed: + if not bridge_confirmed and not rivian: continue bridge_serials.add(serial) - if rivian and firmware_available: + if bridge_confirmed and rivian and firmware_available: _flash_panda(panda) + elif rivian: + cloudlog.warning(f"External Black Panda {serial} is not signed with the Rivian bridge firmware; leaving it untouched") except Exception: cloudlog.exception(f"Failed to prepare Rivian Extreme harness bridge {serial}") finally: diff --git a/selfdrive/pandad/tests/test_rivian_long_flasher.py b/selfdrive/pandad/tests/test_rivian_long_flasher.py index 1f9f4884e..0dce59d3e 100644 --- a/selfdrive/pandad/tests/test_rivian_long_flasher.py +++ b/selfdrive/pandad/tests/test_rivian_long_flasher.py @@ -46,8 +46,10 @@ def test_outdated_rivian_bridge_uses_bridge_flash_path(): panda_class.usb_list.return_value = ["bridge"] panda_class.get_signature_from_firmware.return_value = b"expected" + # An unknown firmware image cannot be safely identified as the bridge and + # must be left to explicit provisioning instead of being overwritten. assert flasher.prepare_rivian_bridge(["internal", "bridge"]) == {"bridge"} - flash_panda.assert_called_once_with(panda) + flash_panda.assert_not_called() def test_non_rivian_matching_bridge_is_reserved_without_flashing(): @@ -78,3 +80,18 @@ def test_non_rivian_external_black_panda_is_not_misidentified(): assert flasher.prepare_rivian_bridge(["internal", "external"]) == set() flash_panda.assert_not_called() + + +def test_bootstub_external_black_panda_is_not_misidentified(): + panda = _external_black_panda(signature=b"expected", bootstub=True) + with patch.object(flasher, "_is_rivian", return_value=True), \ + patch.object(flasher.os.path, "isfile", return_value=True), \ + patch.object(flasher, "Panda", wraps=flasher.Panda) as panda_class, \ + patch.object(flasher, "_flash_panda") as flash_panda: + panda_class.return_value = panda + panda_class.HW_TYPE_BLACK = b"\x03" + panda_class.usb_list.return_value = ["bridge"] + panda_class.get_signature_from_firmware.return_value = b"expected" + + assert flasher.prepare_rivian_bridge(["bridge"]) == {"bridge"} + flash_panda.assert_not_called() diff --git a/starpilot/common/starpilot_utilities.py b/starpilot/common/starpilot_utilities.py index f8b1e2bf6..b207a347f 100644 --- a/starpilot/common/starpilot_utilities.py +++ b/starpilot/common/starpilot_utilities.py @@ -210,9 +210,16 @@ def flash_panda(params_memory): for serial in Panda.list(): try: with Panda(serial=serial) as panda: - if serial in usb_serials and is_rivian_bridge_panda(panda, rivian): - print(f"Skipping Rivian harness bridge {serial}") - continue + if serial in usb_serials and not panda.is_internal() and panda.get_type() == Panda.HW_TYPE_BLACK: + if is_rivian_bridge_panda(panda, rivian): + print(f"Skipping Rivian harness bridge {serial}") + continue + if rivian: + # Never let the generic updater overwrite an unverified external + # Black Panda while a Rivian configuration is active. Provision a + # bridge explicitly before allowing it into the normal updater. + print(f"Skipping unverified external Black Panda on Rivian {serial}") + continue print(f"Flashing Panda {serial}") flash_fn = None app_fn = panda.get_mcu_type().config.app_fn