Skip to content

fix: read each setting from the right source and check its type - #521

Closed
danalec wants to merge 3 commits into
VirtualDrivers:masterfrom
danalec:fix/driver-2-settings
Closed

danalec wants to merge 3 commits into
VirtualDrivers:masterfrom
danalec:fix/driver-2-settings

Conversation

@danalec

@danalec danalec commented Oct 20, 2026

Copy link
Copy Markdown

Stacked on #520, which is stacked on #519. The diff below includes both
of those, since neither is merged yet: +14/−2 from #519, +177/−5 from #520,
and +257/−188 from this PR. Once they land, this is +257/−188.

Summary

Where each setting comes from, and whether the value is the type the reader
expects. One file, +257 / −188.

vdd_settings.xml is parsed once

It was reparsed on every single read. It is parsed once into a cache now, and
the cache is invalidated when the runtime rewriters change the file on disk.

The registry readers check the type

Four of them passed lpType = NULL to RegQueryValueExW and then handed the
raw wchar_t[MAX_PATH] to std::wstring or std::stoi. Two of the four
already captured the type; the other two did not.

All four now require REG_SZ or REG_EXPAND_SZ, reject a length outside the
buffer, drop a trailing non-terminator, and construct the string by the length
the registry reported — instead of scanning for a NUL that a malformed value is
not guaranteed to have. GetDoubleSetting handles REG_DWORD with memcpy,
because REG_DWORD writes four raw bytes with no terminator at all.

A replay harness over six payloads — well-formed REG_SZ, terminator-less
REG_SZ, REG_BINARY filling the buffer, a small REG_BINARY, REG_DWORD,
and an empty value — leaves the old path out of bounds in 5 of 5 adversarial
cases, reaching index 519 of a wchar_t[260]. The new path leaves it in 0.

The empty value is a genuine escape rather than a theoretical one: the registry
writes nothing there, so the buffer keeps whatever the stack held.

EnabledQuery now says whether the key existed

The old contract collapsed "absent" and "present and false" into the same
false. DriverEntry needs the difference for the settings whose compiled
default is true — HardwareCursor ships as <HardwareCursor>true</HardwareCursor>,
and answering false for "no settings file" turned the hardware cursor off on
exactly the configuration nobody had configured.

EnabledOr routes the missing-key case to a caller-chosen default and keeps
EnabledQuery's old contract for the 25 call sites that do not care why the
answer is false.

Five settings collided on the bare name enabled and are now qualified by path
(hdr10_static_metadata/enabled and friends).

Verification

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

Not verified on hardware. The reading of settings was not exercised against
an installed driver; the harness covers the malformed-value paths, which is
where the defects are.

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.
vdd_settings.xml was reparsed on every single read. It is parsed once into a
cache now, and the cache is invalidated when the runtime rewriters change the
file on disk.

Four registry readers passed lpType = NULL to RegQueryValueExW and then handed
the raw wchar_t[MAX_PATH] to std::wstring or std::stoi. Two of the four
already captured the type; the other two did not. All four now require
REG_SZ or REG_EXPAND_SZ, reject a length outside the buffer, drop a trailing
non-terminator, and construct the string by the length the registry reported
instead of scanning for a NUL that a malformed value is not guaranteed to
have. GetDoubleSetting handles REG_DWORD with memcpy, because REG_DWORD
writes four raw bytes with no terminator at all.

A replay harness over six payloads -- well-formed REG_SZ, terminator-less
REG_SZ, REG_BINARY filling the buffer, a small REG_BINARY, REG_DWORD and an
empty value -- leaves the old path out of bounds in 5 of 5 adversarial cases,
reaching index 519 of a wchar_t[260]. The new path leaves it in 0. The empty
value is a genuine escape rather than a theoretical one: the registry writes
nothing there, so the buffer keeps whatever the stack held.

EnabledQuery now reports whether the key was present at all, separately from
its value. The old contract collapsed "absent" and "present and false" into the
same false, and DriverEntry needs the difference for the settings whose
compiled default is true -- HardwareCursor ships as <HardwareCursor>true</
HardwareCursor>, and answering false for "no settings file" turned the
hardware cursor off on exactly the configuration nobody had configured.
EnabledOr routes the missing-key case to a caller-chosen default and keeps
EnabledQuery's old contract for the 25 call sites that do not care why the
answer is false.

Five settings collided on the bare name "enabled" and are now qualified by
path (hdr10_static_metadata/enabled and friends).

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