curl · Report #3973152
37: `curl_mprintf` `%F` format specifier not consumed, causing variadic argument desynchronization
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"—%sdereferences an address encoded in the low 32 bits of the double. - Controlled write:
"%F%n"—%nwrites 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:
- 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/%nto consume the legitimate later pointer instead. - The attacker must control the
doubleargument value with enough precision to encode a useful address. - A downstream application must use
curl_mprintfwith a sequential format containing%Fbefore a pointer-consuming conversion. - No curl-internal format string uses
%F— this does not affect curl's own protocol handling. - 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
- 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.
- Build a small test application against libcurl's public
curl_msnprintf()or anothercurl_mprintf-family API. - 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.
- Construct a finite
doublewhose low pointer-sized portion contains the address of the readable target. Preserve the exact bit representation rather than relying on an imprecise decimal conversion. - Call the formatting function with a constant sequential format containing
%Ffollowed by%s. Supply the crafteddoublefor%Fand a legitimate string pointer for%s, matching the documented argument types. - Inspect the formatted result.
- Observe the vulnerable read behavior:
%Fis emitted or disregarded without consuming itsdouble, and%sdereferences the address encoded in the double instead of the legitimate string pointer. - Confirm that the recognizable target string appears in the output and that the legitimate
%sargument was not used. - Construct another finite
doublewhose low pointer-sized portion contains the address of the writable target integer. - Call the formatting function with a constant sequential format containing some output followed by
%F%n. Supply the crafteddoubleand then a legitimate pointer for%n. - Observe the vulnerable write behavior:
%nwrites the current output count through the address encoded in the double, while the legitimate%ntarget retains its sentinel value. - Use a debugger or memory instrumentation to confirm that the read and write addresses originate from the skipped
%Fargument rather than from out-of-range test data. - As a control, replace
%Fwith the implemented%fconversion while keeping the remaining argument structure equivalent. - Confirm that
%fconsumes the double correctly and the following%sor%nuses the legitimate pointer. - 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.
- Confirm a fix by recognizing
%Fas a double conversion during format parsing and preserving its uppercase formatting semantics. - Repeat the sequential read and write tests and verify that
%Fconsumes 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.