diff --git a/Runner/suites/Kernel/Baseport/RTC_Validation/README.md b/Runner/suites/Kernel/Baseport/RTC_Validation/README.md new file mode 100644 index 00000000..e6ab173d --- /dev/null +++ b/Runner/suites/Kernel/Baseport/RTC_Validation/README.md @@ -0,0 +1,119 @@ +# RTC validation + +`RTC_Validation` discovers Linux RTC class devices dynamically and validates +the selected RTC through the public Linux RTC ioctl interface. It does not use +SoC names, fixed bus addresses, or board-specific RTC mappings. + +The existing `Timer` suite validates POSIX timers, while Ethernet +suspend/resume uses `rtcwake` only as a wake source. Neither provides focused +RTC device, time-progression, wake-alarm, or time-write validation, so this is +a separate suite. + +## Default validation + +The default run is read-only. It: + +- records every `/sys/class/rtc/rtc*` device and matching `/dev/rtc*` node; +- selects the unique RTC marked `hctosys`, or the sole usable RTC; +- reads RTC time twice through `RTC_RD_TIME` and verifies progression; and +- captures RTC-related kernel health evidence with the shared dmesg helper. + +If multiple usable RTCs exist without a unique `hctosys` device, select one +explicitly. If RTC hardware, a required image-provided utility, or an optional +capability is unavailable, the relevant validation skips cleanly. + +```sh +cd Runner/suites/Kernel/Baseport/RTC_Validation +./run.sh +./run.sh --device /dev/rtc0 +./run.sh --read-delay 3 +``` + +## Wake-alarm validation + +Wake-alarm validation is opt-in because it temporarily changes RTC alarm +state. It uses `RTC_WKALM_SET` and `RTC_WKALM_RD`, verifies the programmed +alarm, waits for an RTC alarm interrupt with a finite timeout, and restores the +original disabled alarm state. An already enabled or pending alarm is not +replaced. + +```sh +./run.sh --alarm-test +./run.sh --device /dev/rtc1 --alarm-test --alarm-delay 10 --alarm-timeout 5 +``` + +Unsupported alarm ioctls, an active pre-existing alarm, or insufficient target +permission produce `SKIP`. A programmed alarm that does not expire or report an +alarm event within the requested window produces `FAIL`. + +## RTC time-set validation + +`RTC_SET_TIME` validation changes persistent RTC time briefly and therefore +requires both an explicit test request and write authorization. The helper +snapshots the original RTC time, writes a small future offset, verifies +readback and progression, and restores the original timeline. It also attempts +restoration when interrupted. + +```sh +./run.sh --time-set-test --allow-write +./run.sh --device /dev/rtc0 --time-set-test --allow-write \ + --time-set-offset 5 --read-delay 2 +``` + +Do not enable this mode when another service is actively synchronizing or +managing the same hardware clock. Unsupported `RTC_SET_TIME` or missing +privilege produces `SKIP`. A write, readback, progression, or restoration +failure produces `FAIL`. + +For `rtc-pm8xxx`, an `RTC_SET_TIME` result of `ENODEV` means target support is +not provisioned. The SKIP evidence reports whether the runtime DT provides +`allow-set-time`, an NVMEM cell named `offset`, or `qcom,uefi-rtc-info` for +persistent offset storage. The target needs direct time-setting permission or +one of those persistent offset mechanisms. This is an image or DT capability +gap and cannot be repaired by the test suite. + +## Options and environment variables + +| Command-line option | Environment variable | Default | Purpose | +|---|---|---:|---| +| `--device PATH` | `RTC_DEVICE` | `auto` | Select an RTC character device | +| `--read-delay SECONDS` | `RTC_READ_DELAY` | `2` | Delay between time reads | +| `--alarm-test` | `RTC_ALARM_ENABLE=1` | `0` | Enable wake-alarm validation | +| `--alarm-delay SECONDS` | `RTC_ALARM_DELAY` | `5` | Program the alarm this far ahead | +| `--alarm-timeout SECONDS` | `RTC_ALARM_TIMEOUT` | `3` | Allowed margin after the alarm time | +| `--time-set-test` | `RTC_TIME_SET_ENABLE=1` | `0` | Enable RTC time-set validation | +| `--time-set-offset SECONDS` | `RTC_TIME_SET_OFFSET` | `3` | Temporary future time offset | +| `--allow-write` | `RTC_ALLOW_WRITE=1` | `0` | Authorize the time write and restore | + +CLI values override environment variables. The LAVA YAML enables alarm and +time-set validation with restoration so Yocto CI exercises the complete RTC +flow. An existing active alarm is never replaced, and unsupported time-setting +capability is reported as `SKIP` with target prerequisite evidence. + +Environment examples: + +```sh +RTC_DEVICE=/dev/rtc1 RTC_ALARM_ENABLE=1 ./run.sh +RTC_TIME_SET_ENABLE=1 RTC_ALLOW_WRITE=1 ./run.sh +``` + +## Results and artifacts + +- `PASS`: an applicable operation completed and met its checks. +- `FAIL`: an operation started on an applicable RTC but failed validation, or + target state could not be restored. +- `SKIP`: the RTC, optional ioctl capability, privilege, or requested execution + environment is unavailable. + +Evidence is retained under `results/RTC_Validation/run-*/`, including: + +- `rtc_devices.tsv` for runtime device selection evidence; +- `rtc_read.tsv`, `rtc_alarm.tsv`, and `rtc_time_set.tsv` when applicable; +- helper stdout logs; and +- `kernel/dmesg_snapshot.log`, `kernel/dmesg_errors.log`, and + `kernel/dmesg_access.log`. + +The ioctl ABI follows the public Linux UAPI in +[`include/uapi/linux/rtc.h`](https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtc.h). +The upstream Linux RTC selftest is useful reference coverage: +[`tools/testing/selftests/rtc/rtctest.c`](https://github.com/torvalds/linux/blob/master/tools/testing/selftests/rtc/rtctest.c). diff --git a/Runner/suites/Kernel/Baseport/RTC_Validation/RTC_Validation.yaml b/Runner/suites/Kernel/Baseport/RTC_Validation/RTC_Validation.yaml new file mode 100644 index 00000000..a34ddefc --- /dev/null +++ b/Runner/suites/Kernel/Baseport/RTC_Validation/RTC_Validation.yaml @@ -0,0 +1,25 @@ +metadata: + name: RTC_Validation + format: "Lava-Test Test Definition 1.0" + description: "Dynamically validate Linux RTC time progression, wake-alarm expiration, and guarded time-setting support" + os: + - linux + scope: + - functional + +params: + RTC_DEVICE: "auto" + RTC_READ_DELAY: "2" + +run: + steps: + - REPO_PATH=$PWD + - cd Runner/suites/Kernel/Baseport/RTC_Validation + - >- + ./run.sh + --device "${RTC_DEVICE:-auto}" + --read-delay "${RTC_READ_DELAY:-2}" + --alarm-test + --time-set-test + --allow-write || true + - $REPO_PATH/Runner/utils/send-to-lava.sh RTC_Validation.res diff --git a/Runner/suites/Kernel/Baseport/RTC_Validation/run.sh b/Runner/suites/Kernel/Baseport/RTC_Validation/run.sh new file mode 100755 index 00000000..3039b7a4 --- /dev/null +++ b/Runner/suites/Kernel/Baseport/RTC_Validation/run.sh @@ -0,0 +1,470 @@ +#!/bin/sh + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# ---------- Repo env + helpers ---------- +SCRIPT_DIR="$( + cd "$(dirname "$0")" || exit 1 + pwd +)" +INIT_ENV="" +SEARCH="$SCRIPT_DIR" + +while [ "$SEARCH" != "/" ]; do + if [ -f "$SEARCH/init_env" ]; then + INIT_ENV="$SEARCH/init_env" + break + fi + SEARCH=$(dirname "$SEARCH") +done + +if [ -z "$INIT_ENV" ]; then + echo "[ERROR] Could not find init_env (starting at $SCRIPT_DIR)" >&2 + exit 1 +fi + +# Only source once (idempotent) +# NOTE: We intentionally **do not export** any new vars. They stay local to this shell. +if [ -z "${__INIT_ENV_LOADED:-}" ]; then + # shellcheck disable=SC1090 + . "$INIT_ENV" + __INIT_ENV_LOADED=1 +fi + +# shellcheck disable=SC1090 +. "$INIT_ENV" +# shellcheck disable=SC1091 +. "$TOOLS/functestlib.sh" + +TESTNAME="RTC_Validation" +RES_FILE="$SCRIPT_DIR/$TESTNAME.res" + +RTC_DEVICE="${RTC_DEVICE:-auto}" +RTC_READ_DELAY="${RTC_READ_DELAY:-2}" +RTC_ALARM_ENABLE="${RTC_ALARM_ENABLE:-0}" +RTC_ALARM_DELAY="${RTC_ALARM_DELAY:-5}" +RTC_ALARM_TIMEOUT="${RTC_ALARM_TIMEOUT:-3}" +RTC_TIME_SET_ENABLE="${RTC_TIME_SET_ENABLE:-0}" +RTC_TIME_SET_OFFSET="${RTC_TIME_SET_OFFSET:-3}" +RTC_ALLOW_WRITE="${RTC_ALLOW_WRITE:-0}" + +RESULT_DIR="$SCRIPT_DIR/results/$TESTNAME/run-$(date '+%Y%m%d-%H%M%S')-$$" +RTC_RUNNER="$TOOLS/rtc_validation_runner.py" +SELECTED_RTC="" +SELECTED_RTC_SOURCE="" +RTC_CORE_STATUS="PASS" + +# usage +# Prints the public command-line interface and examples. Produces no target +# state changes and returns success. +usage() { + printf '%s\n' \ + "Usage: ./run.sh [options]" \ + " --device PATH RTC character device, default: auto" \ + " --read-delay SECONDS Delay between RTC reads, default: 2" \ + " --alarm-test Opt in to bounded wake-alarm validation" \ + " --alarm-delay SECONDS Future alarm offset, default: 5" \ + " --alarm-timeout SECONDS Allowed expiration margin, default: 3" \ + " --time-set-test Opt in to RTC_SET_TIME validation" \ + " --time-set-offset SECONDS Temporary RTC offset, default: 3" \ + " --allow-write Authorize RTC_SET_TIME and restoration" \ + " -h, --help Show this help" \ + "" \ + "Examples:" \ + " ./run.sh" \ + " ./run.sh --device /dev/rtc0 --alarm-test" \ + " ./run.sh --time-set-test --allow-write" +} + +# parse_args +# Applies CLI overrides without probing or mutating the target. Returns 0 on +# success and 2 for missing values or unknown arguments. +parse_args() { + while [ "$#" -gt 0 ]; do + case "$1" in + --device) + [ "$#" -ge 2 ] || return 2 + RTC_DEVICE="$2" + shift 2 + ;; + --read-delay) + [ "$#" -ge 2 ] || return 2 + RTC_READ_DELAY="$2" + shift 2 + ;; + --alarm-test) + RTC_ALARM_ENABLE=1 + shift + ;; + --alarm-delay) + [ "$#" -ge 2 ] || return 2 + RTC_ALARM_DELAY="$2" + shift 2 + ;; + --alarm-timeout) + [ "$#" -ge 2 ] || return 2 + RTC_ALARM_TIMEOUT="$2" + shift 2 + ;; + --time-set-test) + RTC_TIME_SET_ENABLE=1 + shift + ;; + --time-set-offset) + [ "$#" -ge 2 ] || return 2 + RTC_TIME_SET_OFFSET="$2" + shift 2 + ;; + --allow-write) + RTC_ALLOW_WRITE=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + log_error "Unknown argument: $1" + return 2 + ;; + esac + done +} + +# rtc_read_value [fallback] +# Prints one whitespace-trimmed sysfs value. Diagnostics are intentionally not +# written to stdout because callers use command substitution. +rtc_read_value() { + rtc_value_path="$1" + rtc_value_fallback="${2:-unavailable}" + + if [ -r "$rtc_value_path" ]; then + tr -d '\r\n' <"$rtc_value_path" 2>/dev/null || \ + printf '%s\n' "$rtc_value_fallback" + else + printf '%s\n' "$rtc_value_fallback" + fi +} + +# rtc_capture_inventory +# Records every Linux RTC class device and its runtime relationships. Sets +# selection counters used by rtc_select_device and returns success. +rtc_capture_inventory() { + rtc_inventory_file="$1" + RTC_USABLE_COUNT=0 + RTC_HCTOSYS_COUNT=0 + RTC_ONLY_USABLE="" + RTC_ONLY_HCTOSYS="" + + printf 'rtc\tdevnode\tcharacter\treadable\thctosys\tname\tdriver\twakeup\tirq\tsince_epoch\n' \ + >"$rtc_inventory_file" + + for rtc_class_dir in /sys/class/rtc/rtc*; do + [ -d "$rtc_class_dir" ] || continue + rtc_class_name=$(basename "$rtc_class_dir") + rtc_devnode="/dev/$rtc_class_name" + rtc_character=0 + rtc_readable=0 + rtc_hctosys=$(rtc_read_value "$rtc_class_dir/hctosys" 0) + rtc_name=$(rtc_read_value "$rtc_class_dir/name") + rtc_since_epoch=$(rtc_read_value "$rtc_class_dir/since_epoch") + rtc_device_dir=$(readlink -f "$rtc_class_dir/device" 2>/dev/null || true) + rtc_driver="unbound" + rtc_wakeup="unavailable" + rtc_irq="unavailable" + + [ -c "$rtc_devnode" ] && rtc_character=1 + [ -r "$rtc_devnode" ] && rtc_readable=1 + + if [ -n "$rtc_device_dir" ]; then + rtc_driver_path=$(readlink -f "$rtc_device_dir/driver" 2>/dev/null || true) + if [ -n "$rtc_driver_path" ]; then + rtc_driver=$(basename "$rtc_driver_path") + fi + rtc_wakeup=$(rtc_read_value "$rtc_device_dir/power/wakeup") + rtc_irq=$(rtc_read_value "$rtc_device_dir/irq") + fi + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$rtc_class_name" \ + "$rtc_devnode" \ + "$rtc_character" \ + "$rtc_readable" \ + "$rtc_hctosys" \ + "$rtc_name" \ + "$rtc_driver" \ + "$rtc_wakeup" \ + "$rtc_irq" \ + "$rtc_since_epoch" \ + >>"$rtc_inventory_file" + + log_info "[RTC-DEVICE] rtc=$rtc_class_name devnode=$rtc_devnode character=$rtc_character readable=$rtc_readable hctosys=$rtc_hctosys name=$rtc_name driver=$rtc_driver wakeup=$rtc_wakeup irq=$rtc_irq since_epoch=$rtc_since_epoch" + + if [ "$rtc_character" -eq 1 ] && [ "$rtc_readable" -eq 1 ]; then + RTC_USABLE_COUNT=$((RTC_USABLE_COUNT + 1)) + RTC_ONLY_USABLE="$rtc_devnode" + if [ "$rtc_hctosys" = "1" ]; then + RTC_HCTOSYS_COUNT=$((RTC_HCTOSYS_COUNT + 1)) + RTC_ONLY_HCTOSYS="$rtc_devnode" + fi + fi + done +} + +# rtc_select_device +# Selects an explicit RTC, the unique hctosys RTC, or the sole usable RTC. +# Returns 0 on selection, 1 for an invalid explicit path, and 2 when automatic +# selection is not applicable or is ambiguous. +rtc_select_device() { + if [ "$RTC_DEVICE" != "auto" ]; then + rtc_explicit_real=$(readlink -f "$RTC_DEVICE" 2>/dev/null || true) + case "$rtc_explicit_real" in + /dev/rtc*) + ;; + *) + return 1 + ;; + esac + if [ ! -c "$rtc_explicit_real" ] || [ ! -r "$rtc_explicit_real" ]; then + return 2 + fi + SELECTED_RTC="$rtc_explicit_real" + SELECTED_RTC_SOURCE="explicit" + return 0 + fi + + if [ "$RTC_HCTOSYS_COUNT" -eq 1 ]; then + SELECTED_RTC="$RTC_ONLY_HCTOSYS" + SELECTED_RTC_SOURCE="unique-hctosys" + return 0 + fi + + if [ "$RTC_USABLE_COUNT" -eq 1 ]; then + SELECTED_RTC="$RTC_ONLY_USABLE" + SELECTED_RTC_SOURCE="sole-usable-device" + return 0 + fi + + return 2 +} + +# rtc_report_value +# Prints the first value for a report key and no diagnostics. +rtc_report_value() { + rtc_report_file="$1" + rtc_report_key="$2" + awk -F '\t' -v wanted="$rtc_report_key" \ + '$1 == wanted { print substr($0, index($0, "\t") + 1); exit }' \ + "$rtc_report_file" 2>/dev/null +} + +# rtc_record_helper_result +# Replays helper evidence and records the mode-specific PASS, FAIL, or SKIP. +rtc_record_helper_result() { + rtc_result_mode="$1" + rtc_result_rc="$2" + rtc_result_report="$3" + rtc_result_reason=$(rtc_report_value "$rtc_result_report" reason) + rtc_result_reason="${rtc_result_reason:-helper-did-not-produce-a-reason}" + rtc_result_message=$(rtc_report_value "$rtc_result_report" message) + + log_file_with_label "RTC-REPORT" "$rtc_result_report" 40 + case "$rtc_result_rc" in + 0) + test_result_record "PASS" "RTC $rtc_result_mode validation passed, reason=$rtc_result_reason report=$rtc_result_report" + ;; + 2) + if [ -n "$rtc_result_message" ]; then + test_result_record "SKIP" "$rtc_result_message, reason=$rtc_result_reason report=$rtc_result_report" + else + test_result_record "SKIP" "RTC $rtc_result_mode validation is unavailable, reason=$rtc_result_reason report=$rtc_result_report" + fi + ;; + *) + test_result_record "FAIL" "RTC $rtc_result_mode validation failed, rc=$rtc_result_rc reason=$rtc_result_reason report=$rtc_result_report" + ;; + esac +} + +parse_args "$@" || { + usage >&2 + exit 2 +} + +for rtc_numeric_value in \ + "$RTC_READ_DELAY" \ + "$RTC_ALARM_DELAY" \ + "$RTC_ALARM_TIMEOUT" \ + "$RTC_TIME_SET_OFFSET"; do + case "$rtc_numeric_value" in + ''|*[!0-9]*|0) + log_error "RTC delay, timeout, and offset values must be positive integers" + exit 2 + ;; + esac +done + +for rtc_boolean_value in \ + "$RTC_ALARM_ENABLE" \ + "$RTC_TIME_SET_ENABLE" \ + "$RTC_ALLOW_WRITE"; do + case "$rtc_boolean_value" in + 0|1) + ;; + *) + log_error "RTC enable and write policy values must be 0 or 1" + exit 2 + ;; + esac +done + +if [ "$RTC_TIME_SET_ENABLE" = "1" ] && [ "$RTC_ALLOW_WRITE" != "1" ]; then + log_error "RTC time-set validation requires --allow-write or RTC_ALLOW_WRITE=1" + exit 2 +fi + +test_result_init "$TESTNAME" "$RES_FILE" || exit 1 +if ! mkdir -p "$RESULT_DIR"; then + test_result_finish "FAIL" "$TESTNAME FAIL: cannot create retained evidence directory $RESULT_DIR" +fi + +log_info "--------------------------------------------------------------------------" +log_info "Starting $TESTNAME" +log_info "Evidence directory: $RESULT_DIR" +log_info "RTC validation: dynamically selecting a Linux RTC and validating direct ioctl behavior" +log_info "Configuration: device=$RTC_DEVICE read_delay=${RTC_READ_DELAY}s alarm_enable=$RTC_ALARM_ENABLE alarm_delay=${RTC_ALARM_DELAY}s alarm_timeout=${RTC_ALARM_TIMEOUT}s time_set_enable=$RTC_TIME_SET_ENABLE time_set_offset=${RTC_TIME_SET_OFFSET}s allow_write=$RTC_ALLOW_WRITE" + +if ! CHECK_DEPS_NO_EXIT=1 check_dependencies awk basename date mkdir python3 readlink tr; then + test_result_finish "SKIP" "$TESTNAME SKIP: required image-provided base utilities are unavailable" +fi + +if [ ! -r "$RTC_RUNNER" ]; then + test_result_finish "FAIL" "$TESTNAME FAIL: in-repository RTC helper is missing: $RTC_RUNNER" +fi + +if [ ! -d /sys/class/rtc ]; then + test_result_finish "SKIP" "$TESTNAME SKIP: Linux RTC class is not exposed" +fi + +rtc_capture_inventory "$RESULT_DIR/rtc_devices.tsv" +log_info "[RTC-DISCOVERY] usable=$RTC_USABLE_COUNT hctosys=$RTC_HCTOSYS_COUNT artifact=$RESULT_DIR/rtc_devices.tsv" + +rtc_select_device +rtc_select_rc=$? +case "$rtc_select_rc" in + 0) + log_info "[RTC-SELECTION] device=$SELECTED_RTC source=$SELECTED_RTC_SOURCE" + ;; + 1) + test_result_finish "FAIL" "$TESTNAME FAIL: explicit RTC path must resolve to a /dev/rtc* character device" + ;; + *) + if [ "$RTC_DEVICE" != "auto" ]; then + test_result_finish "SKIP" "$TESTNAME SKIP: requested RTC device is unavailable or unreadable: $RTC_DEVICE" + elif [ "$RTC_USABLE_COUNT" -eq 0 ]; then + test_result_finish "SKIP" "$TESTNAME SKIP: no readable RTC character device was discovered" + else + test_result_finish "SKIP" "$TESTNAME SKIP: RTC selection is ambiguous, choose one of $RTC_USABLE_COUNT devices with --device" + fi + ;; +esac + +rtc_read_report="$RESULT_DIR/rtc_read.tsv" +rtc_read_log="$RESULT_DIR/rtc_read.log" +rtc_read_timeout=$((RTC_READ_DELAY + 10)) +if run_with_timeout_log \ + "$rtc_read_timeout" \ + "$rtc_read_log" \ + python3 \ + "$RTC_RUNNER" \ + --mode read \ + --device "$SELECTED_RTC" \ + --read-delay "$RTC_READ_DELAY" \ + --report "$rtc_read_report"; then + rtc_read_rc=0 +else + rtc_read_rc=$? +fi +log_file_with_label "RTC-READ" "$rtc_read_log" 20 +rtc_record_helper_result "time-read and progression" "$rtc_read_rc" "$rtc_read_report" +case "$rtc_read_rc" in + 0) + RTC_CORE_STATUS="PASS" + ;; + 2) + RTC_CORE_STATUS="SKIP" + ;; + *) + RTC_CORE_STATUS="FAIL" + ;; +esac + +if [ "$RTC_ALARM_ENABLE" = "1" ]; then + rtc_alarm_report="$RESULT_DIR/rtc_alarm.tsv" + rtc_alarm_log="$RESULT_DIR/rtc_alarm.log" + rtc_alarm_run_timeout=$((RTC_ALARM_DELAY + RTC_ALARM_TIMEOUT + 10)) + if run_with_timeout_log \ + "$rtc_alarm_run_timeout" \ + "$rtc_alarm_log" \ + python3 \ + "$RTC_RUNNER" \ + --mode alarm \ + --device "$SELECTED_RTC" \ + --alarm-delay "$RTC_ALARM_DELAY" \ + --alarm-timeout "$RTC_ALARM_TIMEOUT" \ + --report "$rtc_alarm_report"; then + rtc_alarm_rc=0 + else + rtc_alarm_rc=$? + fi + log_file_with_label "RTC-ALARM" "$rtc_alarm_log" 20 + rtc_record_helper_result "wake-alarm" "$rtc_alarm_rc" "$rtc_alarm_report" +else + test_result_record "SKIP" "RTC wake-alarm validation is disabled by default, use --alarm-test to opt in" +fi + +if [ "$RTC_TIME_SET_ENABLE" = "1" ]; then + rtc_time_set_report="$RESULT_DIR/rtc_time_set.tsv" + rtc_time_set_log="$RESULT_DIR/rtc_time_set.log" + rtc_time_set_timeout=$((RTC_READ_DELAY + 10)) + if run_with_timeout_log \ + "$rtc_time_set_timeout" \ + "$rtc_time_set_log" \ + python3 \ + "$RTC_RUNNER" \ + --mode set-time \ + --device "$SELECTED_RTC" \ + --read-delay "$RTC_READ_DELAY" \ + --time-set-offset "$RTC_TIME_SET_OFFSET" \ + --report "$rtc_time_set_report"; then + rtc_time_set_rc=0 + else + rtc_time_set_rc=$? + fi + log_file_with_label "RTC-TIME-SET" "$rtc_time_set_log" 20 + rtc_record_helper_result "time-set and restoration" "$rtc_time_set_rc" "$rtc_time_set_report" +else + test_result_record "SKIP" "RTC time-set validation is disabled by default, use --time-set-test --allow-write to opt in" +fi + +export KERNEL_LOG_JOURNAL_FALLBACK=1 +scan_dmesg_errors \ + "$RESULT_DIR/kernel" \ + 'rtc[-_0-9 ]|qpnp_rtc|pmic.*rtc|qcom.*rtc' \ + 'registered as rtc[0-9]+|setting system clock|system clock set' +rtc_dmesg_rc=$? +if [ "${DMESG_ACCESS_STATUS:-unavailable}" != "available" ]; then + test_result_record "SKIP" "Kernel log access is unavailable for RTC health validation, status=${DMESG_ACCESS_STATUS:-unknown} provider=${DMESG_ACCESS_PROVIDER:-none} rc=${DMESG_ACCESS_RC:-unknown} artifact=$RESULT_DIR/kernel/dmesg_access.log" +elif [ "$rtc_dmesg_rc" -eq 0 ]; then + log_file_with_label "RTC-KERNEL-ERROR" "$RESULT_DIR/kernel/dmesg_errors.log" 25 + test_result_record "FAIL" "RTC-related kernel errors were detected, artifact=$RESULT_DIR/kernel/dmesg_errors.log" +else + test_result_record "PASS" "No non-benign RTC-related kernel errors were found" +fi + +if [ "$RTC_CORE_STATUS" = "SKIP" ] && [ "$TEST_RESULT_FAIL_COUNT" -eq 0 ]; then + test_result_finish "SKIP" "$TESTNAME SKIP: direct RTC time validation was unavailable" +fi + +test_result_finish diff --git a/Runner/utils/rtc_validation_runner.py b/Runner/utils/rtc_validation_runner.py new file mode 100755 index 00000000..592604ba --- /dev/null +++ b/Runner/utils/rtc_validation_runner.py @@ -0,0 +1,538 @@ +#!/usr/bin/env python3 +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause +"""Bounded Linux RTC ioctl validation with state restoration.""" + +import argparse +import calendar +import ctypes +import datetime +import errno +import fcntl +import os +import select +import signal +import sys +import time + + +PASS = 0 +FAIL = 1 +SKIP = 2 +ERROR = 3 +RTC_AF = 0x20 + + +class RtcTime(ctypes.Structure): + _fields_ = [ + ("tm_sec", ctypes.c_int), + ("tm_min", ctypes.c_int), + ("tm_hour", ctypes.c_int), + ("tm_mday", ctypes.c_int), + ("tm_mon", ctypes.c_int), + ("tm_year", ctypes.c_int), + ("tm_wday", ctypes.c_int), + ("tm_yday", ctypes.c_int), + ("tm_isdst", ctypes.c_int), + ] + + +class RtcWakeAlarm(ctypes.Structure): + _fields_ = [ + ("enabled", ctypes.c_ubyte), + ("pending", ctypes.c_ubyte), + ("time", RtcTime), + ] + + +def _ioc(direction, ioctl_type, number, size): + return ( + (direction << 30) + | (ioctl_type << 8) + | number + | (size << 16) + ) + + +RTC_RD_TIME = _ioc(2, ord("p"), 0x09, ctypes.sizeof(RtcTime)) +RTC_SET_TIME = _ioc(1, ord("p"), 0x0A, ctypes.sizeof(RtcTime)) +RTC_WKALM_SET = _ioc(1, ord("p"), 0x0F, ctypes.sizeof(RtcWakeAlarm)) +RTC_WKALM_RD = _ioc(2, ord("p"), 0x10, ctypes.sizeof(RtcWakeAlarm)) + +UNAVAILABLE_ERRNOS = { + errno.EACCES, + errno.EBUSY, + errno.EINVAL, + errno.ENODEV, + errno.ENOENT, + errno.ENOSYS, + errno.ENOTTY, + errno.EOPNOTSUPP, + errno.EPERM, +} + + +def _interrupted(signum, frame): + del frame + raise InterruptedError("interrupted by signal {}".format(signum)) + + +def ioctl_read(fd, request, structure_type): + buffer = bytearray(ctypes.sizeof(structure_type)) + fcntl.ioctl(fd, request, buffer, True) + return structure_type.from_buffer_copy(buffer) + + +def ioctl_write(fd, request, value): + buffer = bytearray(bytes(value)) + fcntl.ioctl(fd, request, buffer, True) + + +def rtc_to_epoch(value): + rtc_datetime = datetime.datetime( + value.tm_year + 1900, + value.tm_mon + 1, + value.tm_mday, + value.tm_hour, + value.tm_min, + value.tm_sec, + tzinfo=datetime.timezone.utc, + ) + return calendar.timegm(rtc_datetime.utctimetuple()) + + +def epoch_to_rtc(epoch): + rtc_datetime = datetime.datetime.fromtimestamp(epoch, datetime.timezone.utc) + value = RtcTime() + value.tm_sec = rtc_datetime.second + value.tm_min = rtc_datetime.minute + value.tm_hour = rtc_datetime.hour + value.tm_mday = rtc_datetime.day + value.tm_mon = rtc_datetime.month - 1 + value.tm_year = rtc_datetime.year - 1900 + value.tm_wday = (rtc_datetime.weekday() + 1) % 7 + value.tm_yday = rtc_datetime.timetuple().tm_yday - 1 + value.tm_isdst = 0 + return value + + +def format_rtc(value): + return "{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}Z".format( + value.tm_year + 1900, + value.tm_mon + 1, + value.tm_mday, + value.tm_hour, + value.tm_min, + value.tm_sec, + ) + + +def write_report(path, values): + with open(path, "w", encoding="utf-8") as report: + report.write("key\tvalue\n") + for key, value in values: + clean_value = str(value).replace("\t", " ").replace("\n", " ") + report.write("{}\t{}\n".format(key, clean_value)) + + +def read_sysfs_text(path): + try: + with open(path, "rb") as value_file: + value = value_file.read() + except OSError: + return "unavailable" + return value.replace(b"\x00", b" ").decode("utf-8", "replace").strip() + + +def rtc_runtime_evidence(device): + rtc_name = os.path.basename(os.path.realpath(device)) + class_dir = os.path.join("/sys/class/rtc", rtc_name) + device_dir = os.path.realpath(os.path.join(class_dir, "device")) + driver_path = os.path.realpath(os.path.join(device_dir, "driver")) + driver = os.path.basename(driver_path) if os.path.isdir(driver_path) else "unknown" + of_node = os.path.realpath(os.path.join(device_dir, "of_node")) + dt_available = os.path.isdir(of_node) + + def property_status(name): + if not dt_available: + return "unknown" + return "present" if os.path.exists(os.path.join(of_node, name)) else "absent" + + nvmem_names = read_sysfs_text(os.path.join(of_node, "nvmem-cell-names")) + nvmem_offset = "unknown" + if dt_available: + if property_status("nvmem-cells") == "present" and "offset" in nvmem_names.split(): + nvmem_offset = "present" + else: + nvmem_offset = "absent" + + return { + "rtc_name": read_sysfs_text(os.path.join(class_dir, "name")), + "driver": driver, + "dt_node": of_node if dt_available else "unavailable", + "dt_allow_set_time": property_status("allow-set-time"), + "dt_nvmem_offset": nvmem_offset, + "dt_uefi_rtc_info": property_status("qcom,uefi-rtc-info"), + } + + +def emit_result(status, mode, reason, device): + print( + "RTC_RESULT status={} mode={} reason={} device={}".format( + status, + mode, + reason, + device, + ) + ) + + +def read_validation(fd, args): + first = ioctl_read(fd, RTC_RD_TIME, RtcTime) + first_epoch = rtc_to_epoch(first) + time.sleep(args.read_delay) + second = ioctl_read(fd, RTC_RD_TIME, RtcTime) + second_epoch = rtc_to_epoch(second) + delta = second_epoch - first_epoch + values = [ + ("status", "PASS"), + ("reason", "rtc-time-progressed"), + ("device", args.device), + ("first_time", format_rtc(first)), + ("second_time", format_rtc(second)), + ("elapsed_rtc_seconds", delta), + ("sample_delay_seconds", args.read_delay), + ] + if delta < 1 or delta > args.read_delay + 3: + values[0] = ("status", "FAIL") + values[1] = ("reason", "rtc-time-did-not-progress-as-expected") + return FAIL, values + return PASS, values + + +def alarm_validation(fd, args): + try: + original = ioctl_read(fd, RTC_WKALM_RD, RtcWakeAlarm) + except OSError as error: + if error.errno in UNAVAILABLE_ERRNOS: + return SKIP, [ + ("status", "SKIP"), + ("reason", "wake-alarm-ioctl-unavailable"), + ("errno", error.errno), + ("error", error.strerror), + ] + raise + + values = [ + ("device", args.device), + ("original_alarm_enabled", int(original.enabled)), + ("original_alarm_pending", int(original.pending)), + ("original_alarm_time", format_rtc(original.time)), + ("alarm_delay_seconds", args.alarm_delay), + ("alarm_timeout_seconds", args.alarm_timeout), + ] + if original.enabled or original.pending: + return SKIP, [ + ("status", "SKIP"), + ("reason", "existing-alarm-is-active"), + ] + values + + current = ioctl_read(fd, RTC_RD_TIME, RtcTime) + target_epoch = rtc_to_epoch(current) + args.alarm_delay + programmed = RtcWakeAlarm() + programmed.enabled = 1 + programmed.pending = 0 + programmed.time = epoch_to_rtc(target_epoch) + alarm_changed = False + status = FAIL + reason = "alarm-validation-incomplete" + + try: + try: + ioctl_write(fd, RTC_WKALM_SET, programmed) + alarm_changed = True + except OSError as error: + if error.errno in UNAVAILABLE_ERRNOS: + values.extend( + [ + ("errno", error.errno), + ("error", error.strerror), + ] + ) + return SKIP, [ + ("status", "SKIP"), + ("reason", "wake-alarm-programming-unavailable"), + ] + values + raise + + readback = ioctl_read(fd, RTC_WKALM_RD, RtcWakeAlarm) + readback_epoch = rtc_to_epoch(readback.time) + values.extend( + [ + ("programmed_alarm_time", format_rtc(programmed.time)), + ("readback_alarm_time", format_rtc(readback.time)), + ("readback_alarm_enabled", int(readback.enabled)), + ] + ) + if not readback.enabled or abs(readback_epoch - target_epoch) > 1: + reason = "wake-alarm-readback-mismatch" + return status, [ + ("status", "FAIL"), + ("reason", reason), + ("restoration", "verified-on-successful-return"), + ] + values + + ready, _, _ = select.select( + [fd], + [], + [], + args.alarm_delay + args.alarm_timeout, + ) + if not ready: + reason = "wake-alarm-expiration-timeout" + return status, [ + ("status", "FAIL"), + ("reason", reason), + ("restoration", "verified-on-successful-return"), + ] + values + + event_data = os.read(fd, ctypes.sizeof(ctypes.c_ulong)) + event_value = int.from_bytes(event_data, byteorder=sys.byteorder) + event_flags = event_value & 0xFF + after = ioctl_read(fd, RTC_RD_TIME, RtcTime) + after_epoch = rtc_to_epoch(after) + post_alarm = ioctl_read(fd, RTC_WKALM_RD, RtcWakeAlarm) + values.extend( + [ + ("event_value", "0x{:x}".format(event_value)), + ("event_flags", "0x{:02x}".format(event_flags)), + ("alarm_flag_observed", int(bool(event_flags & RTC_AF))), + ("expiration_time", format_rtc(after)), + ("expiration_delta_seconds", after_epoch - target_epoch), + ("post_alarm_enabled", int(post_alarm.enabled)), + ("post_alarm_pending", int(post_alarm.pending)), + ] + ) + if not event_flags & RTC_AF: + reason = "rtc-event-did-not-contain-alarm-flag" + elif after_epoch < target_epoch: + reason = "alarm-event-arrived-before-programmed-time" + elif after_epoch > target_epoch + args.alarm_timeout: + reason = "alarm-event-arrived-after-timeout-window" + else: + status = PASS + reason = "wake-alarm-program-readback-and-expiration-verified" + return status, [ + ("status", "PASS" if status == PASS else "FAIL"), + ("reason", reason), + ("restoration", "verified-on-successful-return"), + ] + values + finally: + if alarm_changed: + restore_value = programmed + restore_value.enabled = 0 + restore_value.pending = 0 + try: + ioctl_write(fd, RTC_WKALM_SET, restore_value) + except OSError as error: + raise RuntimeError( + "failed to restore original wake-alarm state: {}".format(error) + ) from error + + +def set_time_validation(fd, args): + original = ioctl_read(fd, RTC_RD_TIME, RtcTime) + original_epoch = rtc_to_epoch(original) + started = time.monotonic() + test_epoch = original_epoch + args.time_set_offset + test_time = epoch_to_rtc(test_epoch) + time_changed = False + status = FAIL + reason = "rtc-time-set-validation-incomplete" + values = [ + ("device", args.device), + ("original_time", format_rtc(original)), + ("test_time", format_rtc(test_time)), + ("time_set_offset_seconds", args.time_set_offset), + ] + + try: + try: + ioctl_write(fd, RTC_SET_TIME, test_time) + time_changed = True + except OSError as error: + if error.errno in UNAVAILABLE_ERRNOS: + runtime = rtc_runtime_evidence(args.device) + reason = "rtc-time-setting-unavailable" + message = "RTC_SET_TIME is unavailable on the selected target" + if error.errno == errno.ENODEV and runtime["driver"] == "rtc-pm8xxx": + reason = "rtc-pm8xxx-time-setting-target-support-unavailable" + if ( + runtime["dt_allow_set_time"] == "absent" + and runtime["dt_nvmem_offset"] == "absent" + and runtime["dt_uefi_rtc_info"] == "absent" + ): + message = ( + "rtc-pm8xxx returned ENODEV because the target lacks " + "DT allow-set-time and persistent RTC offset storage " + "through NVMEM or UEFI" + ) + else: + message = ( + "rtc-pm8xxx returned ENODEV because target RTC " + "time-setting support is unavailable" + ) + values.extend( + [ + ("errno", error.errno), + ("error", error.strerror), + ("message", message), + ("driver", runtime["driver"]), + ("rtc_name", runtime["rtc_name"]), + ("dt_node", runtime["dt_node"]), + ("dt_allow_set_time", runtime["dt_allow_set_time"]), + ("dt_nvmem_offset", runtime["dt_nvmem_offset"]), + ("dt_uefi_rtc_info", runtime["dt_uefi_rtc_info"]), + ] + ) + return SKIP, [ + ("status", "SKIP"), + ("reason", reason), + ] + values + raise + + readback = ioctl_read(fd, RTC_RD_TIME, RtcTime) + readback_epoch = rtc_to_epoch(readback) + values.append(("set_readback_time", format_rtc(readback))) + if abs(readback_epoch - test_epoch) > 1: + reason = "rtc-time-set-readback-mismatch" + return status, [ + ("status", "FAIL"), + ("reason", reason), + ("restoration", "verified-on-successful-return"), + ] + values + + time.sleep(args.read_delay) + progressed = ioctl_read(fd, RTC_RD_TIME, RtcTime) + progressed_epoch = rtc_to_epoch(progressed) + values.extend( + [ + ("progressed_time", format_rtc(progressed)), + ("progressed_seconds", progressed_epoch - readback_epoch), + ] + ) + if progressed_epoch <= readback_epoch: + reason = "rtc-time-did-not-progress-after-set" + else: + status = PASS + reason = "rtc-time-set-readback-and-progression-verified" + return status, [ + ("status", "PASS" if status == PASS else "FAIL"), + ("reason", reason), + ("restoration", "verified-on-successful-return"), + ] + values + finally: + if time_changed: + elapsed = max(0, round(time.monotonic() - started)) + restore_time = epoch_to_rtc(original_epoch + elapsed) + try: + ioctl_write(fd, RTC_SET_TIME, restore_time) + restored = ioctl_read(fd, RTC_RD_TIME, RtcTime) + if abs(rtc_to_epoch(restored) - rtc_to_epoch(restore_time)) > 1: + raise RuntimeError("RTC time restoration readback mismatch") + except (OSError, RuntimeError, ValueError) as error: + raise RuntimeError( + "failed to restore original RTC timeline: {}".format(error) + ) from error + + +def self_check(args): + values = [ + ("status", "PASS"), + ("reason", "rtc-abi-layout-validated"), + ("rtc_time_size", ctypes.sizeof(RtcTime)), + ("rtc_wkalrm_size", ctypes.sizeof(RtcWakeAlarm)), + ("rtc_rd_time", "0x{:x}".format(RTC_RD_TIME)), + ("rtc_set_time", "0x{:x}".format(RTC_SET_TIME)), + ("rtc_wkalm_set", "0x{:x}".format(RTC_WKALM_SET)), + ("rtc_wkalm_rd", "0x{:x}".format(RTC_WKALM_RD)), + ] + if ctypes.sizeof(RtcTime) != 36 or ctypes.sizeof(RtcWakeAlarm) != 40: + values[0] = ("status", "FAIL") + values[1] = ("reason", "unexpected-rtc-abi-layout") + return FAIL, values + return PASS, values + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--device", default="/dev/rtc0") + parser.add_argument( + "--mode", + required=True, + choices=("read", "alarm", "set-time", "self-check"), + ) + parser.add_argument("--report", required=True) + parser.add_argument("--read-delay", type=int, default=2) + parser.add_argument("--alarm-delay", type=int, default=5) + parser.add_argument("--alarm-timeout", type=int, default=3) + parser.add_argument("--time-set-offset", type=int, default=3) + args = parser.parse_args() + for value in ( + args.read_delay, + args.alarm_delay, + args.alarm_timeout, + args.time_set_offset, + ): + if value <= 0: + parser.error("delay, timeout, and offset values must be positive") + return args + + +def main(): + args = parse_args() + signal.signal(signal.SIGINT, _interrupted) + signal.signal(signal.SIGTERM, _interrupted) + + if args.mode == "self-check": + status, values = self_check(args) + else: + try: + fd = os.open(args.device, os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)) + except OSError as error: + status = SKIP if error.errno in UNAVAILABLE_ERRNOS else FAIL + values = [ + ("status", "SKIP" if status == SKIP else "FAIL"), + ("reason", "rtc-device-open-unavailable"), + ("device", args.device), + ("errno", error.errno), + ("error", error.strerror), + ] + else: + try: + if args.mode == "read": + status, values = read_validation(fd, args) + elif args.mode == "alarm": + status, values = alarm_validation(fd, args) + else: + status, values = set_time_validation(fd, args) + except (OSError, RuntimeError, ValueError, OverflowError) as error: + status = FAIL + values = [ + ("status", "FAIL"), + ("reason", "rtc-validation-error"), + ("device", args.device), + ("error", str(error)), + ] + finally: + os.close(fd) + + write_report(args.report, values) + reason = next(value for key, value in values if key == "reason") + status_name = next(value for key, value in values if key == "status") + emit_result(status_name, args.mode, reason, args.device) + return status + + +if __name__ == "__main__": + sys.exit(main())