Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions .github/workflows/m33mu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,11 @@ jobs:
if grep -Eq '^(\[MEMFAULT\]|\[HARDFLT\]|HardFault|SecureFault)' "$log"; then
exit 1
fi
# Both guests raw-write the same UART, so guest1 can splice into the
# middle of a secure/guest0 marker line. Flatten (strip guest1 text,
# rejoin the split line) before matching those markers; match guest1's
# own markers against the raw log. Mirrors run_m33mu_scenario.sh's
# expect_flat.
flat=$(sed 's/freertos_guest1:.*$//' "$log" | tr -d '\r\n')
need() { printf '%s' "$flat" | grep -Fq "$1" || { echo "missing: $1" >&2; exit 1; }; }
# Rejoin guest0 lines interrupted by UART attach banners or complete
# guest1 records. Keep the raw log for guest1 and fault checks.
guest0_log="$RUNNER_TEMP/wolfboot-wolftrust-m33mu-guest0.log"
python3 tests/target/lib/m33mu_console.py "$log" > "$guest0_log"
need() { grep -Fq "$1" "$guest0_log" || { echo "missing: $1" >&2; exit 1; }; }
need_raw() { grep -Fq "$1" "$log" || { echo "missing: $1" >&2; exit 1; }; }
need "wolfTrust TEE client initialized"
need "wolfTrust FF-M mediated crypto dispatch verified"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
matrix: ${{ steps.s.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- name: Test M33MU console marker reconstruction
run: python3 tests/scripts/test_m33mu_console.py
- id: s
# Single source of truth: the Makefile's UNIT_SUITES. Adding a suite
# there makes it a new CI check automatically — no workflow edit.
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ include mk/common.mk
all: $(ARCH_DEFAULT_GOALS)

test:
@python3 tests/scripts/test_m33mu_console.py
@$(MAKE) --no-print-directory -C tests/host test

C99_CFLAGS := -std=c99 -pedantic-errors -Werror=vla \
Expand Down
164 changes: 164 additions & 0 deletions tests/scripts/test_m33mu_console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
# test_m33mu_console.py
#
# Copyright (C) 2026 wolfSSL Inc.
#
# This file is part of wolfTrust.
#
# wolfTrust is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# wolfTrust is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <https://www.gnu.org/licenses/>.

import importlib.util
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest


HELPER = Path(__file__).resolve().parents[1] / "target/lib/m33mu_console.py"
SPEC = importlib.util.spec_from_file_location("m33mu_console", HELPER)
CONSOLE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(CONSOLE)

PREFIX = (b"[00:00:00.063,000] \x1b[0m<inf> guest0_psa: "
b"wolfTrust FF-M oversized-vector call rejecte")
TAIL = b"d st=-135\x1b[0m\r\n"
MARKER = b"wolfTrust FF-M oversized-vector call rejected st=-135"
BANNER = b"[UART] 40004800 attached to stdio\n"
# PR #52, run 36894209966, job 110477452533. The tail arrived after guest1.
INTERJECTION = (
BANNER
+ b"freertos_guest1: alive\r\n"
+ b"freertos_guest1: ffm sha256 ok first=0x20\r\n"
+ b"freertos_guest1: ffm rng ok\r\n"
+ b"freertos_guest1: psa_crypto_init st=0\r\n"
+ b"freertos_guest1: psa rng ok\r\n"
+ b"freertos_guest1: psa hash ok\r\n"
+ b"freertos_guest1: ffm forged-handle rejected\r\n"
+ b"freertos_guest1: ffm oversized-vector rejected\r\n"
+ b"freertos_guest1: ffm cross-guest vector rejected\r\n"
+ b"freertos_guest1: ffm wrong-sid refused\r\n"
)


class ConsoleTests(unittest.TestCase):
def test_ci_split_rejoins_exactly_without_modifying_the_raw_log(self):
raw = PREFIX + INTERJECTION + TAIL
self.assertNotIn(MARKER, raw)
self.assertEqual(CONSOLE.guest0_console(raw), PREFIX + TAIL)
self.assertIn(b"freertos_guest1: ffm oversized-vector rejected", raw)

def test_missing_marker_bytes_still_fail(self):
for index in range(len(MARKER)):
with self.subTest(index=index):
damaged = MARKER[:index] + MARKER[index + 1:]
split = len(damaged) // 2
raw = damaged[:split] + INTERJECTION + damaged[split:] + b"\n"
self.assertNotIn(MARKER, CONSOLE.guest0_console(raw))
self.assertNotIn(MARKER, CONSOLE.guest0_console(PREFIX + INTERJECTION))

def test_unrelated_lines_do_not_form_a_marker(self):
for ending in (b"\n", b"\r\n"):
with self.subTest(ending=ending):
raw = PREFIX + ending + INTERJECTION + TAIL
self.assertNotIn(MARKER, CONSOLE.guest0_console(raw))

def test_complete_interjections_at_every_marker_position(self):
for index in range(len(MARKER) + 1):
for injection in (BANNER, INTERJECTION,
b"freertos_guest1: heartbeat 0\n"):
with self.subTest(index=index, injection=injection):
raw = MARKER[:index] + injection + MARKER[index:] + b"\n"
self.assertEqual(CONSOLE.guest0_console(raw), MARKER + b"\n")

def test_banner_inside_guest1_record(self):
raw = PREFIX + b"freertos_guest1: heart" + BANNER + b"beat 0\n" + TAIL
self.assertEqual(CONSOLE.guest0_console(raw), PREFIX + TAIL)

def test_conformance_totals_keep_their_line_boundaries(self):
raw = (b"TOTAL PASSED : 85\r\nTOTAL SK" + INTERJECTION
+ b"IPPED : 4\r\nTOTAL FAILED : 0\r\n")
expected = (b"TOTAL PASSED : 85\r\nTOTAL SKIPPED : 4\r\n"
b"TOTAL FAILED : 0\r\n")
self.assertEqual(CONSOLE.guest0_console(raw), expected)

def test_confboot_pipeline_reads_reconstructed_per_test_results(self):
target = HELPER.parent.parent
expected = b"".join(
line + b"\n" for line in
(target / "ffm_ipc_results.txt").read_bytes().splitlines()
if line and not line.startswith(b"#")
)
raw = b""
for line in expected.splitlines():
num, result = line.split()
raw += (b"Num=" + num[:1] + INTERJECTION + num[1:]
+ b" Result=" + result[:3] + BANNER + result[3:]
+ b"\r\n")
# Exercise the runner's actual extraction pipeline without booting.
runner = (target / "run_m33mu_scenario.sh").read_text()
pipeline = runner.split('\n conf_got=', 1)[1]
pipeline = 'conf_got=' + pipeline.split('\n if grep -v', 1)[0]
with tempfile.TemporaryDirectory() as directory:
repo = Path(directory)
(repo / "build").mkdir()
(repo / "raw.log").write_bytes(raw)
(repo / "guest0.log").write_bytes(CONSOLE.guest0_console(raw))
script = ('set -euo pipefail\nrepo=$1\nlog="$repo/raw.log"\n'
'guest0_log="$repo/guest0.log"\n' + pipeline)
completed = subprocess.run(
["bash", "-c", script, "confboot-test", str(repo)],
capture_output=True,
)
self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertEqual((repo / "build/ffm-ipc-results.txt").read_bytes(),
expected)

def test_pty_and_crlf_banner(self):
banner = b"[UART] 44002400 attached to /dev/pts/17\r\n"
self.assertEqual(CONSOLE.guest0_console(PREFIX + banner + TAIL), PREFIX + TAIL)

def test_incomplete_or_unrecognized_records_are_preserved(self):
for raw in (BANNER[:-1], b"freertos_guest1: alive",
b"[UART] 40004800 attached to stdio EXTRA\n",
b"[UART] 4000480 attached to stdio\n",
b"freertos_guest0: alive\n"):
with self.subTest(raw=raw):
self.assertEqual(CONSOLE.guest0_console(raw), raw)

def test_fault_and_unrelated_bytes_are_preserved(self):
raw = b"prefix\xff\r\n[MEMFAULT] addr=0x30028000\n[HARDFLT]\nHardFault\n"
self.assertEqual(CONSOLE.guest0_console(raw), raw)

def test_guest1_alone_cannot_supply_guest0_marker(self):
raw = b"freertos_guest1: " + MARKER + b"\n"
self.assertNotIn(MARKER, CONSOLE.guest0_console(raw))

def test_cli_keeps_raw_file_and_exits_nonzero_for_missing_input(self):
raw = PREFIX + INTERJECTION + TAIL
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "raw.log"
path.write_bytes(raw)
result = subprocess.run([sys.executable, str(HELPER), str(path)],
capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, PREFIX + TAIL)
self.assertEqual(path.read_bytes(), raw)
missing = subprocess.run([sys.executable, str(HELPER), str(path) + ".missing"],
capture_output=True)
self.assertNotEqual(missing.returncode, 0)


if __name__ == "__main__":
unittest.main()
55 changes: 55 additions & 0 deletions tests/target/lib/m33mu_console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
# m33mu_console.py
#
# Copyright (C) 2026 wolfSSL Inc.
#
# This file is part of wolfTrust.
#
# wolfTrust is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# wolfTrust is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <https://www.gnu.org/licenses/>.

"""Remove complete console interjections for guest0 marker checks.

Both H5 guests write USART3. Guest1 can disable/re-enable it and print while
guest0 has a line in progress. Remove the attach banner and tagged guest1
records with their line endings, retaining every other byte and line ending.
The original log remains the evidence for guest1 and fault checks. Arbitrary
interleaving, including an incomplete interjection, must still fail to match.
"""

import argparse
from pathlib import Path
import re
import sys


UART_ATTACH = re.compile(
rb"\[UART\] [0-9a-fA-F]{8} attached to (?:stdio|/dev/pts/[0-9]+)\r?\n"
)
GUEST1_RECORD = re.compile(rb"freertos_guest1:[^\r\n]*\r?\n")


def guest0_console(log: bytes) -> bytes:
# Remove banners first: a reopen can also split a guest1 record.
return GUEST1_RECORD.sub(b"", UART_ATTACH.sub(b"", log))


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("log", type=Path)
args = parser.parse_args()
sys.stdout.buffer.write(guest0_console(args.log.read_bytes()))


if __name__ == "__main__":
main()
54 changes: 26 additions & 28 deletions tests/target/run_m33mu_scenario.sh
Original file line number Diff line number Diff line change
Expand Up @@ -383,21 +383,20 @@ emu_status=${PIPESTATUS[0]}
set -e
echo "wolfBoot/wolfTrust M33MU exit status: $emu_status"

# Preserve raw evidence for guest1 and fault checks. Rejoin guest0 lines only
# across complete, recognized console interjections; retain normal newlines.
guest0_log="$repo/ci-m33mu-$scenario-guest0.log"
python3 "$repo/tests/target/lib/m33mu_console.py" "$log" > "$guest0_log"
Comment thread
aidangarske marked this conversation as resolved.

# Per-assertion reporting so make test-target surfaces what each scenario
# actually checks, not just a single PASS. The Makefile greps these tagged
# lines out of the log; the full boot log stays underneath.
check_pass() { printf ' [check] PASS %s\n' "$1"; }
check_fail() { printf ' [check] FAIL %s (%s)\n' "$1" "$2"; exit 1; }
expect() { if grep -Fq "$2" "$log"; then check_pass "$1"; \
else check_fail "$1" "missing: $2"; fi; }
# Shared-UART tolerant match: guest1's console, or the emulator's own
# "[UART] ... attached" note when guest1 first opens it, can interject
# mid-line in a secure or guest0 print (e.g. "TOTAL SK<freertos_guest1:
# ...>IPPED : 4"), so strip both and rejoin split lines before requiring
# the exact bytes.
expect_flat() { if sed -e 's/freertos_guest1:.*$//' \
-e 's/\[UART\] [0-9a-f]* attached to [^ ]*//' "$log" | \
tr -d '\r\n' | grep -Fq "$2"; then check_pass "$1"; \
# Shared-UART tolerant match against the same guest0 view used by CI.
expect_flat() { if grep -Fq "$2" "$guest0_log"; then check_pass "$1"; \
else check_fail "$1" "missing: $2"; fi; }
refute_re() { if grep -Eq "$2" "$log"; then check_fail "$1" "unexpected: $2"; \
else check_pass "$1"; fi; }
Expand Down Expand Up @@ -502,31 +501,31 @@ case "$scenario" in
refute_re "no fault markers in boot log" \
'^(\[MEMFAULT\]|\[HARDFLT\]|HardFault|SecureFault)'
fi
expect "TEE client initialized" "wolfTrust TEE client initialized"
expect "FF-M psa_framework_version=0x0100" \
expect_flat "TEE client initialized" "wolfTrust TEE client initialized"
expect_flat "FF-M psa_framework_version=0x0100" \
"wolfTrust FF-M psa_framework_version=0x0100"
expect "mediated crypto dispatch verified" \
expect_flat "mediated crypto dispatch verified" \
"wolfTrust FF-M mediated crypto dispatch verified"
expect "ITS set/get verified" \
expect_flat "ITS set/get verified" \
"wolfTrust ITS set/get verified"
expect "PS sealed set/get verified" \
expect_flat "PS sealed set/get verified" \
"wolfTrust PS sealed set/get verified"
expect "key-ops sign/verify verified" \
expect_flat "key-ops sign/verify verified" \
"wolfTrust key-ops sign/verify verified"
expect "key negatives verified" \
expect_flat "key negatives verified" \
"wolfTrust key negatives verified"
expect_flat "forged-handle call rejected" \
"wolfTrust FF-M forged-handle call rejected"
expect_flat "oversized-vector call rejected" \
"wolfTrust FF-M oversized-vector call rejected"
expect "psa_hash_compute(SHA-256) KAT verified" \
expect_flat "psa_hash_compute(SHA-256) KAT verified" \
"psa_hash_compute(SHA-256) KAT verified"
expect "psa_initial_attestation st=0" "psa_initial_attestation st=0"
expect "attestation COSE_Sign1 verified" \
expect_flat "psa_initial_attestation st=0" "psa_initial_attestation st=0"
expect_flat "attestation COSE_Sign1 verified" \
"wolfTrust attestation: COSE_Sign1 verified"
expect "token measurement equals wolfBoot measurement of the signed image" \
expect_flat "token measurement equals wolfBoot measurement of the signed image" \
"wolfTrust attestation: token measurement=$WT_EXPECTED_MEASUREMENT_HEX"
expect "attestation fields verify=0 lifecycle=0x1000 measurement=ok cose=ES256" \
expect_flat "attestation fields verify=0 lifecycle=0x1000 measurement=ok cose=ES256" \
"attestation verify=0 challenge=ok identity=ok lifecycle=0x1000 measurement=ok cose=ES256"
expect "guest1 FF-M SHA-256 KAT through SERVICE_CRYPTO (P7-S3)" \
"freertos_guest1: ffm sha256 ok"
Expand Down Expand Up @@ -653,8 +652,7 @@ case "$scenario" in
"$conf_want was not recorded for psa-arch-tests $conf_rev"
fi
conf_got="$repo/build/ffm-ipc-results.txt"
sed 's/freertos_guest1:.*$//' "$log" | tr -d '\r\n' | \
grep -aoE 'Num=[0-9]+|Result=[A-Za-z]+' | \
grep -aoE 'Num=[0-9]+|Result=[A-Za-z]+' "$guest0_log" | \
awk -F= '$1 == "Num" { num = $2 }
$1 == "Result" { if (num != "") print num, $2; num = "" }' | \
sort -n -u > "$conf_got"
Expand All @@ -672,9 +670,9 @@ case "$scenario" in
expect "TEE client initialized" "wolfTrust TEE client initialized"
expect "conformance val_entry start" \
"wolfTrust FF-M conformance: val_entry start"
# Flatten the shared UART (guest1 can interject mid-line), then read the
# suite totals: every dev_apis storage test must pass or skip, none FAIL.
flat="$(sed 's/freertos_guest1:.*$//' "$log" | tr -d '\r\n')"
# Read suite totals from the reconstructed guest0 console: every storage
# test must pass or skip, none FAIL.
flat="$(cat "$guest0_log")"
passed=$(printf '%s' "$flat" | grep -oE 'TOTAL PASSED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true)
skipped=$(printf '%s' "$flat" | grep -oE 'TOTAL SKIPPED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true)
failed=$(printf '%s' "$flat" | grep -oE 'TOTAL FAILED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true)
Expand All @@ -692,7 +690,7 @@ case "$scenario" in
expect "TEE client initialized" "wolfTrust TEE client initialized"
expect "conformance val_entry start" \
"wolfTrust FF-M conformance: val_entry start"
flat="$(sed 's/freertos_guest1:.*$//' "$log" | tr -d '\r\n')"
flat="$(cat "$guest0_log")"
passed=$(printf '%s' "$flat" | grep -oE 'TOTAL PASSED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true)
skipped=$(printf '%s' "$flat" | grep -oE 'TOTAL SKIPPED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true)
failed=$(printf '%s' "$flat" | grep -oE 'TOTAL FAILED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true)
Expand All @@ -716,7 +714,7 @@ case "$scenario" in
"wolfTrust FF-M conformance: val_entry start"
# test_a001 is the whole suite: get_token/get_token_size across all
# challenge sizes plus val's own COSE_Sign1 verify of the returned token.
flat="$(sed 's/freertos_guest1:.*$//' "$log" | tr -d '\r\n')"
flat="$(cat "$guest0_log")"
passed=$(printf '%s' "$flat" | grep -oE 'TOTAL PASSED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true)
failed=$(printf '%s' "$flat" | grep -oE 'TOTAL FAILED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true)
: "${passed:=-1}"; : "${failed:=-1}"
Expand Down Expand Up @@ -774,7 +772,7 @@ case "$scenario" in
# read over SWD on the H5 board; the emulator asserts the suite instead.)
expect "conformance val_entry start" \
"wolfTrust FF-M conformance: val_entry start"
flat="$(sed 's/freertos_guest1:.*$//' "$log" | tr -d '\r\n')"
flat="$(cat "$guest0_log")"
passed=$(printf '%s' "$flat" | grep -oE 'TOTAL PASSED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true)
skipped=$(printf '%s' "$flat" | grep -oE 'TOTAL SKIPPED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true)
failed=$(printf '%s' "$flat" | grep -oE 'TOTAL FAILED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true)
Expand Down
Loading