CLA (C language) study materials
CLA (C language) study resources

Nested strtok and a Dangling Field Pointer

CLA (C language) · Put your knowledge to work in context.

All case studies

Nested strtok and a Dangling Field Pointer

A configuration loader reads one line at a time into char line[128] with fgets, removes the trailing newline, then calls strtok(line, ";") to walk semicolon-separated fields. For each field it calls parse_pair(field), which calls strtok(field, "=") to split the key from the value. With the input mode=fast;retries=3;host=db1, the program prints only mode=fast; later fields are skipped or show an empty value. A unit test calls parse_line("mode=fast;retries=3") with a string literal and crashes on some builds. parse_pair also returns a pointer to a local char value[16] that it filled with the value; callers sometimes print garbage after the next call. The build uses -std=c11 -Wall, but the warnings were not treated as errors. The loader must also preserve empty fields in inputs such as a=1;;b=2.

1. With `mode=fast;retries=3;host=db1`, why does the program process only the first field? Explain `strtok` state and give a corrected parsing structure.

Read the worked answer

strtok is not reentrant: it stores the position of the next token in a single internal static pointer. The outer loop calls strtok(line, ";"), which sets that state to line. Inside the loop, parse_pair calls strtok(field, "="). That call resets the same static state to field, returns mode, then returns fast; the outer strtok(NULL, ";") now continues from the inner parser's end position, so it returns NULL and the loop stops. The remaining fields are never visited. Fix by using strtok_r with two independent save pointers:

char *outer = NULL, *inner = NULL;
for (char *f = strtok_r(line, ";", &outer); f != NULL;
     f = strtok_r(NULL, ";", &outer)) {
    char *key = strtok_r(f, "=", &inner);
    char *val = strtok_r(NULL, "=", &inner);
    /* ... */
}

Alternatively, copy each field before calling strtok in the helper, or parse manually with strchr. strtok_r is POSIX, not ISO C, so for strict C11 provide your own scanner or use strchr.

2. `parse_line("mode=fast;retries=3")` crashes on some builds, and `parse_pair` returns a pointer to a local `char value[16]` that later prints garbage. Explain both defects and propose one API change that fixes them.

Read the worked answer

A string literal has static storage duration and may be placed in read-only memory. strtok modifies its input by writing '\0' over delimiters. Passing the literal directly to parse_line and then to strtok writes to read-only memory: undefined behavior, which may crash or appear to work. The function must accept const char * and copy into a local mutable buffer, or the caller must pass a writable array. The second defect is lifetime: value is an automatic array local to parse_pair; its lifetime ends when the function returns. Returning value yields a dangling pointer, and the caller reads whatever later occupies the stack slot. Change the API to have the caller own the storage, for example int parse_pair(char *field, char *key, size_t key_size, char *value, size_t value_size), copying each part with bounded copies. Then parse_line can copy the literal into char line[128] first. If allocation is used instead, document that the caller must free it.

3. The input can contain empty fields, such as `a=1;;b=2`, and the loader must preserve an empty value. How does `strtok` behave here, and what parsing approach would you use?

Read the worked answer

strtok treats the delimiter set as a whole and skips runs of delimiters; it does not return empty tokens. For a=1;;b=2, after a=1 it skips both semicolons and returns b=2, so the empty field is lost. If empty fields are meaningful, do not use strtok. Scan with strchr for the next ;, treat the bytes between the current position and that delimiter as a field (length zero is a valid empty field), then advance past the delimiter. For the key/value split, use the first = and allow an empty value after it. This also avoids the shared static state and lets you parse const char * input if you copy fields into bounded buffers. Keep the line buffer mutable only if you choose to insert terminators; a length-based scanner does not need to modify the input.