Skip to content

fix: guard shared state and add the checked parse helpers - #520

Closed
danalec wants to merge 2 commits into
VirtualDrivers:masterfrom
danalec:fix/driver-1-concurrency
Closed

danalec wants to merge 2 commits into
VirtualDrivers:masterfrom
danalec:fix/driver-1-concurrency

Conversation

@danalec

@danalec danalec commented Oct 20, 2026 •

Copy link
Copy Markdown

Stacked on #519. This branch is built on top of
fix/code-analysis-errors,
which fixes two Code Analysis errors that stop the build on a newer toolset.
Since #519 has not been merged yet, the diff below shows both: +14/−2 from
#519 and +177/−5 from this PR. Once #519 lands, this becomes +177/−5.

Summary

Two things that the rest of the driver builds on: the shared state is actually
guarded, and there is a way to parse a user-supplied number that does not throw
out of a UMDF callback.

One file, +177 / −5. No behaviour change — the guarded paths were already
unreachable in the normal case.

Shared state

g_Running is now std::atomic<bool>. The pipe server thread flips it
from a command while the main path reads it, and it was a plain bool.

g_pipeHandle is only written through SetPipeHandle() / ClearPipeHandle(),
under a dedicated mutex.
That mutex is deliberately not g_Mutex:
vddlog() is called from inside the pipe server while that thread holds
g_Mutex, so SendToPipe() cannot take g_Mutex without deadlocking on it.
ClearPipeHandle() returns the previous value so the caller can close it, and
the handle is withdrawn before the close — otherwise another thread could pick
up a handle that is already being closed.

s_KnownMonitorModesMutex protects a std::vector that two IddCx callbacks
can rewrite at the same time.
VirtualDisplayDriverParseMonitorDescription
and ...2 both call RebuildKnownMonitorModesCache(), and one can clear and
refill the vector while the other is indexing it. That is a live data race on
a std::vector — undefined behaviour, not a stale read. Readers now take a
snapshot under the lock instead of holding it across the IddCx calls that
follow.

g_MetadataMutex guards the HDR and gamma stores, which are written by the
settings loader and by the IddCx callbacks. std::map is not safe to read
while another thread inserts. Again distinct from g_Mutex, because those
callbacks log along the way.

Checked parsing

vdd_settings.xml, option.txt and monitor_profile.xml are user-editable
files, and loadSettings() runs inside the DeviceAdd callback. std::stoi
and std::stof throw std::invalid_argument or std::out_of_range on a
malformed value, and an exception leaving a UMDF callback crashes the host.

ParseIntSafe, ParseFloatSafe and ParseDoubleSafe log the offending
value and return the caller's fallback instead. They also reject trailing
garbage — " 480 " parses as 480 with stoi, but the trailing space is
counted as unconsumed input, so it is reported rather than silently accepted.

ParseDoubleSafe exists because the EDID profile's chromaticity and
luminance fields are double. ParseFloatSafe would quietly round them:
redX 0.640 read as float and stored back as double is
0.63999998569488525390625, which then differs from the 0.640 the same file
computes elsewhere.

ClampToUINT16 exists because a light level above 65535 wrapped silently on
the cast, turning a typo in the XML into a plausible-looking wrong number.

Verification

Debug and Release on x64 and ARM64 — four configurations, each producing
MttVDD.dll, MttVDD.inf and a signed mttvdd.cat, with
TreatWarningAsError on.

No hardware was involved: this changes how shared state is reached, not what
the driver does once it is reached.

Two unchecked return values. On MSVC 18 both stop the build; on the pinned CI
toolset neither surfaces. Neither defect is cosmetic.

AvSetMmThreadCharacteristicsW returns NULL on failure, and that NULL went
straight into AvRevertMmThreadCharacteristics, which does not accept it.
Reported as C28183 and promoted to C2220 by TreatWarningAsError. The revert now
happens only when the handle is valid; when the characteristics were never set
there is nothing to revert, so that branch is a log and no call.

CoCreateGuid had its HRESULT discarded (C6031 -> C2220). MonitorInfo is
zero-initialised, so a failure left MonitorContainerId as GUID_NULL, which was
then handed to IddCxMonitorCreate as if it were valid. The result is now
checked, and on failure the driver logs and skips creating that monitor rather
than registering it under GUID_NULL.

Note how this presents: the first C2220 aborts the compile before the next
warning is emitted, so silencing C28183 does not give a green build, it only
moves the error to the next line. Both had to be fixed to find out whether
anything was behind them.

Verified: Debug and Release on x64 and ARM64, 0 errors, all four producing
MttVDD.dll, MttVDD.inf and a signed mttvdd.cat.
Two things the rest of the driver builds on: the shared state is actually
guarded, and there is a way to parse a user-supplied number that does not throw
out of a UMDF callback. No behaviour change.

g_Running becomes std::atomic<bool>: the pipe server thread flips it from a
command while the main path reads it.

g_pipeHandle is only written through SetPipeHandle()/ClearPipeHandle(), under a
mutex that is deliberately not g_Mutex. vddlog() is called from inside the pipe
server while that thread holds g_Mutex, so SendToPipe() cannot take g_Mutex
without deadlocking on it. ClearPipeHandle() returns the previous value so the
caller can close it, and the handle is withdrawn before the close.

s_KnownMonitorModesMutex protects a std::vector that two IddCx callbacks can
rewrite at the same time -- one clears and refills it while the other indexes
it. That is a live data race, not a stale read. Readers take a snapshot under
the lock instead of holding it across the IddCx calls that follow.

g_MetadataMutex guards the HDR and gamma stores, which the settings loader and
the IddCx callbacks both write.

vdd_settings.xml, option.txt and monitor_profile.xml are user-editable, and
loadSettings() runs inside the DeviceAdd callback. std::stoi and std::stof
throw on a malformed value, and an exception leaving a UMDF callback crashes
the host. ParseIntSafe, ParseFloatSafe and ParseDoubleSafe log the offending
value and return the caller's fallback instead, and they reject trailing
garbage. ParseDoubleSafe exists because the EDID chromaticity fields are
double: redX 0.640 read as float and stored back as double is
0.63999998569488525390625, which then differs from the 0.640 the same file
computes elsewhere. ClampToUINT16 exists because a light level above 65535
wrapped silently on the cast.

Verified: Debug and Release on x64 and ARM64, 0 errors, all four producing
MttVDD.dll, MttVDD.inf and a signed mttvdd.cat.
@danalec

danalec commented Oct 20, 2026

Copy link
Copy Markdown
Author

Closing this without merging.

The work is real and it is verified -- every branch builds clean in Debug and
Release on x64 and ARM64 -- but the review surface turned out to be larger
than this series can reasonably ask of a maintainer, and the individual
pieces were not going to be picked up one by one either.

The code stays available on the fork, split into reviewable pieces:

fix/code-analysis-errors 1 file +14/-2
fix/driver-1-concurrency 2 files +134/-36
fix/driver-2-settings 1 file +257/-188
fix/driver-3-defaults 1 file +103/-69
fix/driver-4-metadata 1 file +85/-12
fix/driver-5-modes 1 file +192/-63
fix/driver-6-log-xml 1 file +223/-51
fix/driver-7-monitor 2 files +134/-36
fix/driver-8-callbacks 1 file +120/-89
fix/driver-9-validation 1 file +117/-40
fix/driver-10-pipe 1 file +242/-62
fix/driver-11-build 5 files +6/-214
fix/driver-12-config-docs 4 files +261/-98

If any of it is ever wanted, the branches are there and this PR can be
reopened.

@danalec danalec closed this Oct 20, 2026
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.

1 participant