Skip to content
Merged
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
180 changes: 180 additions & 0 deletions .github/workflows/code-coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
name: Code Coverage

on:
push:
branches: [ 'master', 'main', 'release/**' ]
pull_request:
branches: [ '*' ]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build_wolfssl:
name: Build wolfSSL
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout wolfSSL
uses: actions/checkout@v6
with:
repository: wolfssl/wolfssl
path: wolfssl

# Match the sshd-test workflow so the cert and ML-DSA paths are built
# and measured rather than compiled out.
- name: Build wolfSSL
working-directory: ./wolfssl
run: |
./autogen.sh
./configure --enable-all --enable-mldsa
make -j$(nproc)
sudo make install
sudo ldconfig

- name: tar build-dir
run: tar -zcf wolfssl-install.tgz /usr/local/lib/libwolfssl* /usr/local/include/wolfssl

- name: Upload built lib
uses: actions/upload-artifact@v7
with:
name: wolfssl-coverage
path: wolfssl-install.tgz
retention-days: 5

# Use clang to report line, branch, function and MC/DC coverage in one run.
coverage:
name: Coverage
runs-on: ubuntu-latest
timeout-minutes: 45
needs: build_wolfssl
steps:
- name: Checkout wolfSSH
uses: actions/checkout@v6

# clang 18 is the min: -fcoverage-mcdc does not exist before it.
- name: Install clang and LLVM coverage tools
run: |
sudo apt-get update
sudo apt-get install -y clang-18 llvm-18 libclang-rt-18-dev

- name: Download wolfSSL
uses: actions/download-artifact@v8
with:
name: wolfssl-coverage

- name: Install wolfSSL
run: |
sudo tar -xzf wolfssl-install.tgz -C /
sudo ldconfig

# -O0 keeps line and branch attribution honest; atomic counters are
# required because several tests drive client and server on separate
# threads, and the default non-atomic updates lose increments.
- name: Build wolfSSH
run: |
./autogen.sh
./configure --enable-all --enable-ossh-certs CC=clang-18 \
CPPFLAGS="-DMAX_PATH_SZ=120" \
CFLAGS="-fprofile-instr-generate -fcoverage-mapping -fcoverage-mcdc -fprofile-update=atomic -O0 -g" \
LDFLAGS="-fprofile-instr-generate"
make -j$(nproc)

# %p in the pattern keeps forked servers from overwriting the raw
# profile of the client that spawned them.
- name: Run tests
run: |
mkdir -p prof
LLVM_PROFILE_FILE="$PWD/prof/%p-%m.profraw" \
timeout -k 30 1200 make check

# 'make check' does not execute wolfsshd, so run it separately
- name: Run wolfSSHd tests
working-directory: ./apps/wolfsshd/test
run: |
prof="$GITHUB_WORKSPACE/prof/%p-%m.profraw"
sudo LLVM_PROFILE_FILE="$prof" SSHD_ENV="LLVM_PROFILE_FILE=$prof" \
./run_all_sshd_tests.sh
sudo chown -R "$(id -u):$(id -g)" "$GITHUB_WORKSPACE/prof"

- name: Report coverage
run: |
llvm-profdata-18 merge -sparse prof/*.profraw -o wolfssh.profdata
# llvm-cov takes one binary positionally and the rest via -object.
# Programs linking the shared library are libtool wrapper scripts, so
# take the real binary from .libs when one is there. The apps are
# optional, so skip whatever this configuration did not build.
first=""
args=()
for t in tests/*.test apps/wolfssh/wolfssh apps/wolfsshd/wolfsshd \
apps/wolfsshd/test/test_configuration; do
[ -e "$t" ] || continue
real="$(dirname "$t")/.libs/$(basename "$t")"
[ -x "$real" ] || real="$t"
if [ -z "$first" ]; then
first="$real"
else
args+=(-object "$real")
fi
done
if [ -z "$first" ]; then
echo "no instrumented binaries found"
exit 1
fi
ignore='(tests|examples)/.*|apps/wolfsshd/test/.*'
ignore="$ignore"'|.*/include/wolfssl/.*|.*/wolfssh/.*\.h'
llvm-cov-18 report "$first" "${args[@]}" \
-instr-profile=wolfssh.profdata \
--show-mcdc-summary \
--ignore-filename-regex="$ignore" | tee coverage-report.txt
llvm-cov-18 show "$first" "${args[@]}" \
-instr-profile=wolfssh.profdata \
--show-mcdc --format=html --output-dir=coverage-html \
--ignore-filename-regex="$ignore"
# lcov text for any external dashboard that consumes it.
llvm-cov-18 export "$first" "${args[@]}" \
-instr-profile=wolfssh.profdata \
--format=lcov \
--ignore-filename-regex="$ignore" > coverage.lcov
{
echo '### Coverage'
echo '```'
cat coverage-report.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload coverage report
uses: actions/upload-artifact@v7
with:
name: coverage-report
path: |
coverage-report.txt
coverage.lcov
coverage-html/
retention-days: 30

- name: Show test logs on failure
if: failure()
run: |
echo "=== test-suite.log ==="
cat test-suite.log || true
for f in tests/*.log scripts/*.log; do
[ -f "$f" ] || continue
echo ""
echo "=== $f ==="
cat "$f"
done

- name: Upload failure logs
if: failure()
uses: actions/upload-artifact@v7
with:
name: wolfssh-coverage-logs
path: |
test-suite.log
tests/*.log
scripts/*.log
config.log
retention-days: 5
6 changes: 6 additions & 0 deletions apps/wolfssh-options.c
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ int main(void)
printf("FPKI\n");
#endif

/* certman.c's FPKI certificate profile enforcement. Separate from FPKI
* above. */
#if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_FPKI)
printf("FPKI_PROFILE\n");
#endif

/* PQC Options */
#ifndef WOLFSSH_NO_MLDSA
printf("MLDSA\n");
Expand Down
9 changes: 9 additions & 0 deletions apps/wolfsshd/test/sshd_x509_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
# Not named PWD: the shell rewrites that variable on every cd, so a saved
# copy would not survive the cd to the repository root below.
TESTDIR=`pwd`
. ./wolfssh_options.sh

# No FPKI profiles exist in keys/, so skip this test which would fail.
# Drop this skip once conforming certificates are added.
if wolfssh_has FPKI_PROFILE; then
echo "wolfSSHd enforces FPKI profiles; test certs meet none, skipping"
exit 77
fi

cd ../../..

if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then
Expand Down
7 changes: 7 additions & 0 deletions apps/wolfsshd/test/sshd_x509_upn_fail.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ if ! wolfssh_has FPKI; then
exit 77
fi

# No FPKI profiles exist in keys/, so skip this test which would fail.
# Drop this skip once conforming certificates are added.
if wolfssh_has FPKI_PROFILE; then
echo "wolfSSHd enforces FPKI profiles; UPN check not reached, skipping"
exit 77
fi

# Count existing rejection lines first so a stale match left in the appended
# log (start_sshd.sh uses 'wolfsshd -E ./log.txt', which never truncates) is
# not mistaken for this run's rejection.
Expand Down
22 changes: 18 additions & 4 deletions apps/wolfsshd/test/start_sshd.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,24 @@ EOF
sudo env $SSHD_ENV "$SSHD_BIN" -d -E ./log.txt -f "$CONFIG"

# The PID of the started sshd is the one present now that was not there
# before. The daemon can still die after sudo returns, so guard the same
# way and let the caller's empty-PID check report it.
NEW_PIDS=`pgrep -x wolfsshd | sort -n` || true
PID=`diff <(echo "$CURRENT_PIDS") <(echo "$NEW_PIDS") | sed -n 's/^> *//p' | head -n1`
# before. wolfSSHd forks twice while daemonizing, so for a moment its two
# short lived parents are listed as well; wait for the new pids to settle
# on the single survivor. Recording a parent instead would leave
# stop_wolfsshd killing a pid that is already gone while the real daemon
# keeps the port. The daemon can also die after sudo returns, so leave PID
# empty in that case and let the caller's empty-PID check report it.
PID=""
for i in $(seq 1 50); do
NEW_PIDS=`pgrep -x wolfsshd | sort -n` || true
NEW=`diff <(echo "$CURRENT_PIDS") <(echo "$NEW_PIDS") \
| sed -n 's/^> *//p'`
NEW_COUNT=`printf '%s\n' $NEW | grep -c .` || NEW_COUNT=0
if [ "$NEW_COUNT" -eq 1 ]; then
PID="$NEW"
break
fi
sleep 0.1
done
printf "SSHD running on PID $PID\n"
}

Expand Down
Loading