win-capture: say which path component failed the hook trust check, and act only on the directory - #761
Closed
summeroff wants to merge 2 commits into
Closed
win-capture: say which path component failed the hook trust check, and act only on the directory#761summeroff wants to merge 2 commits into
summeroff wants to merge 2 commits into
Conversation
… only on the directory
hook_path_chain_is_trusted() answered for the object and every directory above
it with one bool, so a refusal named neither the component nor the reason. The
helpers fill in both now: the owning SID, the granting SID with the rights it
holds, a reparse point, or the error that stopped the descriptor being read.
Paths are converted rather than logged through %ls, which goes via the CRT
locale and drops the rest of the line at the first character outside it - a
user profile name is exactly where these paths carry one.
Only the object is acted on now:
- An untrusted ancestor no longer quarantines the shared hook directory, and
no longer marks the hooks in it untrusted.
- It no longer blocks publishing from the install directory.
- It is no longer HOOK_SHARED_UNSAFE, so the implicit vulkan layer stays.
The layer is why this matters. disable_vulkan_layer() removes the HKCU entry
unconditionally and the HKLM one only when elevated, and the app normally is
not - so withdrawing over an ancestor removed the per-user entry, which grants
no privilege a standard user does not already have, and left the machine-wide
entry, which does. The user meanwhile loses vulkan capture for a condition no
elevated writer can repair, and a standard user who can rename a parent of
%ProgramData% can rename a parent of %ProgramFiles% and replace the application
that gets launched.
There is deliberately no chain predicate left. Folding the two answers back
into one bool is what produced the behaviour above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e check The file-level refusals read as though a write grant had been found, but hook_object_trust also refuses a reparse point, an unreadable descriptor or ACE, and an unhandled ACE type. hook_file_is_trusted() carries the reason instead, the way the directory-level refusals in this file already do, so the three that named a cause they had not established no longer do. The same wording came up in review on the updater side, a-files-updater#144, where the shared rules have their other copy. hook_warn_path is now hook_warn_wide, taking the optional reason, and hook_warn_trust routes its object branch through it rather than repeating the conversion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
|
Superseded by #762, which has the same two commits on a fresh branch. The automated review never picked this one up. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two changes to the graphics hook trust check, both prompted by the first field report of the updater half of #756 failing.
Pairs with streamlabs/a-files-updater#144, which makes the same two changes there.
hook-dir-security.hand its copy in the updater are kept in step by hand.Why
The report was from the updater, but the cause sits in the rules both sides share.
hook_path_chain_is_trusted()answers for the object and every directory above it, up to and including the drive root, and returns one bool. On the reporting machine that bool was false, and the log could not say whether the hook directory,%ProgramData%orC:\had been refused, or on what. Working it out took a machine-local reimplementation of the rules, and the answer is still inference: the two failing checks share exactly one object,C:\.That is fix 1. Fix 2 is what the same investigation turned up here:
dir_was_trustedat the quarantine,was_trustedfor the files, and the final gate were all the whole chain, so an ancestor we can neither vouch for nor repair was driving decisions that are only about the directory.What it does now
hook_object_trust()takes an optional reason buffer;hook_path_trust()fills astruct hook_trustwith the object and the ancestors answered separately, the failing ancestor named, and a reason for each — the owning SID, the granting SID with the rights it holds and the full mask, a reparse point, or the error that stopped the descriptor being read.Paths are converted with
os_wcs_to_utf8_ptr()rather than logged through%ls.%lsin a narrowprintfconverts via the CRT locale and drops the rest of the line at the first character outside it, and a user profile name is exactly where these paths carry one — which would lose the diagnostic for the users hardest to reach remotely.Behaviour, for an untrusted ancestor only:
HOOK_SHARED_UNSAFEget_hook_path()Nothing changes when the object is untrusted. Same quarantine, same descriptor, same
HOOK_SHARED_UNSAFE, same layer withdrawal.hook_path_chain_is_trusted()is gone rather than left unused. Folding the two answers back into a single bool is what produced the behaviour above, and leaving the predicate in a shared header is how it comes back.The relaxation, stated plainly
The layer is the part worth arguing about, and the argument is what settled it.
disable_vulkan_layer()removes the HKCU entry unconditionally and the HKLM one only when elevated. The app normally is not elevated. So withdrawing over an ancestor removed the per-user entry — which grants no privilege a standard user does not already have, since they can register their own implicit layer in their own hive whenever they like — and left the machine-wide entry, which is the one that reaches elevated processes and the one a standard user cannot create. The cost and the benefit were the wrong way round for the common case.Against that, the cost is real and permanent: vulkan capture gone for every OBS derived application on the machine, for a condition no elevated writer can repair. Game capture was never affected —
get_hook_path()falls back to the hook in the install directory — so this was only ever vulkan titles, but it was all of them.And a standard user who can rename a parent of
%ProgramData%can rename a parent of%ProgramFiles%and replace the application that gets launched. Whatever we decline to do about the hook is not their limiting factor. The same reasoning is why the install-directory payload is held to its own directory now rather than to its whole path.Testing
win-capturex64 RelWithDebInfo: builds clean, 0 warnings with/WX. ConfirmedTreatWarningAsErrorandWarningLevelin the generated project first, then forced a full recompile of the translation unit rather than relying on an up-to-date target.streamlabsand at this revision into two harnesses and their answers diffed across 13 paths: identical on all of them. That covers the separator handling win-capture: restrict the shared graphics hook directory to administrators #756 added — a fully forward-slash path, a mixed one, and a trailing separator all still resolve the same way — so the trust computation is unchanged and only the policy differs. The harness also asserts the split agrees with the old combined answer on every path.C:\ProgramData(grants S-1-5-32-545 write access 0x00000116—BUILTIN\Users, which is why the ancestor mask is weaker than the object one), the owner branch, a reparse point viaC:\Documents and Settings, a missing path, and a path with no drive letter.C:\work\temp's ancestor failure was correctly attributed toC:\work.C:\as an object reportsgrants S-1-5-11 write access 0x00000004— Authenticated Users withFILE_APPEND_DATA, the entry the weaker ancestor mask exists to tolerate. That is the mechanism behind the original report, and it is one inherit-only flag away from failing every path on a machine.🤖 Generated with Claude Code