Skip to content

Use va_copy in the v1 LogF macro before the sizing pass#144

Open
JustinMDotNet wants to merge 1 commit into
wopss:masterfrom
JustinMDotNet:fix/v1-logger-va-copy
Open

Use va_copy in the v1 LogF macro before the sizing pass#144
JustinMDotNet wants to merge 1 commit into
wopss:masterfrom
JustinMDotNet:fix/v1-logger-va-copy

Conversation

@JustinMDotNet

Copy link
Copy Markdown

Closes #135.

What this changes

In the LogF macro in src/dll/v1/Logger.cpp, copy args into a second va_list before the sizing call and end the copy immediately after. The original va_list is left intact for the formatting call.

va_list args;
va_start(args, aFormat);

va_list argsForLen;
va_copy(argsForLen, args);
auto len = count_fn(aFormat, argsForLen);
va_end(argsForLen);

if (len > 0) {
    ...
    auto res = format_fn(buffer.data(), buffer.size(), buffer.size() - 1, aFormat, args);
    ...
}
...
va_end(args);

Why

The previous code passed the same va_list to _vscprintf / _vscwprintf and then again to vsnprintf_s / _vsnwprintf_s. Per the C standard a va_list is in an indeterminate state after a vararg consumer reads from it; reusing it without va_copy or a fresh va_start is undefined behavior.

This happens to work on MSVC x64 today because the va_list there is effectively a char* and survives the call. It is still UB, and it breaks on:

  • ARM64, where va_list has a different representation.
  • Any future MSVC that changes the va_list layout.
  • Plugins built against a CRT with stricter va_list semantics.

This macro is the entire formatted-logging surface for v1 plugins (TraceF, DebugF, InfoF, WarnF, ErrorF, CriticalF and the wide variants), so getting it wrong breaks every plugin's formatted logger at once.

What I tested

Local rebuild. The change is small enough that the diff is the audit. No behavior change is expected on the current toolchain because the original code happens to do the right thing there; this is preventative.

The LogF macro starts a single va_list, passes it to _vscprintf /
_vscwprintf to size the buffer, and then reuses the same va_list for
vsnprintf_s / _vsnwprintf_s. Per the C standard, a va_list is in an
indeterminate state after a vararg consumer reads from it, and any
further use without va_copy or a fresh va_start is undefined behavior.

This happens to work on MSVC x64 today because va_list is effectively
a char* there. It will not work on ARM64, on a future MSVC that
changes the representation, or on plugins built against a CRT with
stricter va_list semantics. Since this is the entire formatted-
logging surface for v1 plugins, getting it wrong breaks every
plugin's logger at once.

Copy args into a second va_list for the count_fn call and end that
copy immediately, leaving the original args intact for format_fn.

Fixes wopss#135

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v1 logger reuses va_list across two consumers without va_copy

2 participants