[RFC] scanf: add non-allocating bounded string input via %!s / %![...] / %!c (size before pointer)

Yair Lenga yair.lenga@gmail.com
Fri Oct 17 13:20:30 GMT 2025


Hello glibc maintainers,

I’d like to propose a small, backward-compatible GNU extension to the scanf
family that provides a non-allocating, bounds-aware alternative to %s/%[ and
 complements the existing %m (mallocing) extension.

Problem
-------
The %s and %[ conversions read unbounded input unless a numeric width is
manually supplied. In practice, many call sites omit it; this leads to
overflows that Fortify can only catch when the buffer size is visible to
the compiler. There is no portable, non-allocating way to make these reads
explicitly bounded. GNU %m exists, but it always allocates; Annex K scanf_s
requires a size argument but is not implemented in glibc.

Proposal (minimal, no modifiers)
--------------------------------
Introduce a new flag character '!' under _GNU_SOURCE for scanf’s input
conversions, with *no* other modifiers permitted:

    %!s       expects: size_t bufsz, char *buf
    %![...]   expects: size_t bufsz, char *buf
    %!c       expects: size_t n,     char *buf

Rules and semantics:
* The size argument comes **before** the destination pointer (mirrors
printf "%*s").
* No other format decorations are allowed with '!':
  - **no** numeric field width digits,
  - **no** length modifiers (h, hh, l, ll, L, j, z, t),
  - **no** positional "$".
  If present, the conversion is a format error (treated as a match failure).
* Mutually exclusive with assignment suppression and %m:
  - "%*!s" and "%!ms" are invalid (match failure).
* Behavior:
  - %!s / %![...]: read at most (bufsz - 1) bytes, then NUL-terminate if any
    byte was stored. On longer input, **truncate and succeed**, leaving
excess
    unread (like %Ns/fgets).
  - %!c: read exactly n bytes (including whitespace). If fewer than n are
    available, the conversion fails.
  - Whitespace handling is identical to the standard conversions:
      %!s / %![...] skip leading whitespace; %!c does not (use " %!c" to
skip).
  - Each %!… counts as **one** successful assignment.
* Errors:
  - bufsz == 0 (or n == 0 for %!c) is an immediate match failure; no write.
  - (Optional, if desired) errno = EINVAL on invalid combinations.

Examples
--------
1) Bounded word, non-allocating (truncates safely):
   char name[64];
   int age;
   /* Input: "alexanderthegreat 32\n" -> name=="alexanderthegreat" (or
truncated), age==32 */
   scanf("%!s %d", sizeof name, name, &age);

2) Bounded scanset (letters and spaces only), no width digits to juggle:
   char title[16];
   /* Reads up to sizeof(title)-1 letters/spaces, NUL-terminated; leaves
excess unread */
   scanf("%![A-Za-z ]", sizeof title, title);

3) Raw bytes including whitespace (exact count):
   char hdr[8];
   /* Reads exactly 8 bytes (may include '\n' or '\0'); fails if fewer
available */
   scanf("%!c", (size_t)8, hdr);

4) Skipping leading whitespace before a single character (same rule as %c):
   char ch;
   scanf(" %!c", (size_t)1, &ch);  /* consumes one non-whitespace byte */

5) Using sscanf for controlled inputs (unit tests, fmemopen, etc.):
   char tok[5];
   int rc = sscanf("ABCDE", "%!s", sizeof tok, tok);  /* rc==1, tok=="ABCD"
*/

Rationale
---------
* Closes the long-standing safety gap for %s/%[ without introducing new
APIs.
* Backward-compatible: "%!…" is currently invalid, so existing programs are
unaffected.
* Simple to implement and test: a small change in vfscanf.c; no interaction
with
  widths or length modifiers in this initial version.
* Symmetric with printf’s width-from-argument pattern (size before pointer),
  easy for developers to remember.
* Complements %m: "%!…" is the non-allocating counterpart to "%m…".
* Enables stronger diagnostics in compilers: GCC/Clang can type-check that
  %!s/%![ expect (size_t, char*) and warn on bare %s/%[ when formats are
literals.

Testing & documentation
-----------------------
I can provide:
* Tests in stdio-common/ that cover: truncation + NUL, scanset behavior, %!c
  raw reads including whitespace, invalid combinations, and zero size.
* Manual updates (manual/stdio.texi) under “GNU extensions”.
* A small prototype patch (~70–110 LOC in vfscanf.c) with clear comments.

Open questions (seeking guidance)
---------------------------------
* Truncation signaling: as specified above, truncation still counts as
success
  (consistent with %Ns/fgets). If preferred, we can also set errno = ERANGE.
* Locale/multibyte: intention is to match existing %s/%[/%c semantics
exactly.

If there is interest in this minimal form, I will prepare a prototype patch
and
tests for review. I’m also happy to maintain this extension after merge
(address
regressions, keep tests current, and handle doc updates).

Thank you for considering,
Yair Lenga
yair.lenga@gmail.com
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://sourceware.org/pipermail/libc-alpha/attachments/20251017/5feab49f/attachment-0001.htm>


More information about the Libc-alpha mailing list