Why does printf specifier format %n not work?
Asked Answered
H

1

5

This is my code:

#include <stdio.h>

int main(void) {
    int n;

    fprintf(stdout, "Hello%n World\n", &n);
    fprintf(stdout, "n: %d\n", n);

    return 0;
} 

This is my output:

Hellon: 0
  1. Why does the fprintf format specifier "%n" not work?
  2. Why is the string to be printed interrupted?

ISO/IEC 9899:201x C11 - 7.21.6.1 - The fprintf function

The conversion specifiers and their meanings are:

(...)

%n The argument shall be a pointer to signed integer into which is written the number of characters written to the output stream so far by this call to fprintf. No argument is converted, but one is consumed. If the conversion specification includes any flags, a field width, or a precision, the behavior is undefined. ...

(...)

This is my compiler version used on Code::Blocks:

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=C:/Program\ Files/mingw-w64/x86_64-8.1.0-posix-seh-rt_v6-rev
0/mingw64/bin/../libexec/gcc/x86_64-w64-mingw32/8.1.0/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ../../../src/gcc-8.1.0/configure --host=x86_64-w64-mingw32 --bu
ild=x86_64-w64-mingw32 --target=x86_64-w64-mingw32 --prefix=/mingw64 --with-sysr
oot=/c/mingw810/x86_64-810-posix-seh-rt_v6-rev0/mingw64 --enable-shared --enable
-static --disable-multilib --enable-languages=c,c++,fortran,lto --enable-libstdc
xx-time=yes --enable-threads=posix --enable-libgomp --enable-libatomic --enable-
lto --enable-graphite --enable-checking=release --enable-fully-dynamic-string --
enable-version-specific-runtime-libs --disable-libstdcxx-pch --disable-libstdcxx
-debug --enable-bootstrap --disable-rpath --disable-win32-registry --disable-nls
 --disable-werror --disable-symvers --with-gnu-as --with-gnu-ld --with-arch=noco
na --with-tune=core2 --with-libiconv --with-system-zlib --with-gmp=/c/mingw810/p
rerequisites/x86_64-w64-mingw32-static --with-mpfr=/c/mingw810/prerequisites/x86
_64-w64-mingw32-static --with-mpc=/c/mingw810/prerequisites/x86_64-w64-mingw32-s
tatic --with-isl=/c/mingw810/prerequisites/x86_64-w64-mingw32-static --with-pkgv
ersion='x86_64-posix-seh-rev0, Built by MinGW-W64 project' --with-bugurl=https:/
/sourceforge.net/projects/mingw-w64 CFLAGS='-O2 -pipe -fno-ident -I/c/mingw810/x
86_64-810-posix-seh-rt_v6-rev0/mingw64/opt/include -I/c/mingw810/prerequisites/x
86_64-zlib-static/include -I/c/mingw810/prerequisites/x86_64-w64-mingw32-static/
include' CXXFLAGS='-O2 -pipe -fno-ident -I/c/mingw810/x86_64-810-posix-seh-rt_v6
-rev0/mingw64/opt/include -I/c/mingw810/prerequisites/x86_64-zlib-static/include
 -I/c/mingw810/prerequisites/x86_64-w64-mingw32-static/include' CPPFLAGS=' -I/c/
mingw810/x86_64-810-posix-seh-rt_v6-rev0/mingw64/opt/include -I/c/mingw810/prere
quisites/x86_64-zlib-static/include -I/c/mingw810/prerequisites/x86_64-w64-mingw
32-static/include' LDFLAGS='-pipe -fno-ident -L/c/mingw810/x86_64-810-posix-seh-
rt_v6-rev0/mingw64/opt/lib -L/c/mingw810/prerequisites/x86_64-zlib-static/lib -L
/c/mingw810/prerequisites/x86_64-w64-mingw32-static/lib '
Thread model: posix
gcc version 8.1.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project)
Homer answered 2/3, 2019 at 9:50 Comment(8)
You attempt to print an unititialized integer. Because it is uninitialized, the behavior is undefined. The compiler is free to eliminate the integer from your code and then "mangle" the printf call.Reuter
"The types C, n, p, and S, and the behavior of c and s with printf functions, are Microsoft extensions and are not ANSI compatible."Reuter
The relevant text from the standard for [f]printf's %n is The argument shall be a pointer to signed integer into which is written the number of characters written to the output stream so far by this call to fprintf. No argument is converted, but one is consumed. If the conversion specification includes any flags, a field width, or a precision, the behavior is undefined.Theurich
I'm not attempting to print an unitialized integer. The specifier does not work even if the variable is initialized. The% n specifier is included in the C standard and can be used by the printf or fprintf function.Homer
I'm a bit puzzled. Even ANSI C supports %n, so it's not a case of the missing -std=c99 option (gcc used to default to -std=gnu89). And a quick check shows that it should just work. Is the code in your question the same as in your file? I mean, are you sure there aren't any weird non-ASCII characters in the "Hello%n World\n" string? A wrong % or n char could be a problem even if it looks the same as the right one.Theurich
I tried to rewrite and compile the code both inside and outside the IDE but nothing changesHomer
OP is using mingw which uses (an ancient version of) the MSVC implementation of the standard library, so it's probably just MSVC being intentionally non-conforming.Mcclees
(MS is known for harboring a wrong belief that %n is unsafe and disabling it a hardening measure.)Mcclees
L
11

As documented in the Microsoft documentation, the %n is disabled by default in the Microsoft C library used on your MinGW system:

Important

Because the %n format is inherently insecure, it is disabled by default. If %n is encountered in a format string, the invalid parameter handler is invoked, as described in Parameter Validation. To enable %n support, see _set_printf_count_output.

Whether %n is actually unsafe as claimed by Microsoft is highly debatable. The examples shown to support this claim combine this printf function with the use of a variable format string that can by changed by the attacker via a buffer overflow error.

On some Microsoft systems (but maybe not the latest), you could fix your program this way:

#include <stdio.h>

int main(void) {
    int n;

    _set_printf_count_output(1);

    fprintf(stdout, "Hello%n World\n", &n);
    fprintf(stdout, "n: %d\n", n);

    return 0;
} 

For a more portable approach, here is a work around to avoid using %n and still get the same results:

#include <stdio.h>

int main(void) {
    int n;

    n = fprintf(stdout, "Hello");
    fprintf(stdout, " World\n");
    fprintf(stdout, "n: %d\n", n);

    return 0;
} 

Output:

Hello World
n: 5
Langbehn answered 2/3, 2019 at 14:21 Comment(6)
a variable format string that can by changed by the attacker via a buffer overflow error Nice blamecasting by Microsoft there. And it breaks portability. How convenient.Striker
This works on Visual Studio but if I use _set_printf_count_output(1); on MinGW I get this error: "undefined reference to '__imp_set_printf_count_output'".Homer
@AlessandroAvolio: OK, it would be a non portable solution anyway. I shall amend the answer with a better approach.Langbehn
Does mingw have a way to fix this without source changes, just by linking with right lib file? I thought they even provided an enhanced libc with all the printf bugs fixed.Mcclees
Well certainly you could link your own .o file with a ctor that calls the function in this answer, but I kinda remembered them providing a way to do that out of the box...Mcclees
@AlessandroAvolio: I suspect the cause is that mingw is using the new MSVCRT dll that lacks %n by default, but some ancient import library from before _set_printf_count_output existed.Mcclees

© 2022 - 2024 — McMap. All rights reserved.