Proven C Book한국어 GitHub

43 Safe input — blocking overflow, handling failure

What to know first

chapter 25, Input · the danger of input
chapter 42, Strings · a string only marks its end

Looking back

Chapter 42′s gets was expelled for not taking the container’s size, and chapter 25′s fgets is safe because it takes one. Then does “taking the size” alone solve the whole safety problem of input?

A. Only half of it. Passing the size blocks overflow, but input has a second problem — the content cannot be trusted (chapter 25). You expect a number and letters arrive; you expect a short line and a long one arrives; and a malicious counterpart deliberately crafts input aimed at the boundary (chapter 22′s format-string attack was a taste).

Safe input is the sum of [blocking overflow + handling failure explicitly]. Chapters 25 and 42 built the first half, so this chapter builds the second.

The need for this chapter, and its context

The danger flagged in chapter 25 is finally settled. The reason for waiting eighteen chapters is plain: to explain overflow you need array bounds (chapter 38), and to explain truncation you need NUL termination (chapter 42). Taught unprepared, “safe input” is a list of rules; taught here, it is a discipline whose reasons are visible.

By the end of this chapter

Chapter 25 foreshadowed “why safe input is difficult”, and chapters 35 and 42 taught the roots of that danger (the boundary, NUL termination). This chapter finishes the dissection of the accident and gathers how to handle a failed parse into five disciplines. They can be built in standard C alone, and the libraries met later stand on them.

The questions this chapter answers

  1. Does that mean the standard library is not enough?

43.1 Dissecting the accident — the epidemic called the boundary violation

Combine chapter 38′s boundary rule with chapter 42′s NUL termination and the most expensive accident pattern in C’s history assembles itself. Input longer than the container overwrites neighbouring memory (a boundary violation) — and if what was overwritten happens to be the return ledger of a function call (whose identity we see in the next chapter), an attacker seizes the program’s flow of execution with input alone. That is the substance of the buffer overflow attack, an epidemic running unbroken from the Morris worm (1988, chapter 42) to today’s security advisories. The statistics are lopsided too — in the major security bodies’ lists of “most dangerous software weaknesses”, the boundary-violation family has been a fixture at the top for decades.

Because C decided not to check bounds at run time (chapter 38), the defence has to be built in layers — functions that take a size (fgets), warnings and sanitizers (chapter 17), and the discipline of the parsing code itself. That last layer is this chapter’s subject.

43.2 How do the standard tools report failure

First, gather how the tools already in hand announce a failure. That they are all different is itself this chapter’s starting point.

FunctionHow you know it succeededWhere it catches you
fgetsIt returns a non-null pointerWhether the line was cut must be checked separately — by looking for \n at the end
scanf, sscanfIt returns how many conversions succeededYou do not know where it stopped. Code that ignores the count is common
The strtol familyYou look at endptr and errno togetherThree things must be read together — the return value, endptr and errno
atoiThere is no way to knowFailure is indistinguishable from “read a zero”. Not used

Table 43.1

The strtol family has the soundest contract of these, but using it correctly means checking three things at once — which is why practice does not call it directly but wraps it. That wrapping is this chapter’s five disciplines.

43.3 Five disciplines for handling a failed parse

examples/ch43/parse.c

/* 입력 해석의 실패를 다루는 다섯 규율 — 표준 C 만으로. */
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 실패를 '값'으로 돌려주는 결과 꾸러미.
   성공 여부와 값을 갈라 담고, 어디까지 읽었는지도 함께 돌려준다. */
struct parse_i64 {
    bool        ok;
    long long   value;
    const char *rest;      /* 해석이 멈춘 자리 — 이어 읽거나 오류를 가리킬 때 */
    const char *why;       /* 실패한 이유(사람이 읽는 말) */
};

/* [[nodiscard]] 는 "이 반환값을 버리면 경고하라"는 C23 의 표기다.
   실패 확인을 잊는 실수를 컴파일러가 잡아 준다. */
[[nodiscard]]
static struct parse_i64 parse_int(const char *text)
{
    struct parse_i64 r = { .ok = false, .value = 0, .rest = text, .why = "" };
    if (!text) { r.why = "empty input"; return r; }

    errno = 0;
    char *end = nullptr;
    long long v = strtoll(text, &end, 10);

    if (end == text)                 { r.why = "does not start with a number";  return r; }
    if (errno == ERANGE)             { r.why = "does not fit in this type"; r.rest = end; return r; }

    r.ok = true; r.value = v; r.rest = end; r.why = "";
    return r;
}

static void show(const char *text)
{
    struct parse_i64 r = parse_int(text);
    if (r.ok)
        printf("  %-24s -> ok: %lld, input left: \"%s\"\n",
               text, r.value, r.rest);
    else
        printf("  %-24s -> failed: %s\n", text, r.why);
}

/* 잘림을 오류로 다루는 복사 — "잘렸지만 성공"을 남기지 않는다 */
[[nodiscard]]
static bool copy_line(char *dst, size_t cap, const char *src)
{
    size_t n = strlen(src);
    if (n + 1 > cap) return false;          /* 잘릴 상황이면 아예 실패로 */
    memcpy(dst, src, n + 1);
    return true;
}

int main(void)
{
    puts("[1: return the failure as a value - success and value kept apart]");
    show("42");
    show("  42 and then some");
    show("forty-two");
    show("999999999999999999999999");

    puts("\n[2: say how far you read]");
    const char *csv = "10,20,30";
    long long sum = 0;
    const char *p = csv;
    for (;;) {
        struct parse_i64 r = parse_int(p);
        if (!r.ok) break;
        sum += r.value;
        p = r.rest;
        if (*p == ',') p++;
        else break;
    }
    printf("  sum of \"%s\" = %lld  <- rest lets you keep reading\n", csv, sum);

    puts("\n[3: truncation does not count as success]");
    char small[8];
    printf("  \"hello\"%s\n",
           copy_line(small, sizeof small, "hello") ? "ok" : "failed (truncated)");
    printf("  \"hello, world\"%s\n",
           copy_line(small, sizeof small, "hello, world") ? "ok" : "failed (truncated)");

    puts("\n[4: on failure, leave the output argument alone]");
    long long keep = -1;
    struct parse_i64 bad = parse_int("not a number");
    if (bad.ok) keep = bad.value;
    printf("  is the original value still there after the failure: %s (keep = %lld)\n",
           keep == -1 ? "yes" : "no", keep);

    puts("\n[5: let the compiler speak when you forget to check]");
    puts("  parse_int and copy_line are marked [[nodiscard]].");
    puts("  dropping the return value warns - that stops you forgetting to check for failure.");
    return 0;
}

Output

[1: return the failure as a value - success and value kept apart]
  42                       -> ok: 42, input left: ""
    42 and then some       -> ok: 42, input left: " and then some"
  forty-two                -> failed: does not start with a number
  999999999999999999999999 -> failed: does not fit in this type

[2: say how far you read]
  sum of "10,20,30" = 60  <- rest lets you keep reading

[3: truncation does not count as success]
  "hello" → ok
  "hello, world" → failed (truncated)

[4: on failure, leave the output argument alone]
  is the original value still there after the failure: yes (keep = -1)

[5: let the compiler speak when you forget to check]
  parse_int and copy_line are marked [[nodiscard]].
  dropping the return value warns - that stops you forgetting to check for failure.

The listing’s parse_int puts all five into one function. One at a time.

43.3.1 1. Return failure as a value

Return “did it succeed” and “what is the value” kept apart. Return only a value, as atoi does, and there is no room to express failure; report a count, as scanf does, and what failed is not preserved.

struct parse_i64 { bool ok; long long value; const char *rest; const char *why; };

Returning a struct by value may look costly, but a struct this size usually rides in a couple of registers (chapter 47). Writing failure into the type is worth far more than that.

43.3.2 2. Let the compiler speak when a check is forgotten

Attach C23′s [[nodiscard]] and code that throws the return value away gets a warning.

[[nodiscard]] static struct parse_i64 parse_int(const char *text);

This moves “you must check” out of the documentation and into the compiler’s job. A rule a person has to remember is eventually forgotten, and the place it is forgotten is exactly the place the accident happens.

43.3.3 3. Say how far it read

strtol’s endptr is the good precedent. Return where parsing stopped as well and two things become possible — carrying on (the listing’s 10,20,30 sum), and pointing a person at where it went wrong (“it stopped at the third character”).

43.3.4 4. Do not count truncation as success

This is the most frequently broken discipline. Return “it did not fit, so I cut it to size” as a success and what follows is looking up a file under a truncated name and connecting to a truncated address.

The listing’s copy_line does nothing and returns failure when the text will not fit. That is the lesson of the long-standing problem of strncpy not reporting truncation (chapter 64).

43.3.5 5. On failure, touch no output

If a half-filled value is left in the result on failure, code will end up using it. Make change nothing on failure the contract and write it in the documentation — the last part of the listing checks exactly that.

A common misconception. “Checking scanf’s return value is enough to be safe”

Half right. Checking the count tells you how many conversions succeeded, and nothing more; it does not substitute for disciplines 3, 4 and 5 above.

  • You cannot tell where it stopped — there is no good way to re-read the rest.
  • Give %s no width and it writes without knowing the container’s size (overflow). Always write the width, as in %9s.
  • On failure the contract for which arguments were already filled is vague.

So the practice in the field is “read a line (fgets) and parse that line yourself”. This chapter’s five disciplines are the skeleton of that “yourself”.

43.4 Casting the discipline into a component

Build the same discipline by hand in every function and something is eventually left out. So practice’s next step is to cast the discipline into a component — one where failure surfaces as a value, bounds are always checked, and the compiler speaks up when a check is forgotten.

Q. Does that mean the standard library is not enough?

A. Less that it is not enough than that you have to rebuild it every time. The standard library carries the practice of the 1970s and 80s intact (chapters 64 and 65), and it gives none of the five disciplines above as a default. So every real codebase, without exception, lays its own thin layer on top — a layer that puts a safe shell around strings, parsing and number conversion.

Make that layer a library and a whole team stands on the same discipline. This book has one such example ready — proven, written by the author, and Part XII covers its design and use. Chapter 87, “Errors are values”, takes up disciplines 1 and 2 at the library level; chapter 90, “Strings and text”, takes up 3 and 4.

What matters is not which library you use but whether the discipline is carved into the component. Choose another with the same idea, or build your own.

We have safe input. But a phrase went past in this chapter without explanation — “the return ledger of a function call”. Where in memory does what live, and when is it born and when does it die? The next chapter is that answer — lifetime and storage duration.