Safe Penetration Testing Labs · intermediate · ~15 min

Parse saved nmap XML output (defensive)

- Read a saved `nmap -oX` XML report from disk and walk it as plain text with C string functions. - Count `<port>` elements whose nested `<state>` is exactly `open`, ignoring `filtered` and `closed`. - Write a bounded, crash-proof parser that tolerates malformed input instead of trusting it. - Separate the state look-ahead so one port's data can never leak into another port's count. - Turn the parsed result into a defender's answer: "which hosts have an unexpected port open?" backed by logging you can audit.

Overview

Security objective: the asset you are protecting is your organization's attack surface — the set of network services exposed on your hosts. The threat is an unexpected or unauthorized open port (a forgotten Telnet daemon, a debug service, a re-enabled port) that an attacker could reach. In this lesson you build a small C program that reads already-saved scan output and detects open ports, so a defender can compare them against an approved baseline. You detect exposure; you do not create it.

This is pure string parsing. Your program never opens a socket, never sends a packet, and never does DNS. It reads bytes that a previous, authorized scan wrote to a file. That distinction is the whole point: running a scanner is an active, permissioned action; reading its saved output is passive analysis a defender does constantly.

Builds on your prereqs. From C strings you reuse strstr, strchr, NUL-termination, and pointer walking — an nmap XML file is just one long NUL-terminated char buffer. From The main function you reuse int main(void), reading arguments, returning a status code, and structuring a program that reads input, computes a result, and reports it. We add one new idea on top: bounded parsing — always stopping the look-ahead at a known limit so text from a neighboring element can never be counted by mistake.

Why not a real XML library? Because the goal here is to learn defensive text-walking on a small, regular format, and to see exactly where a naive parser goes wrong. In production you would often reach for a hardened XML parser; we discuss that trade-off explicitly.

Why it matters

In authorized professional work, defenders read scan output far more often than they run scans. A blue team (defensive security team) receives nmap XML from a scheduled internal scan, an authorized pentest engagement, or a vulnerability-management tool, and has to answer concrete questions fast: Which hosts have port 23 (Telnet) open? Did anything change since last week? Does this match our approved baseline?

A 25-line parser is often the fastest way to answer. Feeding thousands of lines of XML to a human does not scale; a small, auditable program does. Because the parser handles security data, it must be trustworthy: it cannot crash on a truncated file, it cannot silently miscount, and it must log what it decided so an analyst can verify the result later.

These same habits — never trust input, always bound your reads, log your decisions — are exactly what keep C security tooling from becoming a vulnerability itself. A parser that reads past its buffer is not a defense tool; it is a new bug.

Core concepts

1. What nmap -oX output actually is

Definition. nmap -oX report.xml tells nmap to write scan results as XML (-oX = output XML). The file is regular, mostly-nested tags describing hosts, their ports, and each port's state.

Plain explanation. To your C program it is not "XML" — it is one long char array ending in \0. Every tag, attribute, and value is just bytes you can search.

How it works. A trimmed example:

<host><address addr="10.0.0.5"/><ports>
  <port portid="22"><state state="open"/></port>
  <port portid="23"><state state="filtered"/></port>
  <port portid="443"><state state="open"/></port>
</ports></host>

Each <port ...> element wraps a <state state="..."/> whose value is open, closed, or filtered.

When / when not. Text-walking is fine for a small, trusted-source, regular file like this. It is not fine for untrusted, deeply-nested, or attacker-controlled XML — there you use a hardened, well-maintained XML parser configured to reject external entities (to avoid XXE attacks).

Pitfall. Assuming the format is prettified with newlines and indentation. Real nmap output is often one giant line. Never rely on line breaks as delimiters.

2. Token-walk parsing with a bounded look-ahead

Definition. Repeatedly find the next <port with strstr, then look within that element only for the open-state marker.

Plain explanation. You hop from one <port to the next. For each hop, you decide the element boundary is where the next <port begins. You only search for state="open" before that boundary.

How it works. For port N, limit = start of port N+1 (or end of buffer). If state="open" appears before limit, port N is open.

When / when not. Use a bounded look-ahead whenever you match a marker that could also appear later in the file. Skip the bound only if the marker can appear at most once — which is rarely guaranteed.

Pitfall. An unbounded strstr(p, "state=\"open\"") will happily reach forward into the next port and over-count. Port 23 (filtered) could be counted as open because port 443 (open) sits right after it. Bounding fixes this.

3. Defensive parsing: never trust, never crash

Definition. Treat every input as possibly malformed, truncated, or hostile, and degrade gracefully.

Plain explanation. Count what you can parse, skip what you cannot, and return a number rather than crashing. A defensive tool that segfaults on a truncated report is worse than useless during an incident.

How it works. Check every pointer strstr/strchr returns before dereferencing. Stop cleanly at the NUL terminator. Never write to the input.

When / when not. Always, for security tooling. The only time you "fail hard" is when a silent miscount would be more dangerous than an error — and even then you log and exit, you do not crash.

Pitfall. Using strchr(p, '>') and dereferencing the result without checking for NULL. A truncated file with no closing > returns NULL, and *NULL is undefined behavior (likely a crash).

Threat model

            AUTHORIZED LAB / OWNED NETWORK
  +--------------------------------------------------+
  |                                                  |
  |   [earlier authorized scan]                      |
  |        nmap -oX  --->  report.xml (on disk)       |
  |                            |                     |
  | == trust boundary: file from disk (untrusted) == |
  |                            v                     |
  |   ENTRY POINT: your C parser reads the bytes     |
  |        - bounds every read                        |
  |        - never opens a socket                      |
  |        - logs: file, open-port count, timestamp   |
  |                            |                     |
  |                            v                     |
  |   ASSET PROTECTED: the host attack surface        |
  |   (compare open ports vs approved baseline)       |
  |                                                  |
  +--------------------------------------------------+

  Outside the box: real targets, third-party hosts — OUT OF SCOPE.
  The parser touches only a local file, never the network.

Knowledge check.

  1. What asset is this parser ultimately protecting, and what does an unexpected open port mean for it?
  2. Where is the trust boundary in the diagram, and why is a file read from disk treated as untrusted even though you ran the scan?
  3. Which insecure assumption makes an unbounded state="open" look-ahead over-count, and why is analysis like this only ever run against systems you own or are authorized to scan?

Syntax notes

The whole parser rests on three standard C string functions from <string.h>, plus careful NULL checks.

#include <string.h>

/* strstr: find first occurrence of a substring, or NULL if absent. */
const char *hit = strstr(haystack, "<port");   /* -> points at '<' of "<port", or NULL */

/* strchr: find first occurrence of a single char, or NULL. */
const char *gt  = strchr(hit, '>');            /* -> the '>' ending the tag, or NULL */

/* Bounded look-ahead: only trust a match that lands BEFORE the limit. */
const char *next  = strstr(hit + 1, "<port");         /* start of the NEXT port element */
const char *limit = next ? next : hit + strlen(hit);  /* next port, or end of buffer */
const char *state = strstr(hit, "state=\"open\"");     /* the open marker, if any */
if (state != NULL && state < limit) {
    /* this port element is open */
}

Key points:

  • Every returned pointer can be NULL. Check before you dereference or compare-order.
  • state < limit is the guard that keeps the next port's data out of this port's decision.
  • Advance p past the current tag each loop so you make progress and eventually hit the terminating NULL from strstr.

Lesson

Why this matters

Defenders read scan output far more often than they run scans themselves.

When the SOC team asks "which of our hosts have port 23 open?", the answer is one tiny parser away. (A SOC is a Security Operations Center — the team that monitors and responds to threats.)

This lesson teaches the parsing side. You take the bytes that nmap saved to disk and pull out the answer your team actually needs.

The file is bundled in the harness. The function never opens a socket.

What the file looks like

Here is a trimmed nmap -oX dump (the -oX flag tells nmap to write its results as XML):

<host><address addr="127.0.0.1"/><ports>
  <port portid="22"><state state="open"/></port>
  <port portid="80"><state state="filtered"/></port>
  <port portid="443"><state state="open"/></port>
</ports></host>

You don't need a real XML parser. Walking from one <port to the next with strstr is enough. The format is regular and the buffer is small.

Your job

Write a function that returns the count of <port> elements whose nested <state> has state="open".

Skip everything that is filtered or closed.

Code example

int count_open_ports(const char *xml) {
    int count = 0;
    const char *p = xml;
    while ((p = strstr(p, "<port")) != NULL) {
        const char *end = strchr(p, '>');
        const char *next = strstr(p + 1, "<port");
        const char *limit = next ? next : p + 256;
        const char *state = strstr(p, "state=\"open\"");
        if (state && state < limit) count++;
        if (!end) break;
        p = end + 1;
    }
    return count;
}

Common mistakes

  • Counting state="open|filtered" as open. nmap uses that exact string — match it whole.
  • Walking past the buffer end. The buffer is NUL-terminated, so check for it.
  • Treating malformed XML as a fatal error. Return what you've counted so far. Never crash on bad input.

What this is NOT

This is not a live nmap runner.

The bytes you read came from a previous, authorised scan. The function never opens a socket and never makes a DNS query.

Code examples

The task itself (counting open ports in a saved, authorized report) is defensive. To meet the security standard we show the classic INSECURE parser first (the naive one that over-counts and can crash), then the SECURE fix, then checks that prove the fix rejects bad input and accepts good input.

(1) WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.

/* insecure_count.c
 * WARNING: intentionally vulnerable — use only in a local, isolated,
 * authorized lab. Do not deploy.
 * Bug A: unbounded look-ahead reaches into the NEXT port -> over-counts.
 * Bug B: dereferences strchr result without a NULL check -> can crash
 *        (undefined behavior) on a truncated file with no '>'.
 */
#include <string.h>

int count_open_ports_insecure(const char *xml) {
    int count = 0;
    const char *p = xml;
    while ((p = strstr(p, "<port")) != NULL) {
        /* Bug A: no limit — this match may belong to a LATER port. */
        if (strstr(p, "state=\"open\"") != NULL) count++;
        const char *end = strchr(p, '>');
        /* Bug B: if end == NULL, end + 1 is undefined; the next loop
         * dereferences garbage. */
        p = end + 1;
    }
    return count;
}

Why it is wrong: for a filtered port immediately followed by an open port, the unbounded strstr sees the later state="open" and counts the filtered port too. And a truncated report with no > makes end NULL, so end + 1 is undefined behavior.

(2) SECURE fix

/* secure_count.c — bounded, NULL-checked, crash-resistant. */
#include <string.h>

int count_open_ports(const char *xml) {
    if (xml == NULL) return 0;              /* tolerate a NULL buffer */

    int count = 0;
    const char *p = xml;

    while ((p = strstr(p, "<port")) != NULL) {
        /* Boundary = start of the next port element, or end of buffer. */
        const char *next  = strstr(p + 1, "<port");
        const char *limit = next ? next : (p + strlen(p));

        /* Only count an open marker that falls INSIDE this element. */
        const char *state = strstr(p, "state=\"open\"");
        if (state != NULL && state < limit) count++;

        /* Advance safely: to the next port if any, else stop. */
        if (next == NULL) break;
        p = next;
    }
    return count;
}

(3) VERIFY — prove the fix rejects bad input and accepts good input

/* verify.c — build: cc -std=c11 -Wall -Wextra -fsanitize=address \
 *                     secure_count.c verify.c -o verify && ./verify
 * (ASan flags any out-of-bounds read the parser might make.)
 */
#include <stdio.h>
#include <assert.h>

int count_open_ports(const char *xml);   /* from secure_count.c */

int main(void) {
    /* GOOD input: 2 open, 1 filtered -> expect 2. */
    const char *good =
        "<host><ports>"
        "<port portid=\"22\"><state state=\"open\"/></port>"
        "<port portid=\"23\"><state state=\"filtered\"/></port>"
        "<port portid=\"443\"><state state=\"open\"/></port>"
        "</ports></host>";

    /* The over-count trap: filtered port directly before an open one.
     * The insecure parser reports 2 here; the secure one reports 1. */
    const char *trap =
        "<port portid=\"23\"><state state=\"filtered\"/></port>"
        "<port portid=\"443\"><state state=\"open\"/></port>";

    /* BAD input: the open marker is present but the tag is truncated
     * (no closing '>' and no next port). Must count it without crashing. */
    const char *truncated = "<port portid=\"22\"><state state=\"open\"/";

    const char *empty = "";

    int g = count_open_ports(good);
    int t = count_open_ports(trap);
    int r = count_open_ports(truncated);
    int e = count_open_ports(empty);
    int n = count_open_ports(NULL);

    printf("good=%d trap=%d truncated=%d empty=%d null=%d\n", g, t, r, e, n);

    assert(g == 2);   /* accepts good input, correct count */
    assert(t == 1);   /* rejects the over-count: only the open port */
    assert(r == 1);   /* truncated but the open marker is present; no crash */
    assert(e == 0);   /* empty is fine */
    assert(n == 0);   /* NULL is fine */

    puts("all checks passed");
    return 0;
}

Expected output:

good=2 trap=1 truncated=1 empty=0 null=0
all checks passed

The assert(t == 1) line is the heart of the fix: it fails against the insecure parser (which returns 2) and passes against the secure one, proving the bounded look-ahead actually rejects the over-count. Running under AddressSanitizer (-fsanitize=address) additionally proves the truncated input causes no out-of-bounds read.

Line by line

Walking the secure count_open_ports against the good fixture (2 open, 1 filtered):

  1. if (xml == NULL) return 0; — guards the NULL case up front so the loop never dereferences a null pointer.
  2. p = xml; — start at the beginning of the buffer.
  3. while ((p = strstr(p, "<port")) != NULL) — find the next <port. First iteration lands on port 22. When no more <port exists, strstr returns NULL and the loop ends.
  4. next = strstr(p + 1, "<port") — search from just after the current < for the following port. For port 22 this points at port 23.
  5. limit = next ? next : (p + strlen(p)) — the element boundary. If there is no next port, the limit is the end of the buffer.
  6. state = strstr(p, "state=\"open\"") — find the open marker anywhere from here on.
  7. if (state != NULL && state < limit) count++; — only count if the marker exists and falls before the next port. For port 22 the marker is inside its own element, so count becomes 1.
  8. if (next == NULL) break; p = next; — advance to the next port, or stop.

Trace table:

Iter p at next at open marker before limit? count
1 port 22 port 23 yes (its own open) 1
2 port 23 port 443 no (its state is filtered) 1
3 port 443 NULL yes (its own open) 2
loop ends (next == NULL) 2

The trap fixture (filtered directly before open) is where the bound earns its keep: at the filtered port, limit is the start of the open port, and the open marker lands at or after limit, so state < limit is false and it is correctly not counted.

Common mistakes

Mistake 1 — Unbounded look-ahead.

  • WRONG: if (strstr(p, "state=\"open\"")) count++; with no limit.
  • WHY WRONG: the search runs to end-of-buffer, so a filtered port can be counted as open because a later port is open.
  • CORRECTED: compute limit = next <port (or end), and require state < limit.
  • RECOGNIZE / PREVENT: keep the trap test (filtered immediately before open) in your test set; it fails loudly when the bound is missing.

Mistake 2 — Dereferencing an unchecked strchr/strstr result.

  • WRONG: const char *end = strchr(p, '>'); p = end + 1;
  • WHY WRONG: on a truncated tag end is NULL; end + 1 and the next dereference are undefined behavior.
  • CORRECTED: advance using the already-checked next pointer, or explicitly if (end == NULL) break;.
  • RECOGNIZE / PREVENT: build with -fsanitize=address and feed truncated input; ASan reports the bad read immediately.

Mistake 3 — Matching the value too loosely.

  • WRONG: counting state="open|filtered" (a script-scan combined string) or matching just open as a substring of openfiltered.
  • WHY WRONG: over-counts states that are not simply open.
  • CORRECTED: match the exact token state="open" (including quotes), which nmap emits for a plain open port.
  • RECOGNIZE / PREVENT: add fixtures containing filtered and closed; assert they are not counted.

Mistake 4 — Assuming pretty-printed input.

  • WRONG: splitting on \n and assuming one port per line.
  • WHY WRONG: real nmap XML is often a single line; your loop misses ports.
  • CORRECTED: walk by <port markers, not by lines.
  • RECOGNIZE / PREVENT: test with the fixture collapsed onto one line.

Mistake 5 — Writing to the input buffer.

  • WRONG: null-terminating the buffer mid-parse to "cut" an element.
  • WHY WRONG: mutating shared/const input corrupts later reads and may be a const violation (UB).
  • CORRECTED: keep the buffer const; use pointer limits instead of mutation.

Debugging tips

Common failures and how to chase them:

  • Count is too high. Suspect an unbounded look-ahead. Print p - xml, next - xml, and state - xml each iteration; if state is at or past next, your bound is missing or wrong.
  • Count is too low. You may be advancing p past a real port. Confirm you advance to next (start of the next <port), not past it. Also check you are not accidentally matching <ports (the container) as if it were <portstrstr("<port") matches <ports too, so make sure your logic only reacts to the state marker, which the container lacks.
  • Segfault / ASan report on some files. Almost always an unchecked pointer. Rebuild with cc -std=c11 -Wall -Wextra -fsanitize=address,undefined and rerun; the sanitizer prints the exact line and offset of the bad read.
  • Works on your fixture, wrong on real output. Save a real authorized-lab scan with nmap -oX out.xml <your-own-lab-host> and diff your count against grep -c 'state="open"' out.xml as a sanity cross-check (this grep is a rough oracle, not ground truth — it ignores element boundaries).

Questions to ask when it fails:

  1. Did every strstr/strchr return get NULL-checked before use?
  2. Is my limit truly the start of the next element, and am I comparing state < limit?
  3. Am I making forward progress every loop so I eventually reach the terminating NULL?
  4. Am I matching the exact token, quotes included, and rejecting filtered/closed?

Memory safety

This section covers both the C memory-safety angle and the security detection/logging angle, since this is a C security lesson.

Memory / UB safety for this parser.

  • Never dereference or do pointer arithmetic on a strstr/strchr result before checking it for NULL. NULL + 1 and *NULL are undefined behavior.
  • The input is const char * — treat it read-only; do not null-terminate or edit it in place.
  • Rely on the buffer's NUL terminator to stop strstr; do not assume a fixed length. If you do take a length, prefer bounded scanning and never read at or past buf[len].
  • Build every security tool with -Wall -Wextra -fsanitize=address,undefined during development. For a parser fed by files, also fuzz it (e.g. libFuzzer/AFL++) with truncated and garbage inputs; parsers are a classic memory-bug surface.

Security & safety — detection and logging. When this parser runs as part of triage, log enough to audit the decision, and nothing sensitive.

  • Log: timestamp (UTC); source (the report filename or scan job id); resource examined (host/IP from the report, which is your own lab/owned asset); result (open-port count, and which ports); the security decision ("3 open ports; 1 not on approved baseline"); and a correlation id tying this run to the scan job and the analyst.
  • Never log: credentials, API tokens or session cookies, private keys, full payloads that may contain secrets, or unneeded PII. Scan reports are usually low-sensitivity, but treat internal host inventories as confidential and store logs access-controlled.
  • Events that signal abuse / concern: a host showing a port that is not on the approved baseline; a management port (e.g. 23 Telnet, 3389 RDP, database ports) newly open; a large jump in open-port count between scans; or a report whose host is outside your authorized scope (which means someone scanned something they should not have — investigate the process, not just the host).
  • False positives arise when: a port is legitimately open but not yet added to the baseline; a service was intentionally exposed for a maintenance window; or the report is stale. Reduce them by diffing against a maintained, dated baseline and by recording an expiry on temporary exceptions.

Real-world uses

Concrete authorized use case. A blue team runs a nightly internal nmap scan of its own subnet, saving -oX reports. Each morning a small C tool parses every report, counts open ports per host, and diffs them against an approved baseline file. Any host with a port not on the baseline raises a ticket. No socket is opened by the parser; it only reads yesterday's authorized scan output.

Professional best-practice habits.

  • Validation: treat every report as untrusted input; bound all reads; reject/skip malformed elements rather than crashing.
  • Least privilege: the parser needs read-only access to report files and nothing else — no network, no write access to the reports.
  • Secure defaults: fail closed on ambiguity (log and flag for human review) rather than silently reporting "0 open."
  • Logging: record the decision and a correlation id (see the safety section); keep logs access-controlled.
  • Error handling: distinguish "file missing/unreadable" (operational error, exit non-zero, alert) from "malformed element" (skip and continue).

Beginner vs advanced.

Beginner Advanced
Parsing strstr token-walk on a small fixture Hardened XML parser (e.g. libxml2) with external entities disabled to prevent XXE
Scope Count open ports Correlate ports to services/versions and to a CVE/inventory feed
Trust Assume report is from your own scan Verify report provenance and integrity before ingesting
Output A number to the console Diff-against-baseline, ticketing, and dashboards

Note two misconceptions to avoid: a report that shows all ports closed does not prove the host is secure, and passing an automated scan does not prove a system is secure — scanners see only what they probe, when they probe it. Never describe any host as "completely secure."

Practice tasks

All tasks are lab-only: run them against fixtures you create or against scans of hosts you own or are explicitly authorized to scan (localhost, a container, or an intentionally-vulnerable practice VM). Authorization checklist before any live scan: (1) you own the host or hold written authorization; (2) the target is in an isolated lab network; (3) you have a defined scope and time window; (4) you know how to reset/clean the lab. Cleanup/reset: delete generated *.xml reports and stop/remove any lab container when done. Every task ends defensively — remediate and verify.

Beginner 1 — Count filtered ports.

  • Objective: add int count_filtered_ports(const char *xml) alongside the open-port counter.
  • Requirements: reuse the bounded look-ahead; match state="filtered".
  • Input/Output: the good fixture -> 1.
  • Constraints: no writes to the buffer; NULL-check every pointer.
  • Hints: copy the open-port function and change only the marker string.
  • Concepts: token-walk, bounded look-ahead.

Beginner 2 — Reject the container tag.

  • Objective: prove your parser is not fooled by the <ports> wrapper.
  • Requirements: add a fixture that contains <ports> but zero <port ...> elements; assert the count is 0.
  • Input/Output: "<host><ports></ports></host>" -> 0.
  • Constraints: do not special-case by hard-coding lengths.
  • Hints: think about what distinguishes a real port element (it has a state marker) from the container.
  • Concepts: exact-match discipline, test design.

Intermediate 1 — Ignore XML comments.

  • Objective: make the parser skip anything inside <!-- ... --> so a commented-out state="open" is not counted.
  • Requirements: when you encounter <!--, jump p past the matching --> before continuing; bound safely if --> is missing.
  • Input/Output: a fixture with one real open port and one commented state="open" -> 1.
  • Constraints: never read past the NUL terminator; handle an unterminated comment without crashing.
  • Hints: strstr(p, "-->") returns NULL if the comment is unterminated — decide what to do (stop safely).
  • Concepts: skipping regions, defensive bounds.

Intermediate 2 — Report open ports per host with baseline diff.

  • Objective: for each <host>, extract its addr and count open ports, then flag hosts whose open set differs from an approved baseline.
  • Requirements: walk <host markers as the outer boundary; within each host, count open ports; compare to a small in-code baseline map; print flagged hosts.
  • Input/Output: given two hosts and a baseline, print only the host whose open ports are not all on the baseline.
  • Constraints: read-only input; log the decision (host, open count, flagged?) with a timestamp.
  • Hints: nest the port loop inside a host loop, using the next <host as the outer limit.
  • Concepts: nested bounded parsing, baseline comparison, logging. Defensive conclusion: the flagged port is the finding — remediate by closing/justifying it, then re-run and verify the flag clears.

Challenge — Fuzz-harden the parser.

  • Objective: make count_open_ports provably crash-free on arbitrary bytes.
  • Requirements: write a fuzz target that feeds random and truncated buffers (via libFuzzer or a simple loop) and run under -fsanitize=address,undefined; fix any crash or UB found.
  • Input/Output: thousands of random inputs -> zero sanitizer reports; known-good fixtures still return correct counts.
  • Constraints: keep the buffer const; no reads at or past the terminator; document each fix.
  • Hints: the usual culprits are unchecked pointers and off-by-one advances; add a NULL/length guard at the top.
  • Concepts: fuzzing, sanitizers, defensive bounds. Defensive conclusion: a parser you cannot crash is one you can safely point at real (authorized) scan output; verify by re-running the full test set after each fix.

Summary

Main concepts. A saved nmap -oX report is just a NUL-terminated char buffer. You count open ports by hopping from one <port to the next with strstr and, for each element, checking whether state="open" appears before the next port begins — a bounded look-ahead. The asset you protect is your host attack surface; the finding is an unexpected open port measured against a baseline. The parser reads a local file only and never touches the network.

Key syntax/commands. strstr(hay, needle) and strchr(str, ch) return a pointer or NULL; always NULL-check. The guard state != NULL && state < limit with limit = next ? next : end-of-buffer is what prevents over-counting. Build defensively with cc -std=c11 -Wall -Wextra -fsanitize=address,undefined.

Common mistakes. Unbounded look-ahead (over-counts a filtered port before an open one); dereferencing an unchecked pointer on truncated input (undefined behavior); loose matching that counts filtered/closed; assuming pretty-printed, one-port-per-line input; and mutating the const buffer.

What to remember. Never trust input, bound every read, match the exact token, and log the decision (timestamp, source, host, open-port count, correlation id) while logging no secrets. Only run scans against systems you own or are authorized to test, and remember: closed ports and passed scanners do not prove a host is secure — nothing is ever "completely secure."

Practice with these exercises