From 95307dbb8d8878588729ec7b6c71fc86f2150620 Mon Sep 17 00:00:00 2001 From: Max Vilimpoc Date: Tue, 1 Sep 2026 13:01:04 +0200 Subject: [PATCH 1/1] dotfiles: drop the non-elevated ETW tracing, it never worked Putting an ordinary account into Performance Log Users was supposed to buy user-mode ETW collection without a UAC prompt. Testing the actual workflow says otherwise: xperf and VTune want an administrator account and an elevated prompt, and everything short of that gets blocked somewhere. The kernel logger was already known to be admin-only; the rest turned out not to be worth the machinery either. So the -TraceUser and -EtwRightsOnly parameters, the group membership step, and the UAC launch line that existed only to carry an argument across the elevation boundary all come out. The launch line goes back to the single-argument form it had before. Kept, because they are useful whether or not collection is elevated: the Windows Performance Toolkit step that detects xperf/wpr/wpa, reports their versions and puts them on the machine PATH, and the VTune step that reports whether it is installed. The README keeps the negative result rather than quietly losing it, and gains the commands to undo what earlier revisions left on a box - the group membership, the privilege, and the ACE. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012gCzbrN6p3emFUyufuJsyq --- README.md | 83 +++++++++------------- setup-windows-with-uac.ps1 | 139 ------------------------------------- setup-windows.bat | 17 +---- 3 files changed, 33 insertions(+), 206 deletions(-) diff --git a/README.md b/README.md index e9b2005..7c98650 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ throwaway VM reachable from a Linux host. | --- | --- | | `setup-windows.bat` | Entry point. Runs the winget installs, then launches the elevated half and prints its log, then runs the non-elevated script. | | `setup-windows-no-uac.ps1` | The non-elevated, per-user half: WinMerge and BinSkim on the user `PATH`, and the global git config (identity, plus `core.sshCommand`). Can also be run directly from an ordinary prompt. | -| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs the OpenSSH Client and Server capabilities and starts `sshd`, unpacks the `rsync-windows` release zip for this architecture (`rsync.exe` plus the `ssh.exe` it runs) into `C:\Tools\rsync` on the machine `PATH`, then installs Visual Studio 2022 Community with the required components, the WDK, and the Windows Performance Toolkit, and reports whether Intel VTune Profiler is present. It also puts one ordinary account into `Performance Log Users`, so it can run user-mode ETW sessions unelevated. Can also be run directly from an Administrator prompt — pass `-TraceUser DOMAIN\user` to name that account, and `-EtwRightsOnly` to do that step alone. | +| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs the OpenSSH Client and Server capabilities and starts `sshd`, unpacks the `rsync-windows` release zip for this architecture (`rsync.exe` plus the `ssh.exe` it runs) into `C:\Tools\rsync` on the machine `PATH`, then installs Visual Studio 2022 Community with the required components, the WDK, and the Windows Performance Toolkit, and reports whether Intel VTune Profiler is present. Can also be run directly from an Administrator prompt. | | `setup-windows-7-test-env.bat` | Prepares a **Windows 7 VM** as a test target driven from the host by `VBoxManage guestcontrol`. Copy it into the guest and run it there; it is idempotent, so re-run it after any snapshot restore. The per-user half needs no UAC (crash-dialog suppression, no screen blanking, a staging directory, the shared folder on `Z:`); the machine-wide half is skipped with a notice unless run elevated inside the guest. It then reports what the box can actually test: DWM composition, printers, audio capture devices. | ## Usage @@ -151,71 +151,52 @@ throwaway VM reachable from a Linux host. intel-vtune-_offline.exe -a --silent --cli --eula accept ``` - **Running it needs no elevation, but hardware sampling does.** A standard user - gets the User-Mode Sampling analyses — `vtune -collect hotspots` and threading - — and they work: measured here, collection and finalization, exit 0. Hardware - event-based sampling (`uarch-exploration`, `memory-access`, `hotspots -knob - sampling-mode=hw`) wants administrator, and VTune says so in a warning at the - top of every unelevated run. Note the failure it actually gives is *"cannot + **Run it from an administrator account, elevated.** Hardware event-based + sampling (`uarch-exploration`, `memory-access`, `hotspots -knob + sampling-mode=hw`) requires it, and VTune warns about that at the top of every + unelevated run. Worth knowing that the failure it gives there is *"cannot recognize the processor"*, which reads like a hardware problem and is not one: the drivers (`sepdrv5`, `sepdal`, `vtss`) are installed and running, and VTune - identifies the PMU through them. Unlike ETW there is no group to join for this - — the Linux driver can be handed to a `vtune` group, but on Windows the + identifies the PMU through them. There is no group to join to get around it — + the Linux driver can be handed to a `vtune` group, but on Windows the documented answer is to run as administrator. -- **User-mode ETW tracing without a UAC prompt — and the kernel logger's hard - limit.** Out of the box a standard user cannot start *any* event tracing - session, not even a user-mode one naming a single provider: +- **Collect traces from an elevated Administrator session. Non-elevated + collection was tried here and abandoned.** The attempt was to put one ordinary + account into `BUILTIN\Performance Log Users`, which appears in the default + security descriptors ETW keeps per provider GUID under + `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`, and collect without a UAC + prompt. It does not survive contact with the real workflow: `xperf -on base` + and `wpr -start` drive the *NT Kernel Logger*, reserved for Administrators and + LocalSystem, and granting the group `SeSystemProfilePrivilege` ("Profile system + performance") plus an explicit ACE for `TRACELOG_ACCESS_KERNEL_LOGGER` on + `SystemTraceControlGuid` — all three in place, across a reboot — still answered ```text - xperf -start T -on Microsoft-Windows-Kernel-Process -f trace.etl - -> Access is denied. (0x5) + xperf: error: NT Kernel Logger: Access is denied. (0x5). ``` - Session control is checked against the security descriptor ETW keeps per - provider GUID under `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`, whose - default grants those rights to SYSTEM, Administrators, the service accounts and - `BUILTIN\Performance Log Users` — and to nobody else. So the elevated half puts - the account named by `-TraceUser` into that group, and the command above then - works unelevated; enabling another account later is just - `net localgroup "Performance Log Users" /add`. Membership is read into - the access token **at logon**, so sign out and back in first — any new logon - does it, and an `ssh` login into the box is the quick way to check without - dropping the desktop. - - **Kernel traces are not available this way, and cannot be made to be.** - `xperf -on base` and `wpr -start` drive the *NT Kernel Logger*, which is - reserved for Administrators and LocalSystem — Microsoft documents Performance - Log Users access as explicitly not extending to it. That was measured rather - than assumed, and the negative result is recorded here so nobody repeats the - experiment: with the account in the group, `SeSystemProfilePrivilege` ("Profile - system performance") granted to that group, and an explicit ACE giving the - group `TRACELOG_ACCESS_KERNEL_LOGGER` on `SystemTraceControlGuid` — all three - in place, across a reboot — `xperf -on base` still answered `NT Kernel Logger: - Access is denied. (0x5)`. It is not a check an ACE overrides. The privilege - grant and the ACE were dropped again rather than left on the box earning - nothing: CPU sampling and whole-system traces are elevated work, with `xperf`, - `wpr` or VTune from an Administrator prompt. - - Analysis was never affected — `wpa.exe` opens an existing `.etl` as a plain - user. This is only about collection. - - The step runs **first** in the elevated half, and `-EtwRightsOnly` runs it and - nothing else, which matters because a full run is dominated by the three Visual - Studio passes that take minutes even with nothing to do: + It is not a check an ACE overrides, and the same wall turned up often enough + elsewhere that the whole approach was dropped rather than carried as a + half-working path. **Sign in to an administrator account and run `xperf`, `wpr` + and VTune from an elevated prompt.** Analysis is the exception and never needed + any of this: `wpa.exe` opens an existing `.etl` as a plain user. + + If an earlier revision of these scripts ran on a box, it left that account in + the group. Take it back out with: ```powershell - Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass', - '-File','\setup-windows-with-uac.ps1','-TraceUser','DOMAIN\user','-EtwRightsOnly' + net localgroup "Performance Log Users" DOMAIN\user /delete ``` - If you ran an earlier revision of this script, it left both of the grants above - on the box. Undo the privilege in `secpol.msc` > Local Policies > User Rights - Assignment > "Profile system performance" by removing Performance Log Users. - The ACEs sit in the `{9e814aad-3204-11d2-9a82-006008a86939}` value under + Two revisions also granted the privilege and the ACE. Undo the privilege in + `secpol.msc` > Local Policies > User Rights Assignment > "Profile system + performance" by removing Performance Log Users. The ACEs sit in the + `{9e814aad-3204-11d2-9a82-006008a86939}` value under `HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security`: strip the `LU` entries from that descriptor rather than deleting the value, which also carries entries for SYSTEM, Administrators and two service accounts. + - The scripts were extracted from a native Windows project, so the component selection is tuned for that: Spectre-mitigated runtimes, the v141/XP toolset, and driver-kit headers. Trim the component lists in the `.ps1` if you don't diff --git a/setup-windows-with-uac.ps1 b/setup-windows-with-uac.ps1 index 17d02ee..9f3d8ae 100644 --- a/setup-windows-with-uac.ps1 +++ b/setup-windows-with-uac.ps1 @@ -17,36 +17,12 @@ - Windows Driver Kit 10.0.26100 - Windows Performance Toolkit - xperf, wpr and Windows Performance Analyzer (wpa.exe) - on the machine PATH - - Performance Log Users membership for one ordinary account, so it can run - user-mode ETW sessions (xperf -start ... -on ) without elevation. - Kernel traces are NOT covered - the NT Kernel Logger is admin-only; see the - step for what was measured. Change $VsInstallerUrl below to the Professional or Enterprise bootstrapper if needed: Professional : https://aka.ms/vs/17/release/vs_professional.exe Enterprise : https://aka.ms/vs/17/release/vs_enterprise.exe #> -param( - # Account to put in Performance Log Users (see the "ETW session control" - # step, which runs first). Defaults to the interactive console user, but - # setup-windows.bat passes it explicitly: with over-the-shoulder elevation - # THIS script runs as the administrator whose credentials went into the UAC - # prompt, not as the user who started the batch file, so $env:USERNAME here - # is the wrong answer. - # - # Pass an empty string to skip it; adding an account later is one - # `net localgroup` away. - [string] $TraceUser = '', - - # Do the ETW step and nothing else. It is a group membership and no - # downloads, where a full run is dominated by the three Visual Studio - # passes, which take minutes even when they have nothing to do. It is why - # that step runs FIRST: -EtwRightsOnly is then just an early exit rather - # than a set of guards down the rest of the script. - [switch] $EtwRightsOnly -) - $ErrorActionPreference = 'Stop' function Write-Step([string]$Msg) { @@ -132,121 +108,6 @@ try { Start-Transcript -Path $LogFile -Force | Out-Null } catch {} try { -# --------------------------------------------------------------------------- -# ETW session control for an ordinary account -# -# Creating or controlling an event tracing session - even a user-mode one naming -# a single provider - is checked against the security descriptor ETW keeps per -# provider GUID under HKLM\SYSTEM\CurrentControlSet\Control\WMI\Security. The -# default grants the session-control rights (TRACELOG_CREATE_ONDISK, -# TRACELOG_CREATE_REALTIME, TRACELOG_GUID_ENABLE, TRACELOG_LOG_EVENT) to SYSTEM, -# Administrators, the service accounts and BUILTIN\Performance Log Users, and to -# nobody else. Its own description says members "may ... enable trace providers, -# and collect event traces", and that is what membership buys: -# -# xperf -start MySession -on Microsoft-Windows-Kernel-Process -f trace.etl -# xperf -stop MySession -# -# runs unelevated for a member and is "Access is denied. (0x5)" for everyone -# else. Enough to trace your own application's providers without a UAC prompt. -# -# Membership is read into the access token at LOGON, so the account has to sign -# out and back in. Any NEW logon does it - an ssh login into this box is one, -# which is the quick way to check without dropping the desktop. -# -# WHAT THIS DOES NOT BUY: system-wide kernel traces. `xperf -on base` and -# `wpr -start` drive the NT Kernel Logger, which is reserved for Administrators -# and LocalSystem - Microsoft documents Performance Log Users access as -# explicitly NOT extending to it. Measured here, so that nobody repeats it: with -# the account in the group, SeSystemProfilePrivilege ("Profile system -# performance") granted to that group, and an explicit ACE giving the group -# TRACELOG_ACCESS_KERNEL_LOGGER on SystemTraceControlGuid - all three in place, -# across a reboot - xperf still answered -# -# xperf: error: NT Kernel Logger: Access is denied. (0x5). -# -# It is not a check an ACE overrides. Those two grants were dropped again rather -# than left on the box earning nothing, and CPU sampling and whole-system traces -# are elevated work: run xperf, wpr or VTune from an Administrator prompt. -# -# Analysis needs none of this either way - wpa.exe opens an existing .etl as a -# plain user. -# --------------------------------------------------------------------------- -Write-Step 'ETW session control (non-elevated user-mode tracing)' -$PerfLogUsersSid = 'S-1-5-32-559' # BUILTIN\Performance Log Users -try { - # Fall back to the console user when the caller did not name one: with - # over-the-shoulder elevation that is the person who started - # setup-windows.bat, which is who wants to trace. - $target = $TraceUser - if (-not $target) { - $target = (Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue).UserName - if ($target) { Write-Host " No -TraceUser given; using the console user $target" } - } - - if (-not $target) { - Write-Warning 'No account to add to Performance Log Users (pass -TraceUser DOMAIN\user).' - Write-Warning 'To do it later:' - Write-Warning ' net localgroup "Performance Log Users" DOMAIN\user /add' - } else { - # Resolve to a SID first: it validates the name, and it is what the - # membership check compares, so a member spelled ".\claude" in one place - # and "LATISLAB\claude" in another is still recognised as the same account. - $targetSid = (New-Object System.Security.Principal.NTAccount($target)).Translate( - [System.Security.Principal.SecurityIdentifier]) - - # By SID, never by name: "Performance Log Users" is localised, and - # Get-LocalGroup -SID is how this stays correct on a non-English box. - $group = Get-LocalGroup -SID $PerfLogUsersSid - - # Get-LocalGroupMember throws on a group holding a SID that no longer - # resolves (a known Windows 10 bug), so a failure to READ the membership - # must not stop us from writing it - fall through and let the add report. - $already = $false - try { - $already = @(Get-LocalGroupMember -SID $PerfLogUsersSid | - Where-Object { $_.SID.Value -eq $targetSid.Value }).Count -gt 0 - } catch { - Write-Host " (could not enumerate $($group.Name) members: $($_.Exception.Message))" -ForegroundColor DarkGray - } - - if ($already) { - Write-Host " OK: $target is already in $($group.Name)" - } else { - try { - Add-LocalGroupMember -SID $PerfLogUsersSid -Member $targetSid.Value - } catch { - # "already a member" is only reachable when the enumeration above - # failed, and is not an error. Matched on the type NAME rather - # than in a typed catch clause: catch types are resolved when the - # script is PARSED, before the LocalAccounts module has been - # autoloaded, so naming the type there is a parse error that - # would take the whole script down. - if ($_.Exception.GetType().Name -ne 'MemberExistsException') { throw } - } - Write-Host " Added $target to $($group.Name)" - } - - Write-Host '' - Write-Host " $target must sign out and back in before this takes effect." -ForegroundColor Yellow - Write-Host ' Then, from that account (NOT elevated):' -ForegroundColor Yellow - Write-Host ' xperf -start T -on Microsoft-Windows-Kernel-Process -f trace.etl' -ForegroundColor Yellow - Write-Host ' xperf -stop T' -ForegroundColor Yellow - } -} catch { - Write-Warning "Performance Log Users membership failed: $($_.Exception.Message)" - Write-Warning 'Do it by hand with:' - Write-Warning ' net localgroup "Performance Log Users" /add' -} - -if ($EtwRightsOnly) { - # `exit` inside the try still runs the finally below, so the transcript is - # stopped and the log is left readable by the non-elevated caller. - Write-Host "`n-EtwRightsOnly: skipping the installs." -ForegroundColor Green - exit 0 -} - - # --------------------------------------------------------------------------- # Base tools via winget # --------------------------------------------------------------------------- diff --git a/setup-windows.bat b/setup-windows.bat index cb2765e..54d6956 100644 --- a/setup-windows.bat +++ b/setup-windows.bat @@ -60,25 +60,10 @@ set "DOTNET_EXE=%ProgramFiles%\dotnet\dotnet.exe" @rem --- Elevated installs (VS2022, WDK, system tools) --- @rem The elevated script runs in its own window and logs to setup-windows-uac.log. @rem -PassThru + $p.ExitCode propagates its real exit code back through to ERRORLEVEL. -@rem -@rem -TraceUser passes YOU across the UAC boundary. Accepting that prompt with an -@rem administrator's credentials runs the elevated half AS that administrator, so -@rem it cannot see whose box this is; the account named here is the one it grants -@rem non-elevated ETW collection rights to (xperf / wpr without a UAC prompt). -@rem -@rem The two values go through the environment, and the quotes the child needs -@rem around them are built in PowerShell as [char]34, so the command line below -@rem contains no embedded quote characters at all. Writing them inline as "".."" -@rem works for ONE argument and quietly breaks at two: the quote-state parsing -@rem swallows everything after the first into the -File value, and the elevated -@rem PowerShell dies with "failed because the file does not have a '.ps1' -@rem extension" and exit code -196608 (0xFFFD0000) before it can log a thing. set "UAC_LOG=%~dp0setup-windows-uac.log" if exist "%UAC_LOG%" del "%UAC_LOG%" -set "UAC_SCRIPT=%~dp0setup-windows-with-uac.ps1" -set "UAC_TRACE_USER=%USERDOMAIN%\%USERNAME%" -powershell -NoProfile -Command "$q = [char]34; $p = Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File',($q + $env:UAC_SCRIPT + $q),'-TraceUser',($q + $env:UAC_TRACE_USER + $q) -Wait -PassThru; exit $p.ExitCode" +powershell -NoProfile -Command "$p = Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','""%~dp0setup-windows-with-uac.ps1""' -Wait -PassThru; exit $p.ExitCode" set "UAC_RC=%ERRORLEVEL%" @rem --- Surface the elevated session's output (its window has already closed) --- -- 2.48.2