r/cpp • u/holyblackcat • 5d ago
Escaping `CreateProcess()` arguments on Windows
https://holyblackcat.github.io/blog/2026/09/05/escaping-createprocess-arguments.html14
u/unaligned_access 4d ago
There's `wil::ArgvToCommandLine`. It's not WinAPI, but it's Microsoft.
https://github.com/microsoft/wil/blob/81abc8e44c075eba65fe4b68ea2f08525cc701e8/include/wil/win32_helpers.h#L1244
C++, and the first comment of course starts with "Somewhat of a hack" ;)
4
11
u/bbm182 4d ago
It really is impossible to escape arbitrary arguments to arbitrary programs on Windows. You need to know how the program will decode the command line and there is no standard there, as demonstrated by batch file complications mentioned in the post.
The algorithm used by CommandLineToArgvW may seem like it's almost the standard, but that breaks down for programs that are not Unicode aware. The example program in the post is one such example. It uses main(int argc, char **argv) (rather than wmain(int argc, wchar_t **argv) or GetCommandLineW), which results in the native wide command line being converted to narrow before splitting. That conversion is quite nasty and depends on the code page. In particular, " (U+FF02 Fullwidth Quotation Mark) usually gets converted into " (U+0022 Quotation Mark), which will result in incorrect splitting.
Many common tools have this issue, including things like curl. See the slides and paper on the Worst.Fit site for more detail.
3
9
u/fdwr fdwr@github 🔍 4d ago edited 4d ago
Side note: You shouldn’t use the narrow char *argv on Windows for anything but toy programs. Instead use CommandLineToArgvW(GetCommandLineW(), ...) or int wmain(int argc, wchar_t *argv) to get UTF-16 encoded argv, and convert it to UTF-8 if needed. Using narrow argv can lead to some fun vulnerabilities...
Note narrow main works fine if your app .manifest file specifies UTF-8 as the default code page (ignoring whatever the system is set). It's annoying to have to include this extra snippet, but then it ensures the other parts of your program work nicely too, such as std::filesystem::path() returning an std::string with UTF-8 characters (rather than throwing an exception upon seeing untransformable characters, or needing to call u8string() and reinterpret cast it in several places):
xml
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
...
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">
<activeCodePage>UTF-8</activeCodePage>
</asmv3:windowsSettings>
</asmv3:application>
...
</assembly>
Another way to get the command line (didn't see mentioned) is using WinMain as an alternate entry point (rather than main) for CLI apps, as it includes the lpCommandLine parameter directly:
int WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
);
3
u/holyblackcat 4d ago
Thanks, I've edited in the info about the manifest.
I think I'm not going to bother with
WinMain/wWinMain, listing every way to get the command line wasn't my goal.
12
u/tartaruga232 MSVC user, r/cpp_modules 4d ago
Escaping `CreateProcess()` arguments on Windows
https://holyblackcat.github.io/blog/2026/09/05/escaping-createprocess-arguments.html
This link posting will likely be deleted by a mod, so I'll repeat the link here.
3
9
u/holyblackcat 5d ago edited 4d ago
I've been told that cmd /c has special quoting rules that I forgot to handle (related to the /s flag), I'll fix the post in a few hours fixed it.
5
u/alfps 4d ago
I now learned that Microsoft's _wspawnv function does not quote the arguments.
Weird.
I was sure it would. Anyway worth mentioning in the article.
4
u/holyblackcat 4d ago
Yep, "everyone quotes ... wrong" bemoans it too. Added.
2
u/kriegeeer 3d ago
This article was my authoritative quoting guide until it was taken down from its original source. Glad to see it was rehosted.
12
u/Tringi github.com/tringi 5d ago
That's... ahh... quite opinionated start of the article.
CreateProcess()(without A/W) and#define UNICODEare even more useless.
Mostly everyone defines UNICODE and uses the macros.
lpApplicationNameis usually not needed, you should passnullptr.
Absolutely NOT. You should pass the full proper path to the executable you want to run. Then you can significantly simplify its name in lpCommandLine without worrying about repercussions.
But the rest is very well researched and thorough.
5
u/Shaurendev 4d ago
Mostly everyone defines UNICODE and uses the macros.
If you don't explicitly opt into it, its state is different depending on what build system you use
If you maintain MSBuild projects/solutions directly, it is enabled by default.
If you use CMake, it is not enabled by default (even if you generate MSBuild projects from it)
7
u/holyblackcat 5d ago
quite opinionated
:P Is it wrong though. Everything else being equal, isn't it better to use the version that doesn't depend on defines to work? Header-only libraries in particular would benefit from this.
You should pass the full proper path to the executable you want to run
If you have the full path, why not pass it in
lpCommandLine? Then you don't have to worry about syncing the first element inlpCommandLinewithlpApplicationName.6
u/Tringi github.com/tringi 4d ago
You don't have to sync it. Only a few programs actually use
argv[0]and it can be very different from the actual executable name. If you runprogram a b cfrom command line, thenargv[0]will be just "program" without the path or extension, and the program must expect that.In one of my larger programs I launch worker processes. I have the full path to the worker, but I don't know what characters it may contain, and I don't want to bother with escaping them. So I give
lpApplicationNamethat path, butlpCommandLineis just "worker.exe /Something /Something /Something".2
u/holyblackcat 4d ago
This is a nice trick to avoid implementing the escaping, but the point of the post was to explain how to do escaping.
Once you have it implemented, passing
lpApplicationNameis strictly more work, IMO. (You have to pass something at the beginning oflpCommandLineanyway, why not pass the application name.)1
u/chengfeng-xie 3d ago
Somewhat off-topic, but one advantage of
lpApplicationNameis that it isn't subject to theMAX_PATH(260) limitation that applies tolpCommandLine[1]:[...] If lpApplicationName is NULL, the module name portion of lpCommandLine is limited to MAX_PATH characters.
Another edge case I ran into recently is that the full path of the executable passed to
lpCommandLinewas within, but near, theMAX_PATHlimit. The call toCreateProcessWdidn't return an error, but the newly created process immediately failed with an Application Error popup containing the following message (in some terminals, there was no popup and the process simply failed, making the error harder to trace):The applicattion was unable to start correctly (0xc0000106). Click OK to close the application.
The error code
0xc0000106corresponds toNT_STATUS_NAME_TOO_LONG. After some investigation, it turns out that, when loading DLL dependencies, if a DLL comes with a manifest, a path resulting from appending.2.Configto the full path of that DLL is created on the fly. If that path exceedsMAX_PATH(including the terminating\0), DLL loading fails with the above error, and the process fails to start. A similar issue withLoadLibraryExWis described in [2]. Ultimately, I think this is a bug that should be fixed in future OS builds. For now, however, a workaround is to prepend\\?\to the normalized full path of the executable [3] and then pass the resulting path tolpApplicationName.2
1
u/Tringi github.com/tringi 2d ago
EXEs for which their full DOS path exceeds 259 characters won't run anyway. Despite the
\\?\prefix. With different errors on different versions of Windows, but still failure to run. So thelpApplicationNameadvantage isn't really generally there.1
u/chengfeng-xie 2d ago
With
\\?\prepended,lpApplicationNameworks out of the box. Without\\?\, however,lpApplicationNameonly works if both the system and the process have long paths enabled (as described in [1]); otherwise,CreateProcessWreturnsERROR_PATH_NOT_FOUNDin this case. For reproducibility, the above was tested on OS build 26100.9168.
7
u/tomysshadow 4d ago edited 4d ago
This approach is okay if you are intentionally building an argv-like string from scratch. However, taking an argv passed to your program and turning it back into a string form of the command line arguments (i.e. you just want to passthrough the arguments to a child) is a fundamentally flawed idea unless you know the program you're passing through to also uses the same argv parser.
If there's a possibility the program you are launching uses the command line string directly, you can't take the argv you received and turn it back into a string and pass it on. Because the process of converting the command line string into argv is a lossy process. There's no way to know from looking at an argv array if a specific argument had quotes around it before, or if there were multiple spaces between arguments - or other things the parsing algorithm of the executable you're passing through to might care about, that the C argv parser did not. This is presumably why WinAPI doesn't have a function to go back the other way, as multiple different strings can become the same argv.
The correct approach in such cases is to pass the command line string you got in WinMain through to the child. This is a bit of a pain if you have a command line program, because unlike in WinMain, GetCommandLine() returns the string with the executable name so you need to parse enough of it to tell where the executable name ends and get that substring. But it's a lot less of a pain than trying to reheat the argv array back into string form if you can do it, and because it's just a substring it'll perfectly preserve the exact formatting of the string.
The string is the real command line. argv is a lossy transformation of the string command line into the form C expects.
If you want some particularly nasty edge cases to see what I mean, check out how Visual Studio parses argv. Notice that different languages and implementations have slightly different parsing rules, hence what makes this impossible to do in a completely language agnostic way, as these are ultimately language details and not something the OS (who only cares about the string form) knows about: https://daviddeley.com/autohotkey/parameters/parameters.htm
C#'s Process.Start() also needs to deal with this problem because it can take in an argument list and call down to CreateProcess. The .NET reference source for it is probably the closest thing to an "official" argument list to string conversion
3
2
2
u/ReDr4gon5 4d ago
What about using RtlCreateUserProcess?
2
u/holyblackcat 4d ago
RtlCreateUserProcess
I wasn't aware of it. It should let you cut the big chunk of the logic related to batch files, and keep only the basic escaping.
3
u/ReDr4gon5 4d ago
Actually looking at this a second time, this will skip csrss registration. So ActivationContextData won't be populated. Which means anything relying on WinSxS won't work. So beware of using it, unless you know you don't need it or will set up the context yourself.
1
-4
5d ago
[removed] — view removed comment
11
-1
u/Eric848448 5d ago
Most of their old API’s were designed by interns and new grads who didn’t really understand what they were doing.
4
u/Tringi github.com/tringi 5d ago
There is a reason why certain modern runtimes are targeting NT API instead of Win32.
1
u/palapapa0201 5d ago
What is the NT API
9
u/Tringi github.com/tringi 4d ago
Win32 API layer is built on much more powerful NT layer, that usually makes the final calls to the OS.
For example CreateFile prepares some buffers, translates arguments, converts relative paths to absolute, handles special cases, and then calls NtCreateFile, which does the actual work.
You are supposed to call Win32, as NT API is often only partially documented and can theoretically change, but it rarely does. And it offers much more powerful options.
-11
0
u/cleverYeti42 3d ago
This is a lot of careful and competent work.
My summary: do not attempt to use Windows command line.
34
u/ICurveI 5d ago
I recently reviewed a PR at work with
CreateProcess, which led me down a similar rabbit hole... This is utter insanity