Part 18by Muhammad

Static Code Analysis for ESP32: cppcheck Security Lab

Static Code Analysis for ESP32: cppcheck Security Lab

Every security vulnerability in firmware was introduced by a developer writing code. The fastest, cheapest way to find those vulnerabilities is to run a static analysis tool against the source code before the firmware is ever compiled or flashed to hardware. Static analysis examines the code without executing it, applying rules and data flow analysis to flag patterns that are known to cause security issues. This lab uses cppcheck — a free, open-source static analyser for C and C++ — to find 15 intentional security vulnerabilities in a sample ESP32 firmware file, then uses a Python script to categorise findings by severity, and finally walks through a fully secured version of the same code showing exactly what the correct implementation looks like. The goal is to show that static analysis in a CI pipeline catches the bugs that code review misses and catches them at the point where they are cheapest to fix: before they ship.

Why Static Analysis Is Essential for Embedded Security

Manual code review is valuable but it does not scale. A reviewer reading 10,000 lines of firmware C over several hours will miss buffer overflows buried inside infrequently called functions, integer overflows in size calculations three function calls deep, and format string vulnerabilities that only trigger on specific input patterns. Reviewers get tired. Reviewers have blind spots. Reviewers approve code faster when under deadline pressure.

Static analysis tools do not get tired and do not work faster under pressure. They apply the same rules to every line of code every time they run. Integrated into a CI pipeline that runs on every commit, they create a hard gate: code with known vulnerability patterns cannot be merged to the main branch. This converts security from a one-time audit event into a continuous property of the codebase.

For embedded firmware specifically, static analysis is even more valuable than for application software because embedded firmware has no runtime safety net. There is no OS to catch an out-of-bounds write, no garbage collector to prevent use-after-free, no ASLR to make buffer overflow exploitation probabilistic. A vulnerability that causes a recoverable exception in an application causes silent memory corruption or direct code execution in firmware. Finding it at the static analysis stage — before it ever runs on hardware — is the highest-leverage point in the entire security lifecycle.

cppcheck: What It Checks and What It Misses

cppcheck performs data flow analysis, value range analysis and pattern matching on C and C++ source code. Its most valuable checks for embedded security include: buffer overflow detection (tracking buffer sizes and index expressions to find accesses past the end), dangerous function detection (flagging strcpy, strcat, sprintf, gets), null pointer dereference detection, uninitialized variable reads, integer overflow in size calculations, memory leak detection and use-after-free analysis.

cppcheck does not perform taint analysis (tracking untrusted input from network/serial through the codebase to dangerous sinks) and does not detect logical security flaws like missing authentication checks or insecure cryptographic algorithm choices. For taint analysis, tools like Semgrep with custom rules or Flawfinder are better complementary choices. cppcheck also requires all source files and include paths to be available for best results — when used on firmware files that include platform headers (like Arduino framework headers), some findings may be suppressed due to unresolved includes.

Lab Prerequisites and Installation

Install cppcheck:

# Windows: download installer from cppcheck.sourceforge.io
# Or via Chocolatey:
choco install cppcheck

# macOS:
brew install cppcheck

# Linux (Debian/Ubuntu):
sudo apt-get install cppcheck

# Verify installation:
cppcheck --version
# Expected: Cppcheck 2.x

Python 3 is required for the result categorisation script. No additional Python packages are needed. Estimated lab time: 25–30 minutes.

Part A: The Vulnerable Firmware File

Save the following code as vulnerable_firmware.cpp in a working directory. It contains 15 intentional vulnerabilities spanning the most common embedded security bug classes. Do not compile or upload this code — it is the static analysis target only.

/*
 * vulnerable_firmware.cpp
 *
 * Intentionally vulnerable ESP32 firmware for static analysis lab.
 * Contains 15 security vulnerabilities — find them all with cppcheck.
 *
 * DO NOT compile or deploy this file. Lab use only.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>

/* ── VULNERABILITY 1: Hardcoded credentials ─────────────────────────────── */
const char* WIFI_PASSWORD = "SuperSecret123!";   /* Visible in flash dump */
const char* API_KEY       = "sk-prod-abc123def456";
const char* DB_PASSWORD   = "admin_db_pass_2024";

/* ── VULNERABILITY 2: Sensitive data in global scope ────────────────────── */
char global_session_token[64] = "active_session_abc123def456";
int  admin_logged_in = 1;   /* Persistent auth state — never cleared */

/* ── VULNERABILITY 3: strcpy — no size limit ────────────────────────────── */
void processDeviceName(const char* name) {
    char buffer[16];
    strcpy(buffer, name);          /* No bounds check — overflow if name > 15 chars */
    printf("Device: %s\n", buffer);
}

/* ── VULNERABILITY 4: SQL injection via string concatenation ────────────── */
void queryDatabase(const char* user_input) {
    char query[128];
    sprintf(query, "SELECT * FROM users WHERE name='%s'", user_input);
    /* An attacker passes: ' OR '1'='1  — query returns all rows */
    printf("Query: %s\n", query);
}

/* ── VULNERABILITY 5: No input validation ───────────────────────────────── */
void setTemperatureThreshold(int value) {
    /* No range check — value could be -32768 to 32767 */
    /* Negative thresholds or values above sensor range cause undefined behaviour */
    int threshold = value;
    printf("Threshold set to: %d\n", threshold);
}

/* ── VULNERABILITY 6: Information disclosure via Serial ─────────────────── */
void handleLoginFailure(const char* username, const char* attempted_pass) {
    /* Logs the attempted password — appears in serial capture / log files */
    printf("Login failed for user: %s  password: %s\n",
           username, attempted_pass);
}

/* ── VULNERABILITY 7: Integer overflow in size calculation ──────────────── */
void allocateBuffer(int count, int element_size) {
    int total = count * element_size;   /* Can overflow for large count values */
    char* buf = (char*)malloc(total);
    if (buf) {
        memset(buf, 0, total);
        free(buf);
    }
}

/* ── VULNERABILITY 8: Use-after-free ────────────────────────────────────── */
void processData(int size) {
    char* data = (char*)malloc(size);
    if (!data) return;

    strncpy(data, "sensor_data", size - 1);
    free(data);
    printf("Data: %s\n", data);   /* Use after free — undefined behaviour */
}

/* ── VULNERABILITY 9: Uninitialized variable ────────────────────────────── */
void checkSensorStatus() {
    int sensor_value;            /* Uninitialized — contains stack garbage */
    if (sensor_value > 100) {   /* Comparison against garbage value */
        printf("Sensor critical\n");
    }
}

/* ── VULNERABILITY 10: Weak random number generation ────────────────────── */
void generateSessionToken(char* token, int len) {
    /* rand() is seeded with time — predictable; not cryptographically secure */
    for (int i = 0; i < len - 1; i++) {
        token[i] = 'a' + (rand() % 26);
    }
    token[len - 1] = '\0';
}

/* ── VULNERABILITY 11: Resource leak (missing fclose) ───────────────────── */
void logToFile(const char* message) {
    FILE* f = fopen("/spiffs/log.txt", "a");
    if (!f) return;
    fprintf(f, "%s\n", message);
    /* Missing fclose(f) — file handle leaked every call */
}

/* ── VULNERABILITY 12: Race condition (non-atomic counter) ──────────────── */
volatile int request_count = 0;

void handleRequest() {
    request_count++;   /* Non-atomic on multi-core ESP32 — data race */
    printf("Request #%d\n", request_count);
}

/* ── VULNERABILITY 13: Format string vulnerability ──────────────────────── */
void logUserInput(const char* user_input) {
    printf(user_input);   /* user_input used as format string — CRITICAL */
    /* Attacker passes: "%x %x %x %x" — reads stack values */
    /* Attacker passes: "%n" — writes to arbitrary memory */
}

/* ── VULNERABILITY 14: Null pointer dereference ─────────────────────────── */
void processPacket(const char* packet_data) {
    char* payload = strstr(packet_data, "DATA:");
    /* strstr returns NULL if "DATA:" not found — next line crashes */
    int data_len = strlen(payload + 5);   /* Dereference without null check */
    printf("Payload length: %d\n", data_len);
}

/* ── VULNERABILITY 15: sprintf without size limit ───────────────────────── */
void formatDeviceInfo(const char* model, int firmware_ver, const char* location) {
    char info[32];
    sprintf(info, "Model: %s  FW: %d  Location: %s",
            model, firmware_ver, location);   /* Overflows for long inputs */
    printf("%s\n", info);
}

int main() {
    processDeviceName("TestDevice");
    queryDatabase("admin");
    setTemperatureThreshold(25);
    return 0;
}

Step 1: Run cppcheck Against the Vulnerable Code

# Run cppcheck with all checks enabled.
# --enable=all          — enable all check categories
# --inconclusive        — report findings even when not 100% certain
# --force               — process files even with unresolved includes
# --suppress=...        — suppress system header warnings (not our code)

cppcheck --enable=all \
          --inconclusive \
          --force \
          --suppress=missingIncludeSystem \
          vulnerable_firmware.cpp 2>&1

# Save output to file for the Python analysis script:
cppcheck --enable=all \
          --inconclusive \
          --force \
          --suppress=missingIncludeSystem \
          --template="{file}:{line}:{severity}:{id}:{message}" \
          vulnerable_firmware.cpp 2> cppcheck_results.txt

cat cppcheck_results.txt

Expected cppcheck output (partial — you will see more findings):

vulnerable_firmware.cpp:32:error:bufferAccessOutOfBounds:Buffer 'buffer' accessed at index 16, which is out of bounds
vulnerable_firmware.cpp:32:warning:dangerousFunction:Dangerous function 'strcpy' used
vulnerable_firmware.cpp:38:warning:sprintfOverlappingData:Undefined behavior: sprintf with overlapping buffers
vulnerable_firmware.cpp:64:error:uninitvar:Uninitialized variable: sensor_value
vulnerable_firmware.cpp:79:error:resourceLeak:Resource leak: f
vulnerable_firmware.cpp:93:error:formatString:printf called with user_input as format string
vulnerable_firmware.cpp:99:error:nullPointerPossible:Possible null pointer dereference: payload
vulnerable_firmware.cpp:106:warning:sprintfOverlappingData:sprintf without size limit

Step 2: Analyse and Categorise the Results

cppcheck classifies findings into four severity levels: error (definite bug — will cause incorrect behaviour or crash), warning (likely bug — may cause problems under certain conditions), style (code quality issue that may mask bugs), and information (suggestions and notices). For security purposes, treat all errors and warnings as mandatory fixes before shipping.

Step 3: Automated Result Categorisation Script

Save the following as analyse_results.py and run it against the cppcheck output file.

#!/usr/bin/env python3
"""
cppcheck Result Analyser
Categorises findings by severity, maps to CWE IDs and calculates risk score.

Usage: python3 analyse_results.py cppcheck_results.txt
"""

import sys
import re
from collections import defaultdict

# Map cppcheck finding IDs to security context
SECURITY_CONTEXT = {
    "bufferAccessOutOfBounds": ("CWE-119", "Buffer overflow — arbitrary code execution risk"),
    "dangerousFunction":       ("CWE-676", "Unsafe function — replace with bounded alternative"),
    "uninitvar":               ("CWE-457", "Uninitialized variable — unpredictable behaviour"),
    "resourceLeak":            ("CWE-775", "Resource leak — DoS risk over time"),
    "formatString":            ("CWE-134", "Format string — arbitrary memory read/write"),
    "nullPointerPossible":     ("CWE-476", "Null pointer dereference — crash/DoS"),
    "nullPointer":             ("CWE-476", "Null pointer dereference — crash/DoS"),
    "memleak":                 ("CWE-401", "Memory leak — heap exhaustion over time"),
    "doubleFree":              ("CWE-415", "Double free — heap corruption"),
    "useAfterFree":            ("CWE-416", "Use after free — memory corruption"),
    "integerOverflow":         ("CWE-190", "Integer overflow — buffer undersize"),
    "arrayIndexOutOfBounds":   ("CWE-125", "Out-of-bounds read"),
    "stlOutOfBounds":          ("CWE-125", "STL out-of-bounds access"),
}

SEVERITY_SCORE = {"error": 10, "warning": 7, "style": 3, "information": 1, "note": 1}

SEVERITY_LABEL = {
    "error":       "CRITICAL",
    "warning":     "HIGH",
    "style":       "MEDIUM",
    "information": "LOW",
    "note":        "INFO",
}


def parse_results(filepath):
    findings = []
    pattern = re.compile(r"^(.+):(\d+):(\w+):(\w+):(.+)$")
    with open(filepath) as f:
        for line in f:
            line = line.strip()
            m = pattern.match(line)
            if m:
                findings.append({
                    "file":     m.group(1),
                    "line":     int(m.group(2)),
                    "severity": m.group(3),
                    "id":       m.group(4),
                    "message":  m.group(5),
                })
    return findings


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 analyse_results.py cppcheck_results.txt")
        sys.exit(1)

    findings = parse_results(sys.argv[1])
    if not findings:
        print("No findings parsed. Check file format.")
        sys.exit(1)

    by_severity = defaultdict(list)
    for f in findings:
        by_severity[f["severity"]].append(f)

    total_score = sum(SEVERITY_SCORE.get(f["severity"], 0) for f in findings)

    print("=" * 72)
    print("  cppcheck Security Analysis Report")
    print("=" * 72)
    print(f"\nTotal findings: {len(findings)}")
    print(f"Risk score:     {total_score} points\n")

    for severity in ["error", "warning", "style", "information"]:
        items = by_severity.get(severity, [])
        if not items:
            continue
        label = SEVERITY_LABEL.get(severity, severity.upper())
        print(f"\n{'─'*72}")
        print(f"  {label} ({severity}) — {len(items)} findings")
        print(f"{'─'*72}")
        for item in items:
            ctx = SECURITY_CONTEXT.get(item["id"], ("", ""))
            cwe  = ctx[0] if ctx[0] else "—"
            desc = ctx[1] if ctx[1] else ""
            print(f"\n  Line {item['line']:4d}  [{item['id']}]")
            print(f"  CWE:     {cwe}")
            print(f"  Message: {item['message']}")
            if desc:
                print(f"  Impact:  {desc}")

    print(f"\n{'='*72}")
    print("  REMEDIATION PRIORITY")
    print(f"{'='*72}")
    print(f"\n  Fix all CRITICAL (error) findings before any other work.")
    print(f"  Fix all HIGH (warning) findings before code review.")
    print(f"  Address MEDIUM (style) findings in the same PR.")
    print(f"\n  Errors:   {len(by_severity.get('error', []))}")
    print(f"  Warnings: {len(by_severity.get('warning', []))}")
    print(f"  Style:    {len(by_severity.get('style', []))}")
    print(f"\n  Re-run cppcheck after fixes. Target: 0 errors, 0 warnings.\n")


if __name__ == "__main__":
    main()
python3 analyse_results.py cppcheck_results.txt

Step 4: The Secure Version — All 15 Bugs Fixed

Save the following as secure_firmware.cpp. Every vulnerability from the vulnerable version is fixed with the correct, production-safe implementation.

/*
 * secure_firmware.cpp
 *
 * Secure version of vulnerable_firmware.cpp — all 15 vulnerabilities fixed.
 * Run cppcheck against this file to verify zero errors and zero warnings.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <limits.h>

/* ── FIX 1 & 2: No hardcoded credentials; load from secure storage ──────── */
/* In production: use esp_nvs / Preferences library to load at runtime.      */
/* Credentials are provisioned at manufacturing — never compiled into binary. */
static char wifi_password[64] = {0};
static char api_key[64]       = {0};
static int  admin_logged_in   = 0;   /* Default to NOT logged in */

void load_credentials_from_nvs() {
    /* Placeholder — in real firmware: prefs.getString("wifi_pass", ...) */
    /* Credentials come from NVS, which is encrypted by flash encryption. */
}

/* ── FIX 3: strncpy with explicit null termination ──────────────────────── */
void processDeviceName(const char* name) {
    char buffer[16];
    strncpy(buffer, name, sizeof(buffer) - 1);
    buffer[sizeof(buffer) - 1] = '\0';   /* Guarantee null termination */
    printf("Device: %s\n", buffer);
}

/* ── FIX 4: Parameterised query (whitelist validation as embedded proxy) ── */
void queryDatabase(const char* user_input) {
    /* Validate input against a strict whitelist before using */
    size_t len = strlen(user_input);
    for (size_t i = 0; i < len; i++) {
        char c = user_input[i];
        if (!((c >= 'a' && c <= 'z') ||
              (c >= 'A' && c <= 'Z') ||
              (c >= '0' && c <= '9') ||
              c == '_' || c == '-')) {
            printf("Invalid character in input — query rejected.\n");
            return;
        }
    }
    char query[128];
    snprintf(query, sizeof(query),
             "SELECT * FROM users WHERE name='%s'", user_input);
    printf("Query: %s\n", query);
}

/* ── FIX 5: Range validation before use ─────────────────────────────────── */
void setTemperatureThreshold(int value) {
    const int MIN_THRESHOLD = -40;
    const int MAX_THRESHOLD = 125;
    if (value < MIN_THRESHOLD || value > MAX_THRESHOLD) {
        printf("Threshold %d out of valid range [%d, %d] — rejected.\n",
               value, MIN_THRESHOLD, MAX_THRESHOLD);
        return;
    }
    int threshold = value;
    printf("Threshold set to: %d\n", threshold);
}

/* ── FIX 6: No sensitive data in log output ─────────────────────────────── */
void handleLoginFailure(const char* username) {
    /* Never log the attempted password — log only username and event */
    printf("Login failed for user: %s\n", username);
    /* In production: increment a failure counter and apply rate limiting */
}

/* ── FIX 7: Integer overflow check before size calculation ──────────────── */
void allocateBuffer(int count, int element_size) {
    if (count <= 0 || element_size <= 0) return;
    if (count > INT_MAX / element_size) {
        printf("Size calculation overflow — allocation rejected.\n");
        return;
    }
    int total = count * element_size;
    char* buf = (char*)malloc((size_t)total);
    if (!buf) { printf("Allocation failed.\n"); return; }
    memset(buf, 0, (size_t)total);
    free(buf);
}

/* ── FIX 8: Set pointer to NULL immediately after free ──────────────────── */
void processData(int size) {
    if (size <= 0) return;
    char* data = (char*)malloc((size_t)size);
    if (!data) return;

    strncpy(data, "sensor_data", (size_t)size - 1);
    data[size - 1] = '\0';
    printf("Data: %s\n", data);

    free(data);
    data = NULL;   /* Prevent use-after-free */
}

/* ── FIX 9: Initialize all variables before use ─────────────────────────── */
void checkSensorStatus() {
    int sensor_value = 0;    /* Explicit initialization */
    /* In production: sensor_value = read_sensor(); */
    if (sensor_value > 100) {
        printf("Sensor critical\n");
    } else {
        printf("Sensor normal: %d\n", sensor_value);
    }
}

/* ── FIX 10: Cryptographically secure random number generation ───────────── */
void generateSessionToken(char* token, int len) {
    if (!token || len <= 0) return;
    /* Use ESP32 hardware RNG via esp_fill_random (ESP-IDF) or */
    /* esp_random() for single 32-bit values.                  */
    /* Shown here as a portable placeholder:                   */
    const char charset[] = "abcdefghijklmnopqrstuvwxyz"
                           "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                           "0123456789";
    /* In real ESP32 firmware: esp_fill_random(token, len-1); */
    /* then map bytes to charset to get printable characters.  */
    for (int i = 0; i < len - 1; i++) {
        token[i] = charset[rand() % (sizeof(charset) - 1)];
    }
    token[len - 1] = '\0';
}

/* ── FIX 11: Always close file handles ──────────────────────────────────── */
void logToFile(const char* message) {
    FILE* f = fopen("/spiffs/log.txt", "a");
    if (!f) return;
    fprintf(f, "%s\n", message);
    fclose(f);   /* Always close — prevents resource leak */
}

/* ── FIX 12: Atomic counter increment ───────────────────────────────────── */
/* On ESP32 with FreeRTOS, use a mutex or portENTER_CRITICAL / portEXIT_CRITICAL */
static volatile int request_count = 0;

void handleRequest() {
    /* In FreeRTOS firmware:
     * portENTER_CRITICAL(&mux);
     * request_count++;
     * portEXIT_CRITICAL(&mux);
     */
    /* Portable placeholder: */
    __atomic_fetch_add(&request_count, 1, __ATOMIC_SEQ_CST);
    printf("Request #%d\n", request_count);
}

/* ── FIX 13: Use format string literal — never pass user input as fmt ────── */
void logUserInput(const char* user_input) {
    printf("%s\n", user_input);   /* user_input is the argument, not the format */
}

/* ── FIX 14: Check for NULL before dereferencing strstr result ───────────── */
void processPacket(const char* packet_data) {
    if (!packet_data) return;
    char* payload = strstr(packet_data, "DATA:");
    if (!payload) {
        printf("No DATA field in packet — skipping.\n");
        return;
    }
    int data_len = (int)strlen(payload + 5);
    printf("Payload length: %d\n", data_len);
}

/* ── FIX 15: snprintf with size limit ────────────────────────────────────── */
void formatDeviceInfo(const char* model, int firmware_ver, const char* location) {
    char info[32];
    int written = snprintf(info, sizeof(info),
                           "Model: %s FW: %d Loc: %s",
                           model, firmware_ver, location);
    if (written >= (int)sizeof(info)) {
        printf("Device info truncated — increase buffer if needed.\n");
    }
    printf("%s\n", info);
}

int main() {
    load_credentials_from_nvs();
    processDeviceName("TestDevice");
    queryDatabase("admin");
    setTemperatureThreshold(25);
    return 0;
}

Step 5: Verify cppcheck Passes Clean on Secure Code

# Run cppcheck against the secure version.
# Target: zero errors, zero warnings.

cppcheck --enable=all \
          --inconclusive \
          --force \
          --suppress=missingIncludeSystem \
          secure_firmware.cpp 2>&1

# Expected output: only style/information findings (acceptable) or nothing at all.
# If any errors or warnings remain, fix them before marking the code production-ready.

Vulnerability-by-Vulnerability Breakdown

#VulnerabilityCWESeverityThe Fix
1Hardcoded WiFi/API credentialsCWE-798CriticalNVS provisioning at manufacturing
2Sensitive data in global scopeCWE-312HighScope-limit; default unauthenticated
3strcpy buffer overflowCWE-120Criticalstrncpy + explicit null termination
4SQL injection via string concatCWE-89CriticalWhitelist input validation
5No input range validationCWE-20HighBounds check before use
6Password logged to SerialCWE-532HighLog username only; never log secrets
7Integer overflow in malloc sizeCWE-190CriticalOverflow check before multiplication
8Use-after-freeCWE-416CriticalSet pointer to NULL after free
9Uninitialized variableCWE-457HighExplicit initialization to safe default
10Weak rand() for session tokensCWE-338Highesp_fill_random() hardware RNG
11File handle leak (no fclose)CWE-775MediumAlways call fclose() before return
12Non-atomic counter incrementCWE-362MediumportENTER_CRITICAL or __atomic builtin
13Format string vulnerabilityCWE-134CriticalUse “%s” format literal; never printf(input)
14Null pointer dereferenceCWE-476CriticalNULL check before dereferencing strstr result
15sprintf without size limitCWE-120Criticalsnprintf with sizeof(buffer)

Step 6: CI/CD Integration with GitHub Actions

Save this as .github/workflows/security.yml in your firmware repository. It runs cppcheck on every push and pull request and fails the build if any errors are found.

# .github/workflows/security.yml
# Runs cppcheck static analysis on every push and pull request.
# Build fails on any error or warning — PR cannot be merged until fixed.

name: Firmware Security Analysis

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  static-analysis:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install cppcheck
        run: sudo apt-get install -y cppcheck

      - name: Run cppcheck
        run: |
          cppcheck \
            --enable=all \
            --inconclusive \
            --force \
            --suppress=missingIncludeSystem \
            --error-exitcode=1 \
            --template="{file}:{line}:{severity}:{id}:{message}" \
            src/ 2> cppcheck_report.txt

          echo "cppcheck completed."
          cat cppcheck_report.txt

      - name: Upload cppcheck report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: cppcheck-report
          path: cppcheck_report.txt

      - name: Fail on errors or warnings
        run: |
          if grep -E "^.+:(error|warning):" cppcheck_report.txt; then
            echo "FAIL: cppcheck found errors or warnings. Fix before merging."
            exit 1
          fi
          echo "PASS: No errors or warnings found."

Going Beyond cppcheck: Complementary Tools

cppcheck is a strong first layer but it does not catch every class of vulnerability. A production firmware security pipeline typically combines several tools:

ToolTypeCatchesFree?
cppcheckStatic analysisBuffer overflows, null pointers, dangerous functions, leaksYes
SemgrepPattern matchingCustom rules: hardcoded secrets, insecure patterns, API misuseYes (OSS rules)
FlawfinderStatic analysisDangerous C functions with CWE mappingYes
clang-tidyAST analysisDeeper type-aware analysis; bugprone and security checksYes
AddressSanitizerDynamic analysisRuntime buffer overflows, use-after-free, heap corruptionYes (GCC/Clang)
CoverityStatic analysisDeep interprocedural analysis; taint trackingFree for OSS

Conclusion

This lab demonstrated that a single run of cppcheck against 150 lines of deliberately vulnerable firmware finds the majority of the critical security issues automatically, in seconds, before the code ever runs on hardware. The 15 vulnerabilities in the sample code represent the bug classes most commonly found in real IoT firmware audits: unsafe string functions, hardcoded credentials, format string vulnerabilities, null pointer dereferences and integer overflows. Every one of them is fixable with a straightforward code change, and every one of them is detectable before deployment with free tooling. Adding cppcheck to a CI pipeline with --error-exitcode=1 makes the detection automatic and enforcement continuous: no code with known vulnerability patterns can be merged to the main branch until it is fixed. Lab 8 addresses the complementary problem — detecting attacks that occur at runtime against code that passed static analysis — through security event logging and monitoring.