I am investigating a format string vulnerability on arm64 (linked against musl libc), and am encountering some odd behavior while debugging the output.

From the decompilation, the program has a classic format string vulnerability that boils down to:

fprintf(stdout, user_controlled_data);

Using repetitive format specifiers (e.g. %p%p%p%p), I can dump massive swaths of memory by including thousands of these characters. That works as expected.

The problem arises when I try direct parameter access. For some reason, %1$p works but not %2$p, but 3 up to about 12 works, and everything I've tried after that fails. By "fails", I mean no values are printed, except the newline automatically added to my string earlier in code is eaten somehow. In the debugger, fprintf returns -1, and errno is set to 0x16, which I believe is EINVAL.

For this particular scenario, I need the ability to read/write a particular stack offset in the thousands. But I cannot print it to confirm since direct parameter access does not work. I can see the target parameter by using repeated characters, but I need direct parameters to work going forward due to other constraints.

Now, I understand this is in "undefined behavior" territory, but I compiled a vulnerable test binary (statically linked against Glibc, I should probably try against musl) on the system that works as expected with no issues (e.g. %9000$p prints something).

Is there something that would cause this behavior, or something I am missing? I can provide further information if needed.

1 Answer 1


After posting the question, I started to think about the difference in the libc I was using. I compiled the basic test binary again but with musl, and also saw the unexpected behavior there. This seems to be due to the printf implementation in musl:

        if (isdigit(s[1]) && s[2]=='$') {
            argpos = s[1]-'0';

It only uses direct parameters if there is exactly one digit following the %, followed by a $. This explains why I was unable to print any larger stack offsets.

Edit: This seems standard-compliant:

"%n$", where n is a decimal integer in the range [1,{NL_ARGMAX}], giving the position of the argument in the argument list.

Then, looking at the definition of NL_ARGMAX:


Maximum value of digit in calls to the printf() and scanf() functions. Minimum Acceptable Value: 9

So it appears that the implementation is indeed following the standard by allowing the minimum value of 9, which is certainly inconvenient for writing compact format string exploits.

  • 1
    The author of musl makes a point of sticking to the word of the standards, so perhaps you should consult those. It's also possible you found a defect in the code, though.
    – 0xC0000022L
    May 20, 2020 at 19:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.