HexBrain

curl · Report #3973152

37: `curl_mprintf` `%F` format specifier not consumed, causing variadic argument desynchronization

4upvotes
14 Sept 2026disclosed

How it was found

Summary

docs/libcurl/curl_mprintf.md documents %F as a conversion that consumes a double, alongside %f. However, parse_conversion() in lib/mprintf.c has no F case — it falls through to the default branch and returns without assigning an input slot. In a sequential format like "%F%s", the %s conversion is assigned the slot that the caller filled with a double. On ABIs where variadic floats and pointers share one argument area (32-bit x86), this produces a type-confused pointer fetch: %s dereferences bits from the skipped double, and %n writes through them.

Affected versions

The faulty implementation dates to curl’s custom printf introduction, approximately curl 5.4 through 8.21.0, plus tested 8.22.0 prerelease/development versions. Exploitability is ABI-dependent.

Root cause

In lib/mprintf.c, parse_conversion() (lines 329-424) maps format characters to argument types. It handles f, e, E, g, G but has no F case:

case 'f':
  type = MTYPE_DOUBLE;
  break;
// ... e, E, g, G handled ...
default:
  /* invalid instruction, disregard and continue */
  return TRUE;

The caller in parsefmt() (lines 493-494) skips all slot assignment when parse_conversion() returns TRUE:

if(parse_conversion(*fmt, &flags, &type))
  continue;

This continue bypasses the param = param_num++ increment at line 533. The next valid conversion inherits the slot that the caller's double occupies, creating a type mismatch during va_arg collection.

Security impact

On a 32-bit x86 ABI (unified variadic argument area), the skipped double's bit pattern is fetched as a pointer. This enables:

  • Controlled read: "%F%s" — %s dereferences an address encoded in the low 32 bits of the double.
  • Controlled write: "%F%n" — %n writes the output byte count to an address encoded in the double.

No format-string injection is needed; the PoC uses fixed, correctly-typed format strings per the documentation.

Constraints:

  1. Only exploitable on ABIs where variadic floats and pointers share one argument area (32-bit x86). On 64-bit x86 and ARM64, separate register classes cause %s/%n to consume the legitimate later pointer instead.
  2. The attacker must control the double argument value with enough precision to encode a useful address.
  3. A downstream application must use curl_mprintf with a sequential format containing %F before a pointer-consuming conversion.
  4. No curl-internal format string uses %F — this does not affect curl's own protocol handling.
  5. The documentation discourages new use of this API ("We discourage users from using any of these functions in new applications").

Given these constraints, the practical severity is likely Low. I have not identified an affected curl protocol path or concrete downstream application. I am reporting the confirmed type-confusion primitive for the security team to assess.

Reproduction

  1. Build an affected libcurl version for a 32-bit x86 target, or another ABI where floating-point and pointer variadic arguments use the same argument area.
  2. Build a small test application against libcurl's public curl_msnprintf() or another curl_mprintf-family API.
  3. Define a readable target buffer containing a recognizable string, a writable integer initialized to a sentinel value, and separate legitimate pointer arguments that can be checked afterward.
  4. Construct a finite double whose low pointer-sized portion contains the address of the readable target. Preserve the exact bit representation rather than relying on an imprecise decimal conversion.
  5. Call the formatting function with a constant sequential format containing %F followed by %s. Supply the crafted double for %F and a legitimate string pointer for %s, matching the documented argument types.
  6. Inspect the formatted result.
  7. Observe the vulnerable read behavior: %F is emitted or disregarded without consuming its double, and %s dereferences the address encoded in the double instead of the legitimate string pointer.
  8. Confirm that the recognizable target string appears in the output and that the legitimate %s argument was not used.
  9. Construct another finite double whose low pointer-sized portion contains the address of the writable target integer.
  10. Call the formatting function with a constant sequential format containing some output followed by %F%n. Supply the crafted double and then a legitimate pointer for %n.
  11. Observe the vulnerable write behavior: %n writes the current output count through the address encoded in the double, while the legitimate %n target retains its sentinel value.
  12. Use a debugger or memory instrumentation to confirm that the read and write addresses originate from the skipped %F argument rather than from out-of-range test data.
  13. As a control, replace %F with the implemented %f conversion while keeping the remaining argument structure equivalent.
  14. Confirm that %f consumes the double correctly and the following %s or %n uses the legitimate pointer.
  15. Repeat the test on an ABI with separate floating-point and general-purpose variadic argument streams, such as common x86-64 configurations, and document that the selected-address primitive may not reproduce there.
  16. Confirm a fix by recognizing %F as a double conversion during format parsing and preserving its uppercase formatting semantics.
  17. Repeat the sequential read and write tests and verify that %F consumes the double and every following conversion receives its intended argument.

Expected result

The documented %F conversion must consume one double. Every subsequent conversion must read the next correctly typed variadic argument.

Vulnerable result

The parser treats %F as unknown and does not allocate an argument slot for its double. A following pointer conversion can therefore interpret the skipped double's bits as an address, producing an ABI-dependent memory read or write.

Suggested fix

Add 'F' to parse_conversion() as an MTYPE_DOUBLE conversion with FLAGS_UPPER, mirroring the 'E'/'G' treatment. Preserve uppercase INF/NAN output in out_double(). Add a regression test that places a pointer conversion after %F to verify correct argument consumption.

Impact

Summary:

In a downstream application with the required format sequence and attacker-controlled double value, an attacker may convert the %F argument into a selected-address memory read or write. This could disclose process memory, corrupt application state, cause a crash, or potentially enable code execution, although exploitation requires a compatible ABI, address knowledge, and a suitable application call site.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original