Initial release of the sudo source (#71)

Initial release of the sudo source

---------

Co-authored-by: Mike Griese <zadjii@gmail.com>
This commit is contained in:
Mike Griese 2024-05-22 18:48:06 -07:00 committed by GitHub
parent ce1a3b0384
commit d86b439155
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 6217 additions and 6 deletions

16
.cargo/config.toml Normal file
View File

@ -0,0 +1,16 @@
# -Ccontrol-flow-guard: Enable Control Flow Guard, needed for OneBranch's post-build analysis (https://learn.microsoft.com/en-us/cpp/build/reference/guard-enable-control-flow-guard).
[target.'cfg(target_os = "windows")']
rustflags = [
"-Ccontrol-flow-guard",
"-Ctarget-feature=+crt-static",
"-Clink-args=/DEFAULTLIB:ucrt.lib /NODEFAULTLIB:vcruntime.lib /NODEFAULTLIB:msvcrt.lib /NODEFAULTLIB:libucrt.lib"
]
# This fixes the following linker error on x86:
# error LNK2019: unresolved external symbol _NdrClientCall4 referenced in function ...
[target.'cfg(all(target_os = "windows", target_arch = "x86"))']
rustflags = ["-Clink-args=/DEFAULTLIB:rpcrt4.lib"]
# -Clink-args=/DYNAMICBASE /CETCOMPAT: Enable "shadow stack" (https://learn.microsoft.com/en-us/cpp/build/reference/cetcompat)
[target.'cfg(all(target_os = "windows", any(target_arch = "x86", target_arch = "x86_64")))']
rustflags = ["-Clink-args=/DYNAMICBASE /CETCOMPAT"]

View File

@ -0,0 +1,25 @@
# -Cehcont_guard: Enable EH Continuation Metadata (https://learn.microsoft.com/en-us/cpp/build/reference/guard-enable-eh-continuation-metadata).
# -Ccontrol-flow-guard: Enable Control Flow Guard, needed for OneBranch's post-build analysis (https://learn.microsoft.com/en-us/cpp/build/reference/guard-enable-control-flow-guard).
[target.'cfg(target_os = "windows")']
rustflags = [
"-Cehcont_guard",
"-Ccontrol-flow-guard",
"-Ctarget-feature=+crt-static",
"-Clink-args=/DEFAULTLIB:ucrt.lib /NODEFAULTLIB:vcruntime.lib /NODEFAULTLIB:msvcrt.lib /NODEFAULTLIB:libucrt.lib"
]
# This fixes the following linker error on x86:
# error LNK2019: unresolved external symbol _NdrClientCall4 referenced in function ...
[target.'cfg(all(target_os = "windows", target_arch = "x86"))']
rustflags = ["-Clink-args=/DEFAULTLIB:rpcrt4.lib"]
# -Clink-args=/DYNAMICBASE /CETCOMPAT: Enable "shadow stack" (https://learn.microsoft.com/en-us/cpp/build/reference/cetcompat)
[target.'cfg(all(target_os = "windows", any(target_arch = "x86", target_arch = "x86_64")))']
rustflags = ["-Clink-args=/DYNAMICBASE /CETCOMPAT"]
# Setup the ADO Artifacts feed as a Registry: you'll need to use your own feed in your project that upstreams from crates.io.
# For more details see https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/azure-artifacts/cargo
[registries]
sudo_public_cargo = { index = "sparse+https://pkgs.dev.azure.com/microsoft/Dart/_packaging/sudo_public_cargo/Cargo/index/" }
[source.crates-io]
replace-with = "sudo_public_cargo"

View File

@ -0,0 +1,68 @@
#################################################################################
# OneBranch Pipelines - Buddy #
# This pipeline was created by EasyStart from a sample located at: #
# https://aka.ms/obpipelines/easystart/samples #
# Documentation: https://aka.ms/obpipelines #
# Yaml Schema: https://aka.ms/obpipelines/yaml/schema #
# Retail Tasks: https://aka.ms/obpipelines/tasks #
# Support: https://aka.ms/onebranchsup #
#################################################################################
trigger:
- master
# Hourly builds: you may want to change these to nightly ("0 3 * * *" - run at 3am).
schedules:
- cron: 0 * * * *
displayName: Hourly build
branches:
include:
- master
always: true
variables:
CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning
LinuxContainerImage: 'mcr.microsoft.com/onebranch/cbl-mariner/build:2.0' # Docker image which is used to build the project https://aka.ms/obpipelines/containers
WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2019/vse2022:latest'
DEBIAN_FRONTEND: noninteractive
resources:
repositories:
- repository: templates
type: git
name: OneBranch.Pipelines/GovernedTemplates
ref: refs/heads/main
extends:
template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates
parameters:
cloudvault: # https://aka.ms/obpipelines/cloudvault
enabled: false
globalSdl: # https://aka.ms/obpipelines/sdl
binskim:
# Rust build scripts will not be built with spectre-mitigations enabled, so only scan the actual output binaries.
scanOutputDirectoryOnly: true
stages:
- stage: Build
jobs:
- job: Linux
pool:
type: linux
variables:
ob_outputDirectory: '$(Build.SourcesDirectory)/out' # More settings at https://aka.ms/obpipelines/yaml/jobs
additionalRustTargets: x86_64-unknown-linux-musl
# Cargo's default target dir is $(Build.SourcesDirectory)/target but $(Build.BinariesDirectory)/target is more appropriate.
cargo_target_dir: $(Build.BinariesDirectory)/target
steps:
- template: .pipelines/OneBranch.Common.yml@self
- job: Windows
pool:
type: windows
variables:
ob_outputDirectory: '$(Build.SourcesDirectory)/out' # More settings at https://aka.ms/obpipelines/yaml/jobs
additionalRustTargets: i686-pc-windows-msvc
# For details on this cargo_target_dir setting, see https://eng.ms/docs/more/rust/topics/onebranch-workaround
cargo_target_dir: C:\cargo_target_dir
steps:
- template: .pipelines/OneBranch.Common.yml@self

View File

@ -0,0 +1,103 @@
# Core build logic that is common to all OneBranch builds.
parameters: # parameters are shown up in ADO UI in a build queue time
- name: buildPlatforms
type: object
default:
- x86_64-pc-windows-msvc # x64
- i686-pc-windows-msvc # x86
- aarch64-pc-windows-msvc # arm64
# - # arm32?
- name: brandings
type: object
default:
- Inbox
- Stable
- Dev
- name: tracingGuid
type: string
default: ffffffff-ffff-ffff-ffff-ffffffffffff
steps:
- task: RustInstaller@1
inputs:
# Can be any "MSRustup" version, such as ms-stable, ms-1.54 or ms-stable-20210513.5 - for more details see https://mscodehub.visualstudio.com/Rust/_git/rust.msrustup
# For supported versions see https://mscodehub.visualstudio.com/Rust/_packaging?_a=package&feed=Rust&view=versions&package=rust.tools-x86_64-pc-windows-msvc&protocolType=NuGet
# We recommend setting this to a specific numbered version such as `ms-1.68` -- we do not recommend
# setting it to `ms-stable`.
# For details on this, see https://eng.ms/docs/more/rust/topics/conventions#toolchain-usage
rustVersion: ms-1.75
# Space separated list of additional targets: only the host target is supported with the toolchain by default.
#
# This doesn't actually control what gets built - only which toolchains get installed on the build agent.
#
# Theoretically, I could somehow replace this with the buildPlatforms passed in as a parameter
additionalTargets: i686-pc-windows-msvc aarch64-pc-windows-msvc x86_64-pc-windows-msvc
# URL of an Azure Artifacts feed configured with a crates.io upstream. Must be within the current ADO collection.
# NOTE: Azure Artifacts support for Rust is not yet public, but it is enabled for internal ADO organizations.
# https://learn.microsoft.com/en-us/azure/devops/artifacts/how-to/set-up-upstream-sources?view=azure-devops
cratesIoFeedOverride: sparse+https://pkgs.dev.azure.com/microsoft/Dart/_packaging/sudo_public_cargo/Cargo/index/
# URL of an Azure Artifacts NuGet feed configured with the mscodehub Rust feed as an upstream.
# * The feed must be within the current ADO collection.
# * The CI account, usually "Project Collection Build Service (org-name)", must have at least "Collaborator" permission.
# When setting up the upstream NuGet feed, use following Azure Artifacts feed locator:
# azure-feed://mscodehub/Rust/Rust@Release
toolchainFeed: https://pkgs.dev.azure.com/microsoft/_packaging/RustTools/nuget/v3/index.json
displayName: Install Rust toolchain
# We recommend making a separate `cargo fetch` step, as some build systems
# perform fetching entirely prior to the build, and perform the build with the
# network disabled.
- script: cargo fetch
displayName: Fetch crates
# First, build and test each branding.
- ${{ each brand in parameters.brandings }}:
- script: cargo build --config .cargo\ms-toolchain-config.toml --no-default-features --features ${{brand}} --frozen 2>&1
displayName: Build ${{brand}} Debug
- script: cargo test --config .cargo\ms-toolchain-config.toml --no-default-features --features ${{brand}} --frozen 2>&1
displayName: Test ${{brand}} Debug
# We suggest fmt and clippy after build and test, to catch build breaks and test
# failures as quickly as possible.
# This only needs to happen once, not per-branding.
- script: cargo fmt --check 2>&1
displayName: Check formatting
- ${{ each brand in parameters.brandings }}:
- script: cargo clippy --config .cargo\ms-toolchain-config.toml --no-default-features --features ${{brand}} --frozen -- -D warnings 2>&1
displayName: Clippy (Linting) ${{brand}}
# Build release last because it takes the longest and we should have gotten
# all available error signal by this point.
- ${{ each platform in parameters.buildPlatforms }}:
- script: cargo build --config .cargo\ms-toolchain-config.toml --no-default-features --features ${{brand}} --target ${{platform}} --frozen --release 2>&1
env:
MAGIC_TRACING_GUID: ${{ parameters.tracingGuid }}
displayName: Build ${{brand}}-${{platform}} Release
# At this point, we've completed the build, but each of the outputs is in a
# subdir specific to the architecture being built. That's okay, but the artifact
# drop won't know to look for them in there.
#
# Copy them on out.
- task: CopyFiles@2
displayName: Copy files to output (${{brand}}/${{platform}})
inputs:
sourceFolder: '$(CARGO_TARGET_DIR)/${{platform}}/release'
targetFolder: '$(ob_outputDirectory)/${{brand}}/${{platform}}'
contents: '*'
# only do this once
- task: CopyFiles@2
displayName: Copy instrumentation manifest to output
inputs:
sourceFolder: 'cpp/logging'
targetFolder: '$(ob_outputDirectory)/'
contents: 'instrumentation.man'

112
.pipelines/PR.Pipeline.yml Normal file
View File

@ -0,0 +1,112 @@
#################################################################################
# OneBranch Pipelines - Buddy #
# This pipeline was created by EasyStart from a sample located at: #
# https://aka.ms/obpipelines/easystart/samples #
# Documentation: https://aka.ms/obpipelines #
# Yaml Schema: https://aka.ms/obpipelines/yaml/schema #
# Retail Tasks: https://aka.ms/obpipelines/tasks #
# Support: https://aka.ms/onebranchsup #
#################################################################################
trigger: none
# - main
parameters: # parameters are shown up in ADO UI in a build queue time
# buildPlatforms: This controls which Rust triples we build sudo for.
# These three defaults correspond to x64, x86 and ARM64.
# They're used by:
# - The OneBranch.Common.yml, to control which --target's we build in release
# - The vpack task, below.
- name: buildPlatforms
type: object
default:
- x86_64-pc-windows-msvc # x64
- i686-pc-windows-msvc # x86
- aarch64-pc-windows-msvc # arm64
# Hourly builds: you may want to change these to nightly ("0 3 * * *" - run at 3am).
schedules:
- cron: 0 * * * *
displayName: Hourly build
branches:
include:
- master
always: true
variables:
CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning
WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2019/vse2022:latest'
# LOAD BEARING - the vpack task fails without these
ROOT: $(Build.SourcesDirectory)
REPOROOT: $(Build.SourcesDirectory)
OUTPUTROOT: $(REPOROOT)\out
NUGET_XMLDOC_MODE: none
resources:
repositories:
- repository: templates
type: git
name: OneBranch.Pipelines/GovernedTemplates
ref: refs/heads/main
extends:
# We're an official build, so we need to extend from the _Official_ build template.
template: v2/Microsoft.NonOfficial.yml@templates
parameters:
platform:
name: 'windows_undocked'
product: 'sudo'
cloudvault: # https://aka.ms/obpipelines/cloudvault
enabled: false
globalSdl: # https://aka.ms/obpipelines/sdl
binskim:
# Rust build scripts will not be built with spectre-mitigations enabled,
# so only scan the actual output binaries.
scanOutputDirectoryOnly: true
stages:
# Our Build stage will build all three targets in one job, so we don't need
# to repeat most of the boilerplate work in three separate jobs.
- stage: Build
jobs:
- job: Windows
pool:
type: windows
variables:
# Binaries will go here
ob_outputDirectory: '$(Build.SourcesDirectory)/out' # More settings at https://aka.ms/obpipelines/yaml/jobs
additionalTargets: $(parameters.buildPlatforms)
# For details on this cargo_target_dir setting, see https://eng.ms/docs/more/rust/topics/onebranch-workaround
cargo_target_dir: C:\cargo_target_dir
# The "standard" pipeline has a bunch of other variables it sets here
# to control vpack creation. However, for a PR build, we don't really
# need any of that.
steps:
# The actual build is over in Onebranch.Common.yml
- template: .pipelines/OneBranch.Common.yml@self
parameters:
buildPlatforms: ${{ parameters.buildPlatforms }}
# branding will use the default, which is set to build all of them.
# tracingGuid will use the default placeholder for PR builds
# This is very shamelessly stolen from curl's pipeline. Small
# modification: since our cargo.toml has a bunch of lines with "version",
# bail after the first.
- script: |-
rem Parse the version out of cargo.toml
for /f "tokens=3 delims=- " %%x in ('findstr /c:"version = " sudo\cargo.toml') do (@echo ##vso[task.setvariable variable=SudoVersion]%%~x & goto :EOF)
displayName: 'Set SudoVersion'
# Codesigning. Cribbed directly from the curl codesign task
- task: onebranch.pipeline.signing@1
displayName: 'Sign files'
inputs:
command: 'sign'
signing_profile: 'external_distribution'
files_to_sign: '**/*.exe'
search_root: '$(ob_outputDirectory)'
use_testsign: false
in_container: true

View File

@ -0,0 +1,179 @@
#################################################################################
# OneBranch Pipelines - Buddy #
# This pipeline was created by EasyStart from a sample located at: #
# https://aka.ms/obpipelines/easystart/samples #
# Documentation: https://aka.ms/obpipelines #
# Yaml Schema: https://aka.ms/obpipelines/yaml/schema #
# Retail Tasks: https://aka.ms/obpipelines/tasks #
# Support: https://aka.ms/onebranchsup #
#################################################################################
trigger: none
# - main
parameters: # parameters are shown up in ADO UI in a build queue time
# buildPlatforms: This controls which Rust triples we build sudo for.
# These three defaults correspond to x64, x86 and ARM64.
# They're used by:
# - The OneBranch.Common.yml, to control which --target's we build in release
# - The vpack task, below.
- name: buildPlatforms
type: object
default:
- x86_64-pc-windows-msvc # x64
- i686-pc-windows-msvc # x86
- aarch64-pc-windows-msvc # arm64
# The official builds default to just the Inbox builds
- name: brandings
type: object
default:
- Inbox
# - Stable
# - Dev
# Hourly builds: you may want to change these to nightly ("0 3 * * *" - run at 3am).
schedules:
- cron: 0 * * * *
displayName: Hourly build
branches:
include:
- master
always: true
variables:
CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning
WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2019/vse2022:latest'
# LOAD BEARING - the vpack task fails without these
ROOT: $(Build.SourcesDirectory)
REPOROOT: $(Build.SourcesDirectory)
OUTPUTROOT: $(REPOROOT)\out
NUGET_XMLDOC_MODE: none
resources:
repositories:
- repository: templates
type: git
name: OneBranch.Pipelines/GovernedTemplates
ref: refs/heads/main
extends:
# We're an official build, so we need to extend from the _Official_ build template.
template: v2/Microsoft.Official.yml@templates
parameters:
platform:
name: 'windows_undocked'
product: 'sudo'
cloudvault: # https://aka.ms/obpipelines/cloudvault
enabled: false
globalSdl: # https://aka.ms/obpipelines/sdl
binskim:
# Rust build scripts will not be built with spectre-mitigations enabled,
# so only scan the actual output binaries.
scanOutputDirectoryOnly: true
stages:
# Our Build stage will build all three targets in one job, so we don't need
# to repeat most of the boilerplate work in three separate jobs.
- stage: Build
jobs:
- job: Windows
pool:
type: windows
variables:
# Binaries will go here
ob_outputDirectory: '$(Build.SourcesDirectory)/out' # More settings at https://aka.ms/obpipelines/yaml/jobs
# The vPack gets created from stuff in here
ob_createvpack_vpackdirectory: '$(ob_outputDirectory)/vpack'
# It will have a structure like:
# .../vpack/
# - amd64/
# - sudo.exe
# - i386/
# - sudo.exe
# - arm64/
# - sudo.exe
# not sure where this goes
# additionalRustTargets: i686-pc-windows-msvc
additionalTargets: $(parameters.buildPlatforms)
# For details on this cargo_target_dir setting, see https://eng.ms/docs/more/rust/topics/onebranch-workaround
cargo_target_dir: C:\cargo_target_dir
# All these? Also variables that control the vpack creation.
ob_createvpack_enabled: true
ob_createvpack_packagename: 'windows_sudo.$(Build.SourceBranchName)'
ob_createvpack_owneralias: 'migrie@microsoft.com'
ob_createvpack_description: 'Sudo for Windows'
ob_createvpack_targetDestinationDirectory: '$(Destination)'
ob_createvpack_propsFile: false
ob_createvpack_provData: true
ob_createvpack_versionAs: string
ob_createvpack_version: '$(SudoVersion)-$(CDP_DEFINITION_BUILD_COUNT)'
ob_createvpack_metadata: '$(Build.SourceVersion)'
ob_createvpack_topLevelRetries: 0
ob_createvpack_failOnStdErr: true
ob_createvpack_taskLogVerbosity: Detailed
ob_createvpack_verbose: true
steps:
# Before we build! Right before we build, pull down localizations from
# Touchdown.
#
# The Terminal build would literally pass this as a parameter of a list
# of steps to the job-build-project.yml, which is overkill for us
- template: .pipelines/steps-fetch-and-prepare-localizations.yml@self
parameters:
includePseudoLoc: true
# The actual build is over in Onebranch.Common.yml
- template: .pipelines/OneBranch.Common.yml@self
parameters:
buildPlatforms: ${{ parameters.buildPlatforms }}
brandings: ${{ parameters.brandings }}
tracingGuid: $(SecretTracingGuid)
# This is very shamelessly stolen from curl's pipeline. Small
# modification: since our cargo.toml has a bunch of lines with "version",
# bail after the first.
- script: |-
rem Parse the version out of cargo.toml
for /f "tokens=3 delims=- " %%x in ('findstr /c:"version = " sudo\cargo.toml') do (@echo ##vso[task.setvariable variable=SudoVersion]%%~x & goto :EOF)
displayName: 'Set SudoVersion'
# Codesigning. Cribbed directly from the curl codesign task
- task: onebranch.pipeline.signing@1
displayName: 'Sign files'
inputs:
command: 'sign'
signing_profile: 'external_distribution'
files_to_sign: '**/*.exe'
search_root: '$(ob_outputDirectory)'
use_testsign: false
in_container: true
# This stage grabs each of the sudo's we've built for different
# architectures, and copies them into the appropriate vpack directory.
- ${{ each platform in parameters.buildPlatforms }}:
- task: CopyFiles@2
displayName: Copy files to vpack (${{platform}})
inputs:
# We always use the 'Inbox' branding vvvvv here - vpacks are only ever consumed by the OS
sourceFolder: '$(ob_outputDirectory)/Inbox/${{platform}}'
${{ if eq(platform, 'i686-pc-windows-msvc') }}:
targetFolder: '$(ob_createvpack_vpackdirectory)/i386'
${{ elseif eq(platform, 'x86_64-pc-windows-msvc') }}:
targetFolder: '$(ob_createvpack_vpackdirectory)/amd64'
${{ else }}: # aarch64-pc-windows-msvc
targetFolder: '$(ob_createvpack_vpackdirectory)/arm64'
contents: '*'
- task: CopyFiles@2
# only do this once
displayName: Copy manifest to vpack
inputs:
sourceFolder: '$(ob_outputDirectory)/'
targetFolder: '$(ob_createvpack_vpackdirectory)/'
contents: 'instrumentation.man'

View File

@ -0,0 +1,354 @@
# This script is used to move the resources from all the resx files in the
# directory (args[0]) to a .rc and .h file for use in C++ projects. (and a rust
# file for loading resources in Rust)
# Root directory which contains the resx files
$parentDirectory = $args[0]
# File name of the base resource.h which contains all the non-localized resource definitions
$baseHeaderFileName = $args[1]
# Target file name of the resource header file, which will be used in code - Example: resource.h
$generatedHeaderFileName = $args[2]
# File name of the base ProjectName.rc which contains all the non-localized resources
$baseRCFileName = $args[3]
# Target file name of the resource rc file, which will be used in code - Example: ProjectName.rc
$generatedRCFileName = $args[4]
# Target file name of the rust resource file, which will be used in code - Example: resource_ids.rs
$generatedRustFileName = $args[5]
# Optional argument: Initial resource id in the resource header file. By default it is 101
if ($args.Count -eq 7)
{
$initResourceID = $args[6]
}
else
{
$initResourceID = 101
}
# Flags to check if the first updated has occurred
$rcFileUpdated = $false
# $rustFileUpdated = $false
# Output folder for the new resource files. It will be in ProjectDir\Generated Files so that the files are ignored by .gitignore
$generatedFilesFolder = $parentDirectory + "\Generated Files"
# Create Generated Files folder if it doesn't exist
if (!(Test-Path -Path $generatedFilesFolder))
{
$paramNewItem = @{
Path = $generatedFilesFolder
ItemType = 'Directory'
Force = $true
}
New-Item @paramNewItem
}
# Hash table to get the language codes from the code used in the file name
$languageHashTable = @{
# This is the table straight from PowerToys
# "ar" = @("ARA", "ARABIC", "NEUTRAL", "Arabic");
# "bg" = @("BGR", "BULGARIAN", "NEUTRAL", "Bulgarian");
# "ca" = @("CAT", "CATALAN", "NEUTRAL", "Catalan");
# "cs" = @("CSY", "CZECH", "NEUTRAL", "Czech");
# "de" = @("DEU", "GERMAN", "NEUTRAL", "German");
# "en-US" = @("ENU", "ENGLISH", "ENGLISH_US", "English (United States)");
# "es" = @("ESN", "SPANISH", "NEUTRAL", "Spanish");
# "eu-ES" = @("EUQ", "BASQUE", "DEFAULT", "Basque (Basque)");
# "fr" = @("FRA", "FRENCH", "NEUTRAL", "French");
# "he" = @("HEB", "HEBREW", "NEUTRAL", "Hebrew");
# "hu" = @("HUN", "HUNGARIAN", "NEUTRAL", "Hungarian");
# "it" = @("ITA", "ITALIAN", "NEUTRAL", "Italian");
# "ja" = @("JPN", "JAPANESE", "NEUTRAL", "Japanese");
# "ko" = @("KOR", "KOREAN", "NEUTRAL", "Korean");
# "nb-NO" = @("NOR", "NORWEGIAN", "NORWEGIAN_BOKMAL", "Norwegian Bokmål (Norway)");
# "nl" = @("NLD", "DUTCH", "NEUTRAL", "Dutch");
# "pl" = @("PLK", "POLISH", "NEUTRAL", "Polish");
# "pt-BR" = @("PTB", "PORTUGUESE", "PORTUGUESE_BRAZILIAN", "Portuguese (Brazil)");
# "pt-PT" = @("PTG", "PORTUGUESE", "PORTUGUESE", "Portuguese (Portugal)");
# "ro" = @("ROM", "ROMANIAN", "NEUTRAL", "Romanian");
# "ru" = @("RUS", "RUSSIAN", "NEUTRAL", "Russian");
# "sk" = @("SKY", "SLOVAK", "NEUTRAL", "Slovak");
# "sv" = @("SVE", "SWEDISH", "NEUTRAL", "Swedish");
# "tr" = @("TRK", "TURKISH", "NEUTRAL", "Turkish");
# "zh-CN" = @("CHS", "CHINESE", "NEUTRAL", "Chinese (Simplified)");
# "zh-Hans" = @("CHS", "CHINESE", "NEUTRAL", "Chinese (Simplified)");
# "zh-Hant" = @("CHT", "CHINESE", "CHINESE_TRADITIONAL", "Chinese (Traditional)")
# "zh-TW" = @("CHT", "CHINESE", "CHINESE_TRADITIONAL", "Chinese (Traditional)")
# GENERATE ME WITH gen-lang-codes.ps1
#
# the numbers in params 1 and 2 are the language and sublanguage values for the
# rc file's LANGUAGE statement. Usually those are defined in windows.h, but we
# can't just figure out what the constant in that file is just from a language
# code. I suppose this script could probably me modified to generate these
# values on the fly from the same code in gen-lang-codes.ps1, but I'm not sure
# it's worth it.
"af-ZA" = @("AFR", "1078", "0", "Afrikaans (South Africa)");
"am-ET" = @("AMH", "1118", "0", "Amharic (Ethiopia)");
"ar-SA" = @("ARA", "1025", "0", "Arabic (Saudi Arabia)");
"as-IN" = @("ASM", "1101", "0", "Assamese (India)");
"az-Latn-AZ" = @("AZE", "1068", "0", "Azerbaijani (Latin, Azerbaijan)");
"bg-BG" = @("BUL", "1026", "0", "Bulgarian (Bulgaria)");
"bn-IN" = @("BEN", "1093", "0", "Bangla (India)");
"bs-Latn-BA" = @("BOS", "5146", "0", "Bosnian (Latin, Bosnia & Herzegovina)");
"ca-ES" = @("CAT", "1027", "0", "Catalan (Spain)");
"ca-Es-VALENCIA" = @("CAT", "2051", "0", "Catalan (Spain, Valencian)");
"cs-CZ" = @("CES", "1029", "0", "Czech (Czechia)");
"cy-GB" = @("CYM", "1106", "0", "Welsh (United Kingdom)");
"da-DK" = @("DAN", "1030", "0", "Danish (Denmark)");
"de-DE" = @("DEU", "1031", "0", "German (Germany)");
"el-GR" = @("ELL", "1032", "0", "Greek (Greece)");
"en-GB" = @("ENG", "2057", "0", "English (United Kingdom)");
"en-US" = @("ENG", "1033", "0", "English (United States)");
"es-ES" = @("SPA", "3082", "0", "Spanish (Spain)");
"es-MX" = @("SPA", "2058", "0", "Spanish (Mexico)");
"et-EE" = @("EST", "1061", "0", "Estonian (Estonia)");
"eu-ES" = @("EUS", "1069", "0", "Basque (Spain)");
"fa-IR" = @("FAS", "1065", "0", "Persian (Iran)");
"fi-FI" = @("FIN", "1035", "0", "Finnish (Finland)");
"fil-PH" = @("FIL", "1124", "0", "Filipino (Philippines)");
"fr-CA" = @("FRA", "3084", "0", "French (Canada)");
"fr-FR" = @("FRA", "1036", "0", "French (France)");
"ga-IE" = @("GLE", "2108", "0", "Irish (Ireland)");
"gd-gb" = @("GLA", "1169", "0", "Scottish Gaelic (United Kingdom)");
"gl-ES" = @("GLG", "1110", "0", "Galician (Spain)");
"gu-IN" = @("GUJ", "1095", "0", "Gujarati (India)");
"he-IL" = @("HEB", "1037", "0", "Hebrew (Israel)");
"hi-IN" = @("HIN", "1081", "0", "Hindi (India)");
"hr-HR" = @("HRV", "1050", "0", "Croatian (Croatia)");
"hu-HU" = @("HUN", "1038", "0", "Hungarian (Hungary)");
"hy-AM" = @("HYE", "1067", "0", "Armenian (Armenia)");
"id-ID" = @("IND", "1057", "0", "Indonesian (Indonesia)");
"is-IS" = @("ISL", "1039", "0", "Icelandic (Iceland)");
"it-IT" = @("ITA", "1040", "0", "Italian (Italy)");
"ja-JP" = @("JPN", "1041", "0", "Japanese (Japan)");
"ka-GE" = @("KAT", "1079", "0", "Georgian (Georgia)");
"kk-KZ" = @("KAZ", "1087", "0", "Kazakh (Kazakhstan)");
"km-KH" = @("KHM", "1107", "0", "Khmer (Cambodia)");
"kn-IN" = @("KAN", "1099", "0", "Kannada (India)");
"ko-KR" = @("KOR", "1042", "0", "Korean (Korea)");
"kok-IN" = @("KOK", "1111", "0", "Konkani (India)");
"lb-LU" = @("LTZ", "1134", "0", "Luxembourgish (Luxembourg)");
"lo-LA" = @("LAO", "1108", "0", "Lao (Laos)");
"lt-LT" = @("LIT", "1063", "0", "Lithuanian (Lithuania)");
"lv-LV" = @("LAV", "1062", "0", "Latvian (Latvia)");
"mi-NZ" = @("MRI", "1153", "0", "Māori (New Zealand)");
"mk-MK" = @("MKD", "1071", "0", "Macedonian (North Macedonia)");
"ml-IN" = @("MAL", "1100", "0", "Malayalam (India)");
"mr-IN" = @("MAR", "1102", "0", "Marathi (India)");
"ms-MY" = @("MSA", "1086", "0", "Malay (Malaysia)");
"mt-MT" = @("MLT", "1082", "0", "Maltese (Malta)");
"nb-NO" = @("NOB", "1044", "0", "Norwegian Bokmål (Norway)");
"ne-NP" = @("NEP", "1121", "0", "Nepali (Nepal)");
"nl-NL" = @("NLD", "1043", "0", "Dutch (Netherlands)");
"nn-NO" = @("NNO", "2068", "0", "Norwegian Nynorsk (Norway)");
"or-IN" = @("ORI", "1096", "0", "Odia (India)");
"pa-IN" = @("PAN", "1094", "0", "Punjabi (India)");
"pl-PL" = @("POL", "1045", "0", "Polish (Poland)");
"pt-BR" = @("POR", "1046", "0", "Portuguese (Brazil)");
"pt-PT" = @("POR", "2070", "0", "Portuguese (Portugal)");
"qps-ploc" = @("", "1281", "0", "qps (Ploc)");
"qps-ploca" = @("", "1534", "0", "qps (PLOCA)");
"qps-plocm" = @("", "2559", "0", "qps (PLOCM)");
"quz-PE" = @("", "3179", "0", "Quechua (Peru)");
"ro-RO" = @("RON", "1048", "0", "Romanian (Romania)");
"ru-RU" = @("RUS", "1049", "0", "Russian (Russia)");
"sk-SK" = @("SLK", "1051", "0", "Slovak (Slovakia)");
"sl-SI" = @("SLV", "1060", "0", "Slovenian (Slovenia)");
"sq-AL" = @("SQI", "1052", "0", "Albanian (Albania)");
"sr-Cyrl-BA" = @("SRP", "7194", "0", "Serbian (Cyrillic, Bosnia & Herzegovina)");
"sr-Cyrl-RS" = @("SRP", "10266", "0", "Serbian (Cyrillic, Serbia)");
"sr-Latn-RS" = @("SRP", "9242", "0", "Serbian (Latin, Serbia)");
"sv-SE" = @("SWE", "1053", "0", "Swedish (Sweden)");
"ta-IN" = @("TAM", "1097", "0", "Tamil (India)");
"te-IN" = @("TEL", "1098", "0", "Telugu (India)");
"th-TH" = @("THA", "1054", "0", "Thai (Thailand)");
"tr-TR" = @("TUR", "1055", "0", "Turkish (Türkiye)");
"tt-RU" = @("TAT", "1092", "0", "Tatar (Russia)");
"ug-CN" = @("UIG", "1152", "0", "Uyghur (China)");
"uk-UA" = @("UKR", "1058", "0", "Ukrainian (Ukraine)");
"ur-PK" = @("URD", "1056", "0", "Urdu (Pakistan)");
"uz-Latn-UZ" = @("UZB", "1091", "0", "Uzbek (Latin, Uzbekistan)");
"vi-VN" = @("VIE", "1066", "0", "Vietnamese (Vietnam)");
"zh-CN" = @("ZHO", "2052", "0", "Chinese (China)");
"zh-TW" = @("ZHO", "1028", "0", "Chinese (Taiwan)");
}
# Store the content to be written to a buffer
$rcFileContent = ""
# Start by pre-populating the header file with a warning and the contents of the
# base header file. Do this only once, we'll append generated content to this
# later.
$headerFileContent = "// This file was auto-generated. Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.`r`n"
$rustFileContent = $headerFileContent; # The rust file doesn't have a base currently. We didn't need one.
try {
$headerFileContent += (Get-Content $parentDirectory\$baseHeaderFileName -Raw)
}
catch {
echo "Failed to read base header file."
exit 0
}
$lastResourceID = $initResourceID
# Iterate over all resx files in parent directory
Get-ChildItem $parentDirectory -Recurse -Filter *.resw |
Foreach-Object {
Write-Host "Processing $($_.FullName)"
$xmlDocument = $null
try {
$xmlDocument = [xml](Get-Content $_.FullName -ErrorAction:Stop)
}
catch {
Write-Host "Failed to load $($_.FullName)"
exit 0
}
# Get language code from file name
$lang = "en"
$tokens = $_.Name -split "\."
if ($tokens.Count -eq 3) {
$lang = $tokens[1]
} else {
$d = $_.Directory.Name
If ($d.Contains('-')) { # Looks like a language directory
$lang = $d
}
}
$langData = $languageHashTable[$lang]
if ($null -eq $langData -and $lang.Contains('-')) {
# Modern Localization comes in with language + country tuples;
# we want to detect the language alone if we don't support the language-country
# version.
$lang = ($lang -split "-")[0]
$langData = $languageHashTable[$lang]
}
if ($null -eq $langData) {
Write-Warning "Unknown language $lang"
Return
}
$newLinesForRCFile = ""
$newLinesForHeaderFile = ""
$newLinesForRustFile = ""
try {
foreach ($entry in $xmlDocument.root.data) {
$culture = [System.Globalization.CultureInfo]::GetCultureInfo('en-US')
# Each resource is named as IDS_ResxResourceName, in uppercase. Escape occurrences of double quotes in the string
$lineInRCFormat = "IDS_" + $entry.name.ToUpper($culture) + " L`"" + $entry.value.Replace("`"", "`"`"") + "`""
$newLinesForRCFile = $newLinesForRCFile + "`r`n " + $lineInRCFormat
# Resource header & rust file needs to be updated only for one
# language - en-US, where our strings are first authored. This is to
# avoid duplicate entries in the header file.
if ($lang -eq "en-US") {
$lineInHeaderFormat = "#define IDS_" + $entry.name.ToUpper($culture) + " " + $lastResourceID.ToString()
$newLinesForHeaderFile = $newLinesForHeaderFile + "`r`n" + $lineInHeaderFormat
$lineInRustFormat = "string_resources! { IDS_$($entry.name.ToUpper($culture)) = $($lastResourceID.ToString()); }"
$newLinesForRustFile = $newLinesForRustFile + "`r`n" + $lineInRustFormat
$lastResourceID++
}
}
}
catch {
echo "Failed to read XML document."
exit 0
}
if ($newLinesForRCFile -ne "") {
# Add string table syntax
$newLinesForRCFile = "`r`nSTRINGTABLE`r`nBEGIN" + $newLinesForRCFile + "`r`nEND"
$langStart = "`r`n/////////////////////////////////////////////////////////////////////////////`r`n// " + $langData[3] + " resources`r`n`r`n"
# $langStart += "#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_" + $langData[0] + ")`r`nLANGUAGE LANG_" + $langData[1] + ", SUBLANG_" + $langData[2] + "`r`n"
$langStart += "#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_" + $langData[0] + ")`r`nLANGUAGE " + $langData[1] + ", " + $langData[2] + "`r`n"
$langEnd = "`r`n`r`n#endif // " + $langData[3] + " resources`r`n/////////////////////////////////////////////////////////////////////////////`r`n"
$newLinesForRCFile = $langStart + $newLinesForRCFile + $langEnd
}
# Initialize the rc file with an auto-generation warning and content from the base rc
if (!$rcFileUpdated) {
$rcFileContent = "// This file was auto-generated. Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.`r`n"
try {
$rcFileContent += (Get-Content $parentDirectory\$baseRCFileName -Raw)
}
catch {
echo "Failed to read base rc file."
exit 0
}
$rcFileUpdated = $true
}
# Add in the new string table to the rc file
$rcFileContent += $newLinesForRCFile
# Here we deviate more from the original script. We've got multiple resw
# files to source, and we need to include the resource IDs from all of them
# in the final header (and .rs file).
#
# Our main resw file is the en-US one, so we only ever assembled additional
# header & rust lines in the case that the language for this resw file was
# en-US.
#
# Now that we have those lines, stick them in the header and rust files.
$headerFileContent += $newLinesForHeaderFile
$rustFileContent += $newLinesForRustFile
}
# Write to header file if the content has changed or if the file doesnt exist
try {
if (!(Test-Path -Path $generatedFilesFolder\$generatedHeaderFileName) -or (($headerFileContent + "`r`n") -ne (Get-Content $generatedFilesFolder\$generatedHeaderFileName -Raw))) {
Set-Content -Path $generatedFilesFolder\$generatedHeaderFileName -Value $headerFileContent -Encoding "utf8"
}
else {
# echo "Skipping write to generated header file"
}
}
catch {
echo "Failed to access generated header file."
exit 0
}
# Write to rc file if the content has changed or if the file doesnt exist
try {
if (!(Test-Path -Path $generatedFilesFolder\$generatedRCFileName) -or (($rcFileContent + "`r`n") -ne (Get-Content $generatedFilesFolder\$generatedRCFileName -Raw))) {
Set-Content -Path $generatedFilesFolder\$generatedRCFileName -Value $rcFileContent -Encoding "utf8"
}
else {
# echo "Skipping write to generated rc file"
}
}
catch {
echo "Failed to access generated rc file."
exit 0
}
# Write to rust file if the content has changed or if the file doesnt exist
try {
if (!(Test-Path -Path $generatedFilesFolder\$generatedRustFileName) -or (($rustFileContent + "`r`n") -ne (Get-Content $generatedFilesFolder\$generatedRustFileName -Raw))) {
Set-Content -Path $generatedFilesFolder\$generatedRustFileName -Value $rustFileContent -Encoding "utf8"
}
else {
# echo "Skipping write to generated header file"
}
}
catch {
echo "Failed to access generated rust file."
exit 0
}

View File

@ -0,0 +1,49 @@
trigger: none
pr: none
schedules:
- cron: "0 3 * * 2-6" # Run at 03:00 UTC Tuesday through Saturday (After the work day in Pacific, Mon-Fri)
displayName: "Nightly Localization Build"
branches:
include:
- main
always: false # only run if there's code changes!
pool:
vmImage: windows-2022
resources:
repositories:
- repository: self
type: git
ref: main
steps:
- checkout: self
clean: true
submodules: false
fetchDepth: 1 # Don't need a deep checkout for loc files!
fetchTags: false # Tags still result in depth > 1 fetch; we don't need them here
persistCredentials: true
- task: MicrosoftTDBuild.tdbuild-task.tdbuild-task.TouchdownBuildTask@3
displayName: 'Touchdown Build - 92350, PRODEXT'
inputs:
teamId: 92350
TDBuildServiceConnection: $(TouchdownServiceConnection)
authType: SubjectNameIssuer
resourceFilePath: |
**\en-US\*.resw
outputDirectoryRoot: LocOutput
appendRelativeDir: true
pseudoSetting: Included
# Saving one of these makes it really easy to inspect the loc output...
- powershell: 'tar czf LocOutput.tar.gz LocOutput'
displayName: 'Archive Loc Output for Submission'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: LocOutput'
inputs:
PathtoPublish: LocOutput.tar.gz
ArtifactName: LocOutput

View File

@ -0,0 +1,23 @@
parameters:
- name: includePseudoLoc
type: boolean
default: true
steps:
- task: TouchdownBuildTask@3
displayName: Download Localization Files
inputs:
teamId: 92350
TDBuildServiceConnection: $(TouchdownServiceConnection)
authType: SubjectNameIssuer
resourceFilePath: |
**\en-US\*.resw
appendRelativeDir: true
localizationTarget: false
${{ if eq(parameters.includePseudoLoc, true) }}:
pseudoSetting: Included
- pwsh: |-
$Files = Get-ChildItem . -R -Filter 'Resources.resw' | ? FullName -Like '*en-US\*\Resources.resw'
$Files | % { Move-Item -Verbose $_.Directory $_.Directory.Parent.Parent -EA:Ignore }
displayName: Move Loc files into final locations

75
Building.md Normal file
View File

@ -0,0 +1,75 @@
# Building Sudo for Windows
Sudo for Windows is a Rust project. If you're new to Rust, you can get started with the [Rust Book](https://doc.rust-lang.org/book/). You can quickly get started with rust by installing and running `rustup`:
```cmd
winget install --id Rustlang.rustup --source winget
rustup update
```
## Building
Rust is nice and straightforward. You can build sudo for the default architecture with a simple
```
cargo build
```
You may want to specify a specific architecture. To do that, you'll want instead:
```
cargo build --target x86_64-pc-windows-msvc
```
(You can also use `i686-pc-windows-msvc` as the target).
### Running tests
Assuming that you passed a target architecture above:
```
cargo test --target x86_64-pc-windows-msvc
```
We have additional manual tests that you can use to validate sudo in the
`tools\tests.ipynb` notebook.
### Formatting and clippy
```
cargo fmt
cargo clippy
```
If your code passes a `cargo build && cargo test && cargo fmt && cargo clippy`, you're ready to send a PR.
### Notes on building with the Microsoft internal toolchain
When we're building this project internally, we need to use an internally-maintained fork of the rust toolchain. This toolchain needs to be used for all production work at Microsoft so we can stay compliant with Secure Development Lifecycle (SDL) requirements.
**If you're external to Microsoft, this next section doesn't apply to you**. You
can use the standard Rust toolchain.
First, install the internal `msrustup` toolchain to install the right version of
all our Rust tools. You can get it from the https://aka.ms/msrustup-win. After
that installs, then you'll probably also need to run the following:
```
rustup default ms-stable
```
That'll select the ms toolchain as the default. If you ever want to switch back, you can always just run
```
rustup default stable-x86_64-pc-windows-msvc
```
Additionally, we've got a separate fork of our `.cargo/config.toml` we need to use for internal builds. Notably, this includes `-Cehcont_guard` to enable EH Continuation Metadata. It also redirects cargo to use our own package feed for dependencies.
You can manually build with that config with:
```
cargo build --config .cargo\ms-toolchain-config.toml
```
Note, if you run that on the public toolchain, you'll most definitely run into ``error: unknown codegen option: `ehcont_guard` `` when building.

View File

@ -1,10 +1,5 @@
# Sudo for Windows Contributor's Guide
**Sudo for Windows is not currently open source**. We're still in the process of
crossing our "t"'s and dotting our "i"s. Stay tuned for more updates. In the
meantime, we are still accepting issues, feature requests, and contributions to
the [`sudo.ps1`] script.
Below is our guidance for how to report issues, propose new features, and submit
contributions via Pull Requests (PRs). Our philosophy is heavily based around
understanding the problem and scenarios first, this is why we follow this
@ -63,7 +58,52 @@ and fix them. However we currently don't accept community Pull Requests fixing
localization issues. Localization is handled by the internal Microsoft team
only.
### Repo Bot
## Contributing code
As you might imagine, shipping something inside of Windows is a complicated
process--doubly so for a security component. There is a lot of validation and
paperwork that we don't really want to subject the community to. We want you to
be able to contribute easily and freely, and to let us deal with the paperwork.
We'll do our best to make sure this process is as seamless as possible.
To support the community in building new feature areas for Sudo for Windows,
we're going to make extensive use of feature flags, to conditionally add new
features to Sudo for Windows.
When contributing to Sudo for Windows, we will treat "bugfixes" and "features"
separately. Bug fixes can be merged into the codebase freely, so long as they
don't majorly change existing behaviors. New features will need to have their
code guarded by feature flag checks before we can accept them as contributions.
As always, filing issues on the repo is the best way to have the team evaluate
if the change you're proposing would be considered a "bug fix" or "feature"
work. We will indicate which is which using the `Issue-Bug` and
`Issue-Feature`/`Issue-Task` labels. These labels are intended to be informative,
and may change throughout the lifecycle of a discussion.
We'll be grouping sets of feature flags into different "branding"s throughout
the project. These brandings are as follows:
* **"Inbox"**: These are features that are included in the version of sudo that
ships with Windows itself.
* **"Stable"**: Features that ship in stable versions of sudo released here on
GitHub and on WinGet.
* **"Dev"**: The least stable features, which are only built in local development
builds. These are for work-in-progress features, that aren't quite ready for
public consumption
All new features should be added under the "Dev" branding first. The core team
will then be responsible for moving those features into the appropriate branding
as we get internal signoffs. This will allow features to be worked on
continually in the open, while we slowly roll them into the OS product. We
unfortunately cannot provide timelines for when features will be able to move
from Stable into Inbox. Historical data showing that a feature has a track
record of being stable and secure is a great way for us to justify any
particular feature's inclusion into the product.
If you're ready to jump in, head on over to [Building.md](./Building.md) to get
started.
## Repo Bot
The team triages new issues several times a week. During triage, the team uses
labels to categorize, manage, and drive the project workflow.

516
Cargo.lock generated Normal file
View File

@ -0,0 +1,516 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anstream"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "clap"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "either"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]]
name = "embed-manifest"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cd446c890d6bed1d8b53acef5f240069ebef91d6fae7c5f52efe61fe8b5eae"
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys",
]
[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustix"
version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
"bitflags 2.4.2",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.49",
]
[[package]]
name = "sha1_smol"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "sudo"
version = "1.0.0"
dependencies = [
"cc",
"clap",
"embed-manifest",
"sudo_events",
"which",
"win32resources",
"windows",
"windows-registry",
"winres",
]
[[package]]
name = "sudo_events"
version = "0.1.0"
dependencies = [
"win_etw_macros",
"win_etw_provider",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
dependencies = [
"sha1_smol",
]
[[package]]
name = "w32-error"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7c61a6bd91e168c12fc170985725340f6b458eb6f971d1cf6c34f74ffafb43"
dependencies = [
"winapi",
]
[[package]]
name = "which"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c"
dependencies = [
"either",
"home",
"once_cell",
"rustix",
"windows-sys",
]
[[package]]
name = "widestring"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8"
[[package]]
name = "win32resources"
version = "0.1.0"
[[package]]
name = "win_etw_macros"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ec5071e615bb8b34c39dc852f82df53f29cb7108a93324a7101f7b2e6284b0e"
dependencies = [
"proc-macro2",
"quote",
"sha1_smol",
"syn 1.0.109",
"uuid",
"win_etw_metadata",
]
[[package]]
name = "win_etw_metadata"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e50d0fa665033a19ecefd281b4fb5481eba2972dedbb5ec129c9392a206d652f"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "win_etw_provider"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7f61e9dfafedf5eb4348902f2a32d326f2371245d05f012cdc67b9251ad6ea3"
dependencies = [
"w32-error",
"widestring",
"win_etw_metadata",
"winapi",
"zerocopy",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
dependencies = [
"windows-core",
"windows-targets",
]
[[package]]
name = "windows-core"
version = "0.54.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
dependencies = [
"windows-result",
"windows-targets",
]
[[package]]
name = "windows-registry"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e885d2dff8cad07e7451b78eac1ff62f958825c4598d6ddf87e7d2661980c1c"
dependencies = [
"windows-result",
"windows-targets",
]
[[package]]
name = "windows-result"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd19df78e5168dfb0aedc343d1d1b8d422ab2db6756d2dc3fef75035402a3f64"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f"
[[package]]
name = "windows_i686_gnu"
version = "0.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb"
[[package]]
name = "windows_i686_msvc"
version = "0.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6"
[[package]]
name = "winres"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
dependencies = [
"toml",
]
[[package]]
name = "zerocopy"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.49",
]

45
Cargo.toml Normal file
View File

@ -0,0 +1,45 @@
[workspace]
resolver = "2"
members = [
"sudo",
"sudo_events",
"win32resources",
]
# This list of dependencies allows us to specify version numbers for dependency in a single place.
# The dependencies in this list are _not_ automatically added to crates (Cargo.toml files).
# Each individual Cargo.toml file must explicitly declare its dependencies. To use a dependency
# from this list, specify "foo.workspace = true". For example:
#
# [dependencies]
# log.workspace = true
#
# See: https://doc.rust-lang.org/cargo/reference/workspaces.html#the-dependencies-table
#
[workspace.dependencies]
cc = "1.0"
# We're disabling the default features for clap because we don't need the
# "suggestions" feature. That provides really amazing suggestions for typos, but
# it unfortunately does not seem to support localization.
#
# To use clap at all, you do need the std feature enabled, so enable that.
#
# See: https://docs.rs/clap/latest/clap/_features/index.html
clap = { version = "4.4.7", default_features = false, features = ["std"] }
embed-manifest = "1.4"
which = "6.0"
win_etw_provider = "0.1.8"
win_etw_macros = "0.1.8"
windows = "0.54"
windows-registry = "0.1"
winres = "0.1"
# For more profile settings, and details on the ones below, see https://doc.rust-lang.org/cargo/reference/profiles.html#profile-settings
[profile.release]
# Enable full debug info for optimized builds.
debug = "full"
# Split debuginfo into its own file to reduce binary size.
split-debuginfo = "packed"
lto = true
panic = "abort"

View File

@ -0,0 +1,7 @@
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <evntprov.h>
#include "instrumentation.h" // Generated from manifest

28
cpp/logging/README.md Normal file
View File

@ -0,0 +1,28 @@
## Helpful info for Event Viewer logging
This C++ project logs to the Windows Event Viewer. It's all wired up to be called from Rust just the same as our RPC code. If you want to test changes here:
1. Make sure to go change the `resourceFileName` and the `messageFileName` in
`instrumentation.man` to point at where the files are in your build
directory. (For me, that was
`D:\dev\private\sudo\target\x86_64-pc-windows-msvc\debug\sudo.exe`). It needs
to be the full path, so Event Viewer can find the exe (to load the resources
from it to know how to format the packet of binary data written to it)
- Make sure to change it back to `%systemroot%\System32\sudo.exe` before you push!
2. Make sure that Event Viewer is closed, and do
```bat
wevtutil um cpp\logging\instrumentation.man
```
to remove the old manifest from event viewer
3. Build the project
4. Do a
```bat
wevtutil im cpp\logging\instrumentation.man
```
to install the new manifest to event viewer
5. Open event viewer, and navigate to "Applications and Services Logs" ->
"Microsoft" -> "Windows" -> "Sudo" -> "Admin"
- alternatively:
```bat
wevtutil qe Microsoft-Windows-Sudo/Admin /c:3 /rd:true /f:text
```

View File

@ -0,0 +1,93 @@
<instrumentationManifest
xmlns="http://schemas.microsoft.com/win/2004/08/events"
xmlns:win="http://manifests.microsoft.com/win/2004/08/windows/events"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
<instrumentation>
<events>
<provider name="Microsoft-Windows-Sudo"
guid="{9d74dc62-b75f-54cd-be9e-c28940b5feed}"
symbol="PROVIDER_GUID"
resourceFileName="%systemroot%\System32\sudo.exe"
messageFileName="%systemroot%\System32\sudo.exe"
message="$(string.Provider.Name)">
<!-- Note to reviewers! Make sure the above both point at %systemroot%\System32\sudo.exe -->
<keywords>
<keyword name="Client" symbol="CLIENT_KEYWORD" mask="0x1" />
<keyword name="Server" symbol="SERVER_KEYWORD" mask="0x2" />
</keywords>
<channels>
<channel chid="c1"
name="Microsoft-Windows-Sudo/Admin"
type="Admin"
enabled="true"
/>
</channels>
<templates>
<template tid="SudoCommandlineTemplate">
<data name="Application" inType="win:AnsiString" outType="win:Utf8" />
<data name="ArgsCount" inType="win:UInt32" />
<data name="Argument" inType="win:AnsiString" outType="win:Utf8" count="ArgsCount" />
<data name="CurrentWorkingDirectory" inType="win:AnsiString" outType="win:Utf8" />
<data name="Mode" inType="win:UInt32" />
<data name="InheritEnvironment" inType="win:UInt8" outType="xs:boolean" />
<data name="Redirected" inType="win:UInt8" outType="xs:boolean" />
<data name="FullCommandline" inType="win:AnsiString" outType="win:Utf8" />
<data name="RequestID" inType="win:GUID"/>
<UserData>
<EventData xmlns="ProviderNamespace">
<Application> %1 </Application>
<ArgsCount> %2 </ArgsCount>
<Argument> %3 </Argument>
<CurrentWorkingDirectory> %4 </CurrentWorkingDirectory>
<Mode> %5 </Mode>
<InheritEnvironment> %6 </InheritEnvironment>
<Redirected> %7 </Redirected>
<FullCommandline> %8 </FullCommandline>
<RequestID> %9 </RequestID>
</EventData>
</UserData>
</template>
</templates>
<events>
<event value="1"
level="win:Informational"
template="SudoCommandlineTemplate"
symbol="SudoRequestRunEvent"
message="$(string.Event.FullSudoCommandline)"
channel="c1"
keywords="Client" />
<event value="2"
level="win:Informational"
template="SudoCommandlineTemplate"
symbol="SudoRecieveRunRequestEvent"
message="$(string.Event.FullSudoCommandline)"
channel="c1"
keywords="Server" />
</events>
</provider>
</events>
</instrumentation>
<localization>
<resources culture="en-US">
<stringTable>
<string id="Provider.Name" value="Microsoft-Windows-Sudo"/>
<string id="Event.FullSudoCommandline" value="%8"/>
</stringTable>
</resources>
</localization>
</instrumentationManifest>

17
cpp/rpc/README.md Normal file
View File

@ -0,0 +1,17 @@
# Sudo RPC library
To do local RPC, we need to use midl to generate function bindings for our RPC
interface, which is defined in `sudo_rpc.idl`. midl expects implementations and
callbacks via C functions which we can do in Rust, but there's one problem:
Error handling on the client side occurs with structed exceptions (SEH).
Those cannot be easily replicated in pure Rust and so the client side calls
are all wrapped in C functions.
Changes here go as follows:
* Change the interface in `sudo_rpc.idl`
* Write a client-side wrapper in `RpcClient.c` in the style of the other ones
* Implement a client-side wrapper Rust in `rpc_bindings_client.rs`
* Implement the server-side part in `rpc_bindings_server.rs`
Be careful about the function definitions. At the moment, there are no checks
in place that ensure that the Rust code matches the C code or the .idl file.

82
cpp/rpc/RpcClient.c Normal file
View File

@ -0,0 +1,82 @@
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <stdbool.h>
#include <stdlib.h>
// Our generated header file
#include "sudo_rpc.h"
// Rust can't (easily) handle SEH exceptions, which the RPC however unfortunately uses.
// And so this wrapper C implementation exists.
// From <wil/rpc_helpers.h>:
// Some RPC exceptions are already HRESULTs. Others are in the regular Win32
// error space. If the incoming exception code isn't an HRESULT, wrap it.
inline HRESULT map_rpc_status(DWORD code)
{
return IS_ERROR(code) ? code : HRESULT_FROM_WIN32(code);
}
HRESULT seh_wrapper_client_DoElevationRequest(
RPC_IF_HANDLE binding,
HANDLE parent_handle,
const HANDLE* pipe_handles,
const HANDLE* file_handles,
DWORD sudo_mode,
UTF8_STRING application,
UTF8_STRING args,
UTF8_STRING target_dir,
UTF8_STRING env_vars,
GUID eventId,
HANDLE* child)
{
RpcTryExcept
{
return client_DoElevationRequest(
binding,
parent_handle,
pipe_handles,
file_handles,
sudo_mode,
application,
args,
target_dir,
env_vars,
eventId,
child);
}
RpcExcept(RpcExceptionFilter(RpcExceptionCode()))
{
return map_rpc_status(RpcExceptionCode());
}
RpcEndExcept;
}
HRESULT seh_wrapper_client_Shutdown(RPC_IF_HANDLE binding)
{
RpcTryExcept
{
client_Shutdown(binding);
return S_OK;
}
RpcExcept(RpcExceptionFilter(RpcExceptionCode()))
{
return map_rpc_status(RpcExceptionCode());
}
RpcEndExcept;
}
/******************************************************/
/* MIDL allocate and free */
/******************************************************/
void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len)
{
return (malloc(len));
}
void __RPC_USER midl_user_free(void __RPC_FAR* ptr)
{
free(ptr);
}

5
cpp/rpc/sudo_rpc.acf Normal file
View File

@ -0,0 +1,5 @@
[implicit_handle(handle_t sudo_rpc_IfHandle)]
interface sudo_rpc
{
}

30
cpp/rpc/sudo_rpc.idl Normal file
View File

@ -0,0 +1,30 @@
// This is where GUID is defined:
import "wtypesbase.idl";
typedef struct tagUTF8_STRING {
DWORD length;
[size_is(length)] const unsigned char *data;
} UTF8_STRING;
[
uuid (f691b703-f681-47dc-afcd-034b2faab911), // You must change this when you change the interface
version(1.0),
]
interface sudo_rpc
{
HRESULT DoElevationRequest(
[in] handle_t binding,
[in, system_handle(sh_process)] HANDLE parent_handle,
[in, system_handle(sh_pipe), unique, size_is(3)] const HANDLE* pipe_handles, // in, out, err
[in, system_handle(sh_file), unique, size_is(3)] const HANDLE* file_handles, // in, out, err
[in] DWORD sudo_mode,
[in] UTF8_STRING application,
[in] UTF8_STRING args, // a null-delimited list
[in] UTF8_STRING target_dir,
[in] UTF8_STRING env_vars, // a null-delimited list
[in] GUID eventId,
[out, system_handle(sh_process)] HANDLE* child
);
void Shutdown([in] handle_t h1);
}

28
docs/generate.bat Normal file
View File

@ -0,0 +1,28 @@
@echo off
@rem calculate the next version number based on whatever the last draft-xyz.docx file is
@rem this is a bit of a hack, but it works
@rem get the last draft file
for /f "delims=" %%a in ('dir /b /on draft-*.docx') do set lastdraft=%%a
@rem if we didn't find an existing one, start with 000
if "%lastdraft%"=="" set lastdraft=draft-000.docx
@rem get the version number from the last draft file
for /f "tokens=2 delims=-." %%a in ("%lastdraft%") do set /a version=%%a+1
echo Generating draft-%version%.docx...
@rem create the new draft file
@rem
@rem mermaid-filter.cmd is from github.com/raghur/mermaid-filter. That's deeply
@rem out of date, so some of the newer features are missing. You can manually
@rem patch the mermaid.min.js if you want though.
pandoc -s -F mermaid-filter.cmd --from=markdown+yaml_metadata_block --to=docx .\draft.md -o .\draft-%version%.docx
@rem delete mermaid-filter.err, if it's empty
if exist mermaid-filter.err (
for %%a in (mermaid-filter.err) do if %%~za==0 del mermaid-filter.err
)

View File

@ -0,0 +1,17 @@
local stringify = (require "pandoc.utils").stringify
function BlockQuote (el)
start = el.content[1]
if (start.t == "Para" and start.content[1].t == "Str" and
start.content[1].text:match("^%[!%w+%][-+]?$")) then
_, _, ctype = start.content[1].text:find("%[!(%w+)%]")
el.content:remove(1)
start.content:remove(1)
div = pandoc.Div(el.content, {class = "callout"})
div.attributes["data-callout"] = ctype:lower()
div.attributes["title"] = stringify(start.content):gsub("^ ", "")
return div
else
return el
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
docs/sudo-conhost.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/sudo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

22
docs/versions.md Normal file
View File

@ -0,0 +1,22 @@
# Sudo for Windows - Versions
There are a few different versions of Sudo for Windows - this doc aims to
outline the differences between them. Each includes different sets of code, and
releases in different cadences.
* **"Inbox"**: This is the version of Sudo that ships with Windows itself. This
is the most stable version, and might only include a subset of the features in
the source code. This is delivered with the OS, via servicing upgrades.
- Build this version with `cargo build --no-default-features --features Inbox`
* **"Stable"**: The stable version of Sudo for Windows which ships out of this
repo. This can be installed side-by-side with the inbox version.
- Build this version with `cargo build --no-default-features --features Stable`
* **"Dev"**: This is a local-only build of sudo. This has all the bits of code
turned on, for the most up-to-date version of the code.
- Build this version with `cargo build`
Dev builds are the default for local compilation, to make the development inner loop the
easiest.
For more info, see "[Contributing code](./Contributing.md#contributing-code)" in
the contributors guide.

19
enable_sudo.cmd Normal file
View File

@ -0,0 +1,19 @@
@echo off
net session >nul 2>&1
if %errorLevel% == 0 (
goto :do_it
)
echo You need to be admin to enable sudo!
goto :exit
:do_it
echo Enabling sudo...
set key=HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Sudo
set value=Enabled
set data=3
reg add "%key%" /v "%value%" /t REG_DWORD /d %data% /f
:exit

83
sudo/Cargo.toml Normal file
View File

@ -0,0 +1,83 @@
[package]
name = "sudo"
version = "1.0.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
build = "build.rs"
[[bin]]
test = true
name = "sudo"
[build-dependencies]
winres.workspace = true
cc.workspace = true
embed-manifest.workspace = true
which = { workspace = true }
[dependencies]
clap = { workspace = true, default_features = false, features = ["color", "help", "usage", "error-context"] }
which = { workspace = true }
windows-registry = { workspace = true }
sudo_events = { path = "../sudo_events" }
win32resources = { path = "../win32resources" }
[dependencies.windows]
workspace = true
features = [
"Wdk_Foundation",
"Wdk_System_Threading",
"Win32_Foundation",
"Win32_Globalization",
"Win32_Security",
"Win32_Security_Authorization",
"Win32_Storage_FileSystem",
"Win32_System_Console",
"Win32_System_Diagnostics_Debug",
"Win32_System_Diagnostics_Etw",
"Win32_System_Environment",
"Win32_System_Kernel",
"Win32_System_Memory",
"Win32_System_Registry",
"Win32_System_Rpc",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_WindowsProgramming",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
]
[features]
# We attempt to use feature flags in a similar way to how the rest of the
# Windows codebase does. We've got a set of "brandings", each which contain a
# set of feature flags. Each branding is a superset of the previous branding,
# and is progressively "less stable" that the previous.
#
# The idea is that we can build with a specific branding, and get all the
# features that are enabled for that branding, plus all the "more stable" ones.
#
# We default to "Dev" branding, which has all the code turned on. Call `cargo
# build --no-default-features --features Inbox` to just get the inbox build (for
# example).
############################################
# Feature flags
Feature_test_flag = [] # This is a test feature flag, to demo how they can be used.
############################################
# Branding
# Put each individual feature flag into ONE of the following brandings
Inbox = []
Stable = ["Inbox"]
Dev = ["Stable", "Feature_test_flag"]
# by default, build everything. This is a little different than you'd typically
# expect for a rust crate, but since we're not actually expecting anyone to be
# ingesting us as a crate, it's fine.
default = ["Dev"]

View File

@ -0,0 +1,273 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SudoName" xml:space="preserve">
<value>Sudo for Windows</value>
<comment>{Locked=qps-ploc,qps-ploca,qps-plocm}</comment>
</data>
<data name="LongAbout" xml:space="preserve">
<value>Sudo for Windows.
Can be used to run a command as admin. When run, will prompt for confirmation with a User Accounts Control dialog. Currently only supports running commands as the admin user who confirms the dialog.
If used to run a console application, sudo will return when the target process exits.
If used to run a graphical application (for example, notepad.exe), sudo will return immediately.
In New Window mode, sudo will launch applications from the Windows directory in C:\\Windows\\System32. You can use the --chdir option to change the working directory before running the command.
If a mode is omitted, sudo will use the mode set in the system settings. If a mode is specified with one of --new-window, --disable-input, or --inline, sudo will exit with an error if that mode is not currently allowed by the system settings.
</value>
<comment>{Locked="sudo","notepad.exe","C:\\Windows\\System32","--chdir"}</comment>
</data>
<data name="DisabledLongAbout" xml:space="preserve">
<value>Sudo is disabled on this machine. To enable it, go to the \x1b]8;;ms-settings:developers\x1b\\Developer Settings page\x1b]8;;\x1b\\ in the Settings app</value>
<comment>{Locked="\x1b]8;;ms-settings:developers\x1b\\","\x1b]8;;\x1b\\"}</comment>
</data>
<data name="Base_Help_Help_Long" xml:space="preserve">
<value>Print help (see less with '-h')</value>
<comment>{Locked="-h"}</comment>
</data>
<data name="Base_Help_Help_Short" xml:space="preserve">
<value>Print help (see more with '--help')</value>
<comment>{Locked="--help"}</comment>
</data>
<data name="Base_Version_Help" xml:space="preserve">
<value>Print version</value>
</data>
<data name="Elevate_About" xml:space="preserve">
<value>The elevate subcommand is for internal use only</value>
<comment>{Locked="elevate"}</comment>
</data>
<data name="Elevate_Parent" xml:space="preserve">
<value>Parent process ID</value>
</data>
<data name="Elevate_Commandline" xml:space="preserve">
<value>Command-line to run</value>
</data>
<data name="Config_About" xml:space="preserve">
<value>Get current configuration information of sudo</value>
</data>
<data name="Run_About" xml:space="preserve">
<value>Run a command as admin</value>
</data>
<data name="Run_CopyEnv_Help" xml:space="preserve">
<value>Pass the current environment variables to the command</value>
</data>
<data name="Run_NewWindow_Help" xml:space="preserve">
<value>Use a new window for the command</value>
</data>
<data name="Run_DisableInput_Help" xml:space="preserve">
<value>Run in the current terminal, with input to the target application disabled</value>
</data>
<data name="Run_Inline_Help" xml:space="preserve">
<value>Run in the current terminal</value>
<comment>Description of a command-line flag that will cause sudo to run the target application in the current console window.</comment>
</data>
<data name="Run_Commandline_Help" xml:space="preserve">
<value>Command-line to run</value>
</data>
<data name="DisabledByPolicy" xml:space="preserve">
<value>Sudo is disabled by your organization's policy.</value>
</data>
<data name="DisabledMessage" xml:space="preserve">
<value>Sudo is disabled on this machine.</value>
</data>
<data name="InvalidMode" xml:space="preserve">
<value>Invalid mode:</value>
<comment>This string will be followed by a string the user entered (which was and invalid value)</comment>
</data>
<data name="ErrorSettingMode" xml:space="preserve">
<value>Error setting mode:</value>
<comment>This string will be followed by an error message</comment>
</data>
<data name="UnknownError" xml:space="preserve">
<value>Unknown error:</value>
<comment>This string will be followed by an error message</comment>
</data>
<data name="CurrentMode_ForceNewWindow" xml:space="preserve">
<value>Sudo is currently in Force New Window mode on this machine</value>
</data>
<data name="CurrentMode_DisableInput" xml:space="preserve">
<value>Sudo is currently in Disable Input mode on this machine</value>
<comment>Message printed to the user informing them of the current sudo mode. In this case, the mode is the "Disable Input" mode.</comment>
</data>
<data name="CurrentMode_Inline" xml:space="preserve">
<value>Sudo is currently in Inline mode on this machine</value>
</data>
<data name="RequireAdminToConfig" xml:space="preserve">
<value>You must run this command as an administrator.</value>
</data>
<data name="MaxPolicyMode_ForceNewWindow" xml:space="preserve">
<value>You cannot set a mode higher than Force New Window mode on this machine</value>
<comment>Error message printed when the user attempts to set sudo into a mode higher than the mode currently allowed by policy. In this case, the currently allowed mode is the "Force New Window" mode.</comment>
</data>
<data name="MaxPolicyMode_DisableInput" xml:space="preserve">
<value>You cannot set a mode higher than Disable Input mode on this machine</value>
<comment>Error message printed when the user attempts to set sudo into a mode higher than the mode currently allowed by policy. In this case, the currently allowed mode is the "Disable Input" mode.</comment>
</data>
<data name="MaxPolicyMode_Inline" xml:space="preserve">
<value>You cannot set a mode higher than Inline mode on this machine</value>
<comment>Error message printed when the user attempts to set sudo into a mode higher than the mode currently allowed by policy. In this case, the currently allowed mode is the "Inline" mode.</comment>
</data>
<data name="SetMode_ForceNewWindow" xml:space="preserve">
<value>Sudo mode set to Force New Window mode</value>
</data>
<data name="SetMode_DisableInput" xml:space="preserve">
<value>Sudo mode set to DisableInput mode</value>
</data>
<data name="SetMode_Inline" xml:space="preserve">
<value>Sudo mode set to Inline mode</value>
</data>
<data name="CommandNotFound" xml:space="preserve">
<value>Command not found</value>
</data>
<data name="Cancelled" xml:space="preserve">
<value>Operation was cancelled by the user</value>
</data>
<data name="LaunchedNewWindow" xml:space="preserve">
<value>Launched {0} in a new window.</value>
<comment>{0} will be replaced by the name of a command-line executable</comment>
</data>
<data name="Run_Chdir_Help" xml:space="preserve">
<value>Change the working directory before running the command</value>
</data>
<data name="MaxRunMode_ForceNewWindow" xml:space="preserve">
<value>You cannot run in a mode higher than Force New Window mode on this machine</value>
<comment>Error message printed when the user requested a mode higher than the currently allowed mode. In this case, the currently allowed mode is the "Force New Window" mode.</comment>
</data>
<data name="MaxRunMode_DisableInput" xml:space="preserve">
<value>You cannot run in a mode higher than Disable Input mode on this machine</value>
<comment>Error message printed when the user requested a mode higher than the currently allowed mode. In this case, the currently allowed mode is the "Disable Input" mode.</comment>
</data>
<data name="MaxRunMode_Inline" xml:space="preserve">
<value>You cannot run in a mode higher than Inline mode on this machine</value>
<comment>Error message printed when the user requested a mode higher than the currently allowed mode. In this case, the currently allowed mode is the "Inline" mode.</comment>
</data>
<data name="PreserveEnv_Disallowed" xml:space="preserve">
<value>You are not allowed to preserve environment variables</value>
<comment>Error message printed when the user tries to preserve environment variables, but is not allowed to</comment>
</data>
<data name="Sudo_Disallowed" xml:space="preserve">
<value>You are not allowed to run sudo</value>
<comment>Error message printed when the user is not allowed to run sudo because they aren't an administrator</comment>
</data>
<data name="Run_SetHome_Help" xml:space="preserve">
<value>set USERPROFILE variable to target user's USERPROFILE</value>
<comment>{Locked="USERPROFILE"} Help text for a commandline arg that sets the USERPROFILE variable</comment>
</data>
</root>

280
sudo/build.rs Normal file
View File

@ -0,0 +1,280 @@
use embed_manifest::embed_manifest_file;
use std::path::PathBuf;
use std::process::Command;
use {
std::{env, io},
winres::WindowsResource,
};
fn get_sdk_path() -> Option<String> {
let mut sdk_path: Option<String> = None;
let target = env::var("TARGET").unwrap();
// For whatever reason, find_tool doesn't work directly on `midl.exe`. It
// DOES work however, on `link.exe`, and will hand us back a PATH that has
// the path to the appropriate midl.exe in it.
let link_exe = cc::windows_registry::find_tool(target.as_str(), "link.exe")
.expect("Failed to find link.exe");
link_exe.env().iter().for_each(|(k, v)| {
if k == "PATH" {
let elements = (v.to_str().expect("path exploded"))
.split(';')
.collect::<Vec<&str>>();
// iterate over the elements to find one that starts with
// "C:\Program Files (x86)\Windows Kits\10\bin\10.0.*"
for element in elements {
if element.starts_with("C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.") {
sdk_path = Some(element.to_string());
}
}
}
});
sdk_path
}
fn get_sdk_tool(sdk_path: &Option<String>, tool_name: &str) -> String {
// seems like, in a VS tools prompt, midl.exe is in the path so the above
// doesn't include the path. kinda weird but okay?
let tool_path = match sdk_path {
Some(s) => PathBuf::from(s)
.join(tool_name)
.to_str()
.unwrap()
.to_owned(),
None => {
// This is the case that happens when you run the build from a VS
// tools prompt. In this case, the tool is already in the path, so
// we can just get the absolute path to the exe using the windows
// path search.
let tool_path = which::which(tool_name).expect("Failed to find tool in path");
tool_path.to_str().unwrap().to_owned()
}
};
tool_path
}
fn build_rpc() {
// Build our RPC library
let sdk_path: Option<String> = get_sdk_path();
let midl_path = get_sdk_tool(&sdk_path, "midl.exe");
// Now, we need to get the path to the shared include directory, which is
// dependent on the SDK version. We're gonna find it based on the midl we
// already found.
//
// Our midl path is now something like:
// C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\midl.exe
//
// We want to get the path to the shared include directory, which is like
//
// C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared
//
// (of course, the version number will change depending on the SDK version)
// So, just take that path, remove two elements from the end, replace bin with Include, and add shared.
let mut include_path = PathBuf::from(midl_path.clone());
include_path.pop();
include_path.pop();
// now we're at C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0
let copy_of_include_path = include_path.clone();
let version = copy_of_include_path.file_name().unwrap().to_str().unwrap();
include_path.pop();
include_path.pop();
// now we're at C:\Program Files (x86)\Windows Kits\10\
include_path.push("Include");
include_path.push(version);
include_path.push("shared");
println!("midl_path: {:?}", midl_path);
let target = env::var("TARGET").unwrap();
let cl_path =
cc::windows_registry::find_tool(target.as_str(), "cl.exe").expect("Failed to find cl.exe");
// add cl.exe to our path
let mut path = std::env::var("PATH").unwrap();
path.push(';');
path.push_str(cl_path.path().parent().unwrap().to_str().unwrap());
std::env::set_var("PATH", path);
// Great! we've now finally got a path to midl.exe, and cl.exe is on the PATH
// Now we can actually run midl.exe, to compile the IDL file. This will
// generate a bunch of files in the OUT_DIR which we need to do RPC.
let mut cmd = std::process::Command::new(midl_path);
cmd.arg("../cpp/rpc/sudo_rpc.idl");
cmd.arg("/h").arg("sudo_rpc.h");
cmd.arg("/target").arg("NT100"); // LOAD BEARING: Enables system_handle
cmd.arg("/acf").arg("../cpp/rpc/sudo_rpc.acf");
cmd.arg("/prefix").arg("client").arg("client_");
cmd.arg("/prefix").arg("server").arg("server_");
cmd.arg("/oldnames");
cmd.arg("/I").arg(include_path);
// Force midl to use the right architecture depending on our Rust target.
cmd.arg("/env").arg(match target.as_str() {
"x86_64-pc-windows-msvc" => "x64",
"i686-pc-windows-msvc" => "win32",
"aarch64-pc-windows-msvc" => "arm64",
_ => panic!("Unknown target {}", target),
});
// I was pretty confident that we needed to pass /protocol ndr64 here, but
// if we do that it'll break the win32 build. Omitting it entirely seems to
// Just Work.
// cmd.arg("/protocol").arg("ndr64");
cmd.arg("/out").arg(env::var("OUT_DIR").unwrap());
println!("Full midl command: {:?}", cmd);
let mut midl_result = cmd.spawn().expect("Failed to run midl.exe");
println!(
"midl.exe returned {:?}",
midl_result.wait().expect("midl.exe failed")
);
// Now that our PRC header and stubs were generated, we can compile them
// into our actual RPC lib.
let mut rpc_build = cc::Build::new();
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
rpc_build
.warnings(true)
.warnings_into_errors(true)
.include(env::var("OUT_DIR").unwrap())
.file(out_dir.join("sudo_rpc_c.c"))
.file(out_dir.join("sudo_rpc_s.c"))
.file("../cpp/rpc/RpcClient.c")
.flag("/guard:ehcont");
println!("build cmdline: {:?}", rpc_build.get_compiler().to_command());
rpc_build.compile("myRpc");
println!("cargo:rustc-link-lib=myRpc");
println!("cargo:rerun-if-changed=../cpp/rpc/RpcClient.c");
println!("cargo:rerun-if-changed=../cpp/rpc/sudo_rpc.idl");
}
fn build_logging() {
// Build our Event Logging library
let sdk_path: Option<String> = get_sdk_path();
let mc_path = get_sdk_tool(&sdk_path, "mc.exe");
println!("mc_path: {:?}", mc_path);
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let mut cmd = std::process::Command::new(mc_path);
cmd.arg("-h").arg(&out_dir);
cmd.arg("-r").arg(&out_dir);
cmd.arg("../cpp/logging/instrumentation.man");
println!("Full mc command: {:?}", cmd);
let mc_result = cmd
.spawn()
.expect("Failed to run mc.exe")
.wait()
.expect("mc.exe failed");
if !mc_result.success() {
eprintln!("\n\nerror occurred: {}\n\n", mc_result);
std::process::exit(1);
}
let mut logging_build = cc::Build::new();
logging_build
.warnings(true)
.warnings_into_errors(true)
.include(env::var("OUT_DIR").unwrap())
.file("../cpp/logging/EventViewerLogging.c")
.flag("/guard:ehcont");
println!(
"build cmdline: {:?}",
logging_build.get_compiler().to_command()
);
logging_build.compile("myEventLogging");
println!("cargo:rustc-link-lib=myEventLogging");
println!("cargo:rerun-if-changed=../cpp/logging/EventViewerLogging.c");
println!("cargo:rerun-if-changed=../cpp/logging/instrumentation.man");
}
fn main() -> io::Result<()> {
// Always build the RPC lib.
build_rpc();
// Always build the Event Logging lib.
build_logging();
println!("cargo:rerun-if-changed=sudo/Resources/en-US/Sudo.resw");
println!("cargo:rerun-if-changed=sudo.rc");
println!("cargo:rerun-if-changed=../Generated Files/out.rc");
println!("cargo:rerun-if-changed=../Generated Files/out_resources.h");
// compile the resource file.
// Run
// powershell -c .pipelines\convert-resx-to-rc.ps1 .\ no_existy.h res.h no_existy.rc out.rc resource_ids.rs
// to generate the resources
Command::new("powershell")
.arg("-NoProfile")
.arg("-c")
.arg("..\\.pipelines\\convert-resx-to-rc.ps1")
.arg("..\\") // Root directory which contains the resx files
.arg("no_existy.h") // File name of the base resource.h which contains all the non-localized resource definitions
.arg("resource.h") // Target file name of the resource header file, which will be used in code - Example: resource.h
.arg("sudo\\sudo.rc") // File name of the base ProjectName.rc which contains all the non-localized resources
.arg("out.rc") // Target file name of the resource rc file, which will be used in code - Example: ProjectName.rc
.arg("resource_ids.rs") // Target file name of the rust resource file, which will be used in code - Example: resource.rs
.status()
.expect("Failed to generate resources");
// witchcraft to get windows.h from the SDK to be able to be found, for the resource compiler
let target = std::env::var("TARGET").unwrap();
if let Some(tool) = cc::windows_registry::find_tool(target.as_str(), "cl.exe") {
for (key, value) in tool.env() {
std::env::set_var(key, value);
}
}
if std::env::var_os("CARGO_CFG_WINDOWS").is_some() {
// TODO:MSFT
// Re-add the following:
// <windowsSettings>
// <consoleAllocationPolicy xmlns="http://schemas.microsoft.com/SMI/2024/WindowsSettings">detached</consoleAllocationPolicy>
// </windowsSettings>
// to our manifest
embed_manifest_file("sudo.manifest").expect("Failed to embed manifest");
let generated_rc_content = std::fs::read_to_string("../Generated Files/out.rc").unwrap();
let instrumentation_rc_content =
std::fs::read_to_string(env::var("OUT_DIR").unwrap() + "/instrumentation.rc").unwrap();
let generated_header = std::fs::read_to_string("../Generated Files/resource.h").unwrap();
WindowsResource::new()
// We don't want to use set_resource_file here, because we _do_ want
// the file version info that winres can autogenerate. Instead,
// manually stitch in our generated header (with resource IDs), and
// our generated rc file (with the localized strings)
.append_rc_content(&generated_header)
.append_rc_content(&instrumentation_rc_content)
.append_rc_content(&generated_rc_content)
.compile()?;
}
Ok(())
}
// Magic incantation to get the build to generate the .rc file, before we build things:
//
// powershell -c .pipelines\convert-resx-to-rc.ps1 src\cascadia\ this_doesnt_exist.h out_resources.h no_existy.rc out.rc resource_ids.rs
//
// do that from the root of the repo, and it will generate the .rc file, into
// src\cascadia\Generated Files\{out.rc, out_resources.h}
//
//
// Alternatively,
// powershell -c .pipelines\convert-resx-to-rc.ps1 .\ no_existy.h res.h no_existy.rc out.rc resource_ids.rs
//
// will generate the .rc file into the a "Generated Files" dir in the root of the repo.

167
sudo/src/elevate_handler.rs Normal file
View File

@ -0,0 +1,167 @@
use crate::helpers::*;
use crate::logging_bindings::event_log_request;
use crate::messages::ElevateRequest;
use crate::rpc_bindings_server::rpc_server_setup;
use crate::tracing;
use std::ffi::CString;
use std::os::windows::io::{FromRawHandle, IntoRawHandle};
use std::os::windows::process::CommandExt;
use std::process::Stdio;
use windows::{
core::*, Win32::Foundation::*, Win32::System::Console::*, Win32::System::Threading::*,
};
fn handle_to_stdio(h: HANDLE) -> Stdio {
if h.is_invalid() {
return Stdio::inherit();
}
unsafe {
let p = GetCurrentProcess();
let mut clone = Default::default();
match DuplicateHandle(p, h, p, &mut clone, 0, true, DUPLICATE_SAME_ACCESS) {
Ok(_) => Stdio::from_raw_handle(clone.0 as _),
Err(_) => Stdio::null(),
}
}
}
/// Prepare the target process, spawn it, and hand back the Child process. This will take care of setting up the handles for redirected input/output, and setting the environment variables.
pub fn spawn_target_for_request(request: &ElevateRequest) -> Result<std::process::Child> {
tracing::trace_log_message(&format!("Spawning: {}...", &request.application));
let mut command_args = std::process::Command::new(request.application.clone());
command_args.current_dir(request.target_dir.clone());
command_args.args(request.args.clone());
tracing::trace_log_message(&format!("args: {:?}", &request.args));
if !request.env_vars.is_empty() {
command_args.env_clear();
command_args.envs(env_from_raw_bytes(&request.env_vars));
}
// If we're in ForceNewWindow mode, we want the target process to use a new
// console window instead of inheriting the one from the parent process.
if request.sudo_mode == SudoMode::ForceNewWindow {
command_args.creation_flags(CREATE_NEW_CONSOLE.0);
}
// Set the stdin/stdout/stderr of the child process In disabled input
// mode, set stdin to null. We don't want the target application to be
// able to read anything from stdin.
command_args.stdin(if request.sudo_mode != SudoMode::DisableInput {
handle_to_stdio(request.handles[0])
} else {
Stdio::null()
});
command_args.stdout(handle_to_stdio(request.handles[1]));
command_args.stderr(handle_to_stdio(request.handles[2]));
command_args.spawn().map_err(|err| {
match err.kind() {
std::io::ErrorKind::NotFound => {
// This error code is MSG_DIR_BAD_COMMAND_OR_FILE. That's
// what CMD uses to indicate a command not found.
E_DIR_BAD_COMMAND_OR_FILE.into()
}
_ => err.into(),
}
})
}
/// Execute the elevation request.
/// * Conditionally attach to the parent process's console (if requested)
/// * Spawn the target process (with redirected input/output if requested, and with the environment variables passed in if needed)
/// Called by rust_handle_elevation_request
pub fn handle_elevation_request(request: &ElevateRequest) -> Result<OwnedHandle> {
// Log the request we received to the event log. This should create a pair
// of events, one for the request, and one for the response, each with the
// same RequestID.
event_log_request(false, request);
// Check if the requested sudo mode is allowed
let config: RegistryConfigProvider = Default::default();
let allowed_mode = get_allowed_mode(&config)?;
if request.sudo_mode > allowed_mode {
tracing::trace_log_message(&format!(
"Requested sudo mode is not allowed: {:?} ({:?})",
request.sudo_mode, allowed_mode
));
return Err(E_ACCESSDENIED.into());
}
// If we're in ForceNewWindow mode, we _don't_ want to detach from our
// current console and reattach to the parent process's console. Instead,
// we'll just create the target process with CREATE_NEW_CONSOLE.
//
// This scenario only happens when we're running `sudo -E --newWindow`, to
// copy env vars but also use a new console window. If we don't pass -E,
// then we'll have instead just directly ShellExecute'd the target
// application (and never hit this codepath)
//
// Almost all the time, we'll actually hit the body of this conditional.
if request.sudo_mode != SudoMode::ForceNewWindow {
// It would seem that we always need to detach from the current console,
// even in redirected i/o mode. In the case that we aren't fully redirected
// (like, if stdin is redirected but stdout isn't), we'll still need to
// attach to the parent console for the other std handles.
unsafe {
// Detach from the current console
_ = FreeConsole();
// Attach to the parent process's console
if { AttachConsole(request.parent_pid) }.is_ok() {
// Add our own CtrlC handler, so that we can ignore it.
_ = SetConsoleCtrlHandler(Some(ignore_ctrl_c), true);
// TODO! add some error handling here you goober
}
}
}
// We're attached to the right console, Run the command.
let process_launch = spawn_target_for_request(request);
unsafe {
_ = SetConsoleCtrlHandler(Some(ignore_ctrl_c), false);
_ = FreeConsole();
}
let child = process_launch?;
// Limit the things the caller can do with the process handle, because the one we just created is PROCESS_ALL_ACCESS.
// I tried to use [out, system_handle(sh_process, PROCESS_QUERY_LIMITED_INFORMATION)]
// in the COM API to have it limit the handle permissions but that didn't work at all.
// So now we do it manually here.
unsafe {
let mut child_handle = OwnedHandle::default();
let current_process = GetCurrentProcess();
DuplicateHandle(
current_process,
HANDLE(child.into_raw_handle() as _),
current_process,
&mut *child_handle,
(PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_DUP_HANDLE | PROCESS_SYNCHRONIZE).0,
false,
DUPLICATE_CLOSE_SOURCE,
)?;
Ok(child_handle)
}
}
/// Starts the RPC server and blocks until Shutdown() is called.
pub fn start_rpc_server(
parent_pid: u32,
_caller_sid: Option<&String>,
_args: &[&String],
) -> Result<i32> {
// TODO:48520593 In rust_handle_elevation_request, validate that the parent
// process handle is the same one that we opened here.
let endpoint = generate_rpc_endpoint_name(parent_pid);
let endpoint = CString::new(endpoint).unwrap();
rpc_server_setup(&endpoint, parent_pid)?;
Ok(0)
}

762
sudo/src/helpers.rs Normal file
View File

@ -0,0 +1,762 @@
use crate::rpc_bindings::Utf8Str;
use crate::trace_log_message;
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::mem::{size_of, MaybeUninit};
use std::ops::{Deref, DerefMut};
use std::os::windows::ffi::OsStringExt;
use std::os::windows::fs::FileExt;
use std::path::{Path, PathBuf};
use std::slice::{from_raw_parts, from_raw_parts_mut};
use windows::Win32::Storage::FileSystem::GetFullPathNameW;
use windows::Win32::System::Diagnostics::Debug::{IMAGE_NT_HEADERS32, IMAGE_SUBSYSTEM};
use windows::Win32::System::Environment::{FreeEnvironmentStringsW, GetEnvironmentStringsW};
use windows::Win32::System::Rpc::RPC_STATUS;
use windows::Win32::System::SystemServices::{
IMAGE_DOS_HEADER, IMAGE_DOS_SIGNATURE, IMAGE_NT_SIGNATURE, SE_TOKEN_USER, SE_TOKEN_USER_1,
};
use windows::{
core::*, Win32::Foundation::*, Win32::Security::Authorization::*, Win32::Security::*,
Win32::System::Console::*, Win32::System::Threading::*,
};
// https://github.com/microsoft/win32metadata/issues/1857
pub const RPC_S_ACCESS_DENIED: RPC_STATUS = RPC_STATUS(ERROR_ACCESS_DENIED.0 as i32);
pub const E_FILENOTFOUND: HRESULT = ERROR_FILE_NOT_FOUND.to_hresult();
pub const E_CANCELLED: HRESULT = ERROR_CANCELLED.to_hresult();
pub const MSG_DIR_BAD_COMMAND_OR_FILE: WIN32_ERROR = WIN32_ERROR(9009);
pub const E_DIR_BAD_COMMAND_OR_FILE: HRESULT = MSG_DIR_BAD_COMMAND_OR_FILE.to_hresult();
pub const E_ACCESS_DISABLED_BY_POLICY: HRESULT = ERROR_ACCESS_DISABLED_BY_POLICY.to_hresult();
#[derive(PartialEq, Eq, Debug, Clone, Copy, PartialOrd, Ord)]
pub enum SudoMode {
Disabled = 0,
ForceNewWindow = 1,
DisableInput = 2,
Normal = 3,
}
impl TryFrom<u32> for SudoMode {
type Error = Error;
fn try_from(value: u32) -> Result<Self> {
match value {
0 => Ok(SudoMode::Disabled),
1 => Ok(SudoMode::ForceNewWindow),
2 => Ok(SudoMode::DisableInput),
3 => Ok(SudoMode::Normal),
_ => Err(ERROR_INVALID_PARAMETER.into()),
}
}
}
impl From<SudoMode> for u32 {
fn from(value: SudoMode) -> Self {
value as u32
}
}
impl From<SudoMode> for i32 {
fn from(val: SudoMode) -> Self {
val as i32
}
}
#[repr(transparent)]
#[derive(Default)]
pub struct OwnedHandle(pub HANDLE);
impl OwnedHandle {
pub fn new(handle: HANDLE) -> Self {
OwnedHandle(handle)
}
}
impl Drop for OwnedHandle {
fn drop(&mut self) {
if !self.0.is_invalid() {
unsafe { _ = CloseHandle(self.0) }
}
}
}
impl Deref for OwnedHandle {
type Target = HANDLE;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for OwnedHandle {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
// There can be many different types that need to be LocalFree'd. PWSTR, PCWSTR, PSTR, PCSTR, PSECURITY_DESCRIPTOR
// are all distinct types, but they are compatible with the windows::core::IntoParam<HLOCAL> trait.
// There's also *mut ACL though which is also LocalAlloc'd and that's the problem (probably not the last of its kind).
// Writing a wrapper trait that is implemented for both IntoParam<HLOCAL> (or its friends) and *const/mut T
// doesn't work due to E0119. Implementing our own trait for each concrete type is highly annoying and verbose.
// So now this calls transmute_copy and zeroed. It's ugly and somewhat unsafe, but it's simple and short.
#[repr(transparent)]
pub struct OwnedLocalAlloc<T>(pub T);
impl<T> Default for OwnedLocalAlloc<T> {
fn default() -> Self {
unsafe { std::mem::zeroed() }
}
}
impl<T> Drop for OwnedLocalAlloc<T> {
fn drop(&mut self) {
unsafe {
let ptr: HLOCAL = std::mem::transmute_copy(self);
if !ptr.0.is_null() {
LocalFree(ptr);
}
}
}
}
impl<T> Deref for OwnedLocalAlloc<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for OwnedLocalAlloc<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub unsafe extern "system" fn ignore_ctrl_c(ctrl_type: u32) -> BOOL {
match ctrl_type {
CTRL_C_EVENT | CTRL_BREAK_EVENT => TRUE,
_ => FALSE,
}
}
pub fn generate_rpc_endpoint_name(pid: u32) -> String {
format!(r"sudo_elevate_{pid}")
}
pub fn is_running_elevated() -> Result<bool> {
// TODO!
// Do the thing Terminal does to see if UAC is entirely disabled:
// Which is basically (from Utils::CanUwpDragDrop)
// const auto elevationType = wil::get_token_information<TOKEN_ELEVATION_TYPE>(processToken.get());
// const auto elevationState = wil::get_token_information<TOKEN_ELEVATION>(processToken.get());
// if (elevationType == TokenElevationTypeDefault && elevationState.TokenIsElevated)
//
let current_token = current_process_token()?;
let elevation: TOKEN_ELEVATION = get_token_info(*current_token)?;
Ok(elevation.TokenIsElevated == 1)
}
fn current_process_token() -> Result<OwnedHandle> {
let mut token: OwnedHandle = Default::default();
unsafe {
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut *token)?;
}
Ok(token)
}
fn get_process_token(process: HANDLE) -> Result<OwnedHandle> {
let mut token: OwnedHandle = Default::default();
unsafe {
OpenProcessToken(process, TOKEN_QUERY, &mut *token)?;
}
Ok(token)
}
// helper trait to get the TOKEN_INFORMATION_CLASS for a given type
trait TokenInfo {
fn info_class() -> TOKEN_INFORMATION_CLASS;
}
impl TokenInfo for TOKEN_ELEVATION_TYPE {
fn info_class() -> TOKEN_INFORMATION_CLASS {
TokenElevationType
}
}
impl TokenInfo for TOKEN_ELEVATION {
fn info_class() -> TOKEN_INFORMATION_CLASS {
TokenElevation
}
}
impl TokenInfo for SE_TOKEN_USER {
fn info_class() -> TOKEN_INFORMATION_CLASS {
TokenUser
}
}
fn get_token_info<T: TokenInfo>(token: HANDLE) -> Result<T> {
unsafe {
let mut info: T = std::mem::zeroed();
let size = std::mem::size_of::<T>() as u32;
let mut ret_size = size;
GetTokenInformation(
token,
T::info_class(),
Some(&mut info as *mut _ as _),
size,
&mut ret_size,
)?;
Ok(info)
}
}
pub fn can_current_user_elevate() -> Result<bool> {
let current_token = current_process_token()?;
let elevation_type: TOKEN_ELEVATION_TYPE = get_token_info(*current_token)?;
Ok(elevation_type == TokenElevationTypeFull || elevation_type == TokenElevationTypeLimited)
}
pub fn get_sid_for_process(process: HANDLE) -> Result<SE_TOKEN_USER_1> {
let process_token = get_process_token(process)?;
let token_user: SE_TOKEN_USER = get_token_info(*process_token)?;
Ok(token_user.Anonymous2)
}
pub fn get_current_user() -> Result<HSTRING> {
unsafe {
let user = get_sid_for_process(GetCurrentProcess())?;
let mut str_sid = OwnedLocalAlloc::default();
ConvertSidToStringSidW(PSID(&user.Sid as *const _ as _), &mut *str_sid)?;
str_sid.to_hstring()
}
}
pub fn is_cmd_intrinsic(application: &str) -> bool {
// List from https://ss64.com/nt/syntax-internal.html
//
// The following are internal commands to cmd.exe
// ASSOC, BREAK, CALL ,CD/CHDIR, CLS, COLOR, COPY, DATE, DEL, DIR, DPATH,
// ECHO, ENDLOCAL, ERASE, EXIT, FOR, FTYPE, GOTO, IF, KEYS, MD/MKDIR,
// MKLINK (vista and above), MOVE, PATH, PAUSE, POPD, PROMPT, PUSHD, REM,
// REN/RENAME, RD/RMDIR, SET, SETLOCAL, SHIFT, START, TIME, TITLE, TYPE,
// VER, VERIFY, VOL
// if the application is one of these, we need to do something special
// to make sure it works.
//
// We also want to makke sure it's case insensitive
matches!(
application.to_uppercase().as_str(),
"ASSOC"
| "BREAK"
| "CALL"
| "CD"
| "CHDIR"
| "CLS"
| "COLOR"
| "COPY"
| "DATE"
| "DEL"
| "DIR"
| "DPATH"
| "ECHO"
| "ENDLOCAL"
| "ERASE"
| "EXIT"
| "FOR"
| "FTYPE"
| "GOTO"
| "IF"
| "KEYS"
| "MD"
| "MKDIR"
| "MKLINK"
| "MOVE"
| "PATH"
| "PAUSE"
| "POPD"
| "PROMPT"
| "PUSHD"
| "REM"
| "REN"
| "RENAME"
| "RD"
| "RMDIR"
| "SET"
| "SETLOCAL"
| "SHIFT"
| "START"
| "TIME"
| "TITLE"
| "TYPE"
| "VER"
| "VERIFY"
| "VOL"
)
}
/// Returns the current environment as a null-delimited string.
pub fn env_as_string() -> String {
unsafe {
let beg = GetEnvironmentStringsW().0 as *const _;
let mut end = beg;
// Try to figure out the end of the double-null terminated env block.
loop {
let len = wcslen(PCWSTR(end));
if len == 0 {
break;
}
end = end.add(len + 1);
}
// The string we want to return should not be double-null terminated.
// The last iteration above however added `len + 1` and so we need to undo that now.
// We use `saturating_sub` because at least theoretically `beg` may be an empty string.
let len = usize::try_from(end.offset_from(beg))
.unwrap()
.saturating_sub(1);
let str = String::from_utf16_lossy(from_raw_parts(beg, len));
let _ = FreeEnvironmentStringsW(PCWSTR(beg));
str
}
}
/// Splits a null-delimited environment string into key/value pairs.
pub fn env_from_raw_bytes(env_string: &str) -> impl Iterator<Item = (&OsStr, &OsStr)> {
env_string.split('\0').filter_map(|s| {
// In the early days the cmd.exe devs added env variables that start with "=".
// They look like "=C:=C:\foo\bar" and are used to track per-drive CWDs across cmd child-processes.
// See here for more information: https://devblogs.microsoft.com/oldnewthing/20100506-00/?p=14133
// The get() call simultaneously takes care of rejecting empty strings, which is neat.
let idx = s.get(1..)?.find('=')?;
// The `.get(1..)` call will slice off 1 character from the start of the string and thus from the `idx` value.
// This means that when we want to split the string into two parts `[0,idx)` and `(idx,length)`
// (= `[idx+1,length)` = without the "=" character) then we need to add +1 to both sides now.
Some((OsStr::new(&s[..idx + 1]), OsStr::new(&s[idx + 2..])))
})
}
/// Windows does not actually support distinct command line parameters. They're all just given as a single string.
/// We can't just use `.join(" ")` either, because this breaks arguments with whitespaces. This function handles these details.
pub fn join_args<T: AsRef<str>>(args: &[T]) -> String {
// We estimate 3*args.len() overhead per arg: 2 quotes and 1 whitespace.
let expected_len = args
.len()
.checked_mul(3)
.and_then(|n| {
args.iter()
.map(|s| s.as_ref().len())
.try_fold(n, usize::checked_add)
})
.unwrap();
let mut accumulator = Vec::with_capacity(expected_len);
// Fun fact: At the time of writing, Windows Terminal has a function called `QuoteAndEscapeCommandlineArg`
// and Rust's `std::sys::windows::args` crate has a `append_arg` function. Both functions are pretty much
// 1:1 identical to the code below, but both were written independently. I guess there aren't too many
// ways to express this concept, but I'm still somewhat surprised there aren't multiple ways to do it.
for (idx, arg) in args.iter().enumerate() {
if idx != 0 {
accumulator.push(b' ');
}
let str = arg.as_ref();
let quote = str.is_empty() || str.contains(' ') || str.contains('\t');
if quote {
accumulator.push(b'"');
}
let mut backslashes: usize = 0;
for &x in str.as_bytes() {
if x == b'\\' {
backslashes += 1;
} else {
if x == b'"' {
accumulator.extend((0..=backslashes).map(|_| b'\\'));
}
backslashes = 0;
}
accumulator.push(x);
}
if quote {
accumulator.extend((0..backslashes).map(|_| b'\\'));
accumulator.push(b'"');
}
}
// Assuming that our `args` slice was UTF8 the accumulator can't suddenly contain non-UTF8.
unsafe { String::from_utf8_unchecked(accumulator) }
}
/// Joins a list of strings into a single string, each of which is null-terminated (including the final one).
pub fn pack_string_list_for_rpc<T: AsRef<str>>(args: &[T]) -> String {
let expected_len = args
.iter()
.map(|s| s.as_ref().len())
// We extend each arg in args with 1 character: \0.
// This results in an added overhead of args.len() characters, which we
// implicitly add by setting it as the initial value for the fold().
.try_fold(args.len(), usize::checked_add)
.unwrap();
let mut accumulator = String::with_capacity(expected_len);
for arg in args {
accumulator.push_str(arg.as_ref());
accumulator.push('\0');
}
accumulator
}
/// Splits a string generated by `pack_args` up again.
pub fn unpack_string_list_from_rpc(args: Utf8Str) -> Result<Vec<String>> {
Ok(args
.as_str()?
.split_terminator('\0')
.map(String::from)
.collect())
}
pub trait ConfigProvider {
fn get_setting_mode(&self) -> Result<u32>;
fn get_policy_mode(&self) -> Result<u32>;
}
#[derive(Default)]
pub struct RegistryConfigProvider;
impl ConfigProvider for RegistryConfigProvider {
fn get_setting_mode(&self) -> Result<u32> {
windows_registry::LOCAL_MACHINE
.open("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Sudo")
.and_then(|key| key.get_u32("Enabled"))
}
fn get_policy_mode(&self) -> Result<u32> {
windows_registry::LOCAL_MACHINE
.open("SOFTWARE\\Policies\\Microsoft\\Windows\\Sudo")
.and_then(|key| key.get_u32("Enabled"))
}
}
/// Get the current mode allowed by policy.
/// * If the policy isn't set (we fail to read the reg key), we'll return Ok(3)
/// (to indicate that all modes up to inline are allowed).
/// * If the policy is set, we'll return the value from the policy.
/// * If we fail to read the policy for any other reason, we'll return an error
/// (which should be treated as "disabled by policy")
pub fn get_allowed_mode_from_policy(config: &impl ConfigProvider) -> Result<SudoMode> {
match config.get_policy_mode() {
Ok(v) => v.min(3).try_into(),
// This is okay! No policy state really means that it's _not_ disabled by policy.
Err(e) if e.code() == E_FILENOTFOUND => Ok(SudoMode::Normal),
Err(e) => Err(e),
}
}
/// Get the current setting mode from the registry. If the setting isn't there,
/// we're disabled. If we fail to read the setting, we're disabled. Errors
/// should be treated as disabled.
pub fn get_setting_mode(config: &impl ConfigProvider) -> Result<SudoMode> {
match config.get_setting_mode() {
Ok(v) => v.min(3).try_into(),
Err(e) if e.code() == E_FILENOTFOUND => Ok(SudoMode::Disabled),
Err(e) => Err(e),
}
}
pub fn get_allowed_mode(config: &impl ConfigProvider) -> Result<SudoMode> {
let allowed_mode_from_policy = get_allowed_mode_from_policy(config)?;
if allowed_mode_from_policy == SudoMode::Disabled {
return Err(E_ACCESS_DISABLED_BY_POLICY.into());
}
let setting_mode = get_setting_mode(config).unwrap_or(SudoMode::Disabled);
SudoMode::try_from(std::cmp::min::<u32>(
allowed_mode_from_policy.into(),
setting_mode.into(),
))
}
pub fn get_process_path_from_handle(process: HANDLE) -> Result<PathBuf> {
let mut buffer = vec![0u16; MAX_PATH as usize];
// Call QueryFullProcessImageNameW in a loop to make sure we actually get the
// full path. We have to do it in a loop, because QueryFullProcessImageNameW
// doesn't actually tell us how big the buffer needs to be on error.
loop {
let mut len = buffer.len() as u32;
match unsafe {
QueryFullProcessImageNameW(
process,
Default::default(),
PWSTR(buffer.as_mut_ptr()),
&mut len,
)
} {
Ok(()) => return Ok(PathBuf::from(OsString::from_wide(&buffer[..len as usize]))),
Err(err) if err.code() != ERROR_INSUFFICIENT_BUFFER.to_hresult() => return Err(err),
Err(_) => buffer.resize(buffer.len() * 2, 0),
};
}
}
/// Check that the client process is the same as the server process.
pub fn check_client(client_handle: HANDLE) -> Result<()> {
// Open a handle to the provided process
let process_path = get_process_path_from_handle(client_handle)?;
let our_path = std::env::current_exe().unwrap();
trace_log_message(&format!(
"{process_path:?} connected to server {our_path:?}"
));
// Now, is this path the same as us? (ignoring case)
if !process_path
.as_os_str()
.eq_ignore_ascii_case(our_path.as_os_str())
{
return Err(E_ACCESSDENIED.into());
}
let mut client_sid = get_sid_for_process(client_handle)?;
let mut our_sid = unsafe { get_sid_for_process(GetCurrentProcess())? };
// Are these SIDs the same? This check prevents over-the-shoulder elevation
// when the RPC server is in use.
unsafe {
// If the SID structures are equal, the return value is nonzero (TRUE)
// Then the windows-rs projection will take the true and convert that to Ok(()), or FALSE to Err(GetLastError())
// EqualSid(PSID{ 0: &mut client_sid.Sid as *mut _ } , &mut our_sid.Sid as *mut _ as PSID)?;
let client_psid: PSID = PSID(&mut client_sid.Buffer as *mut _ as _);
let our_psid: PSID = PSID(&mut our_sid.Buffer as *mut _ as _);
EqualSid(client_psid, our_psid)?;
};
Ok(())
}
/// Make a Windows path absolute, using GetFullPathNameW to resolve the file on disk.
/// Largely lifted from the rust stdlib, because it's _currently_ a nightly-only function.
/// We don't have all the same internal stdlib helpers they do, but it's effectively the same..
pub fn absolute_path(path: &Path) -> Result<PathBuf> {
if path.as_os_str().as_encoded_bytes().starts_with(br"\\?\") {
return Ok(path.into());
}
let lpfilename = HSTRING::from(path.as_os_str());
let mut buffer = vec![0u16; MAX_PATH as usize];
loop {
// GetFullPathNameW will return the required buffer size if the buffer is too small.
let res = unsafe { GetFullPathNameW(&lpfilename, Some(buffer.as_mut_slice()), None) };
match res as usize {
0 => return Err(Error::from_win32()), // returns GLE
len if len <= buffer.len() => {
return Ok(PathBuf::from(OsString::from_wide(&buffer[..len])))
}
new_len => buffer.resize(new_len, 0),
}
}
}
unsafe fn read_struct_at<T>(f: &mut File, offset: u64) -> Result<T> {
let mut data = MaybeUninit::<T>::uninit();
let bytes = from_raw_parts_mut(data.as_mut_ptr() as *mut u8, size_of::<T>());
let read = f.seek_read(bytes, offset)?;
if read != bytes.len() {
return Err(ERROR_HANDLE_EOF.into());
}
Ok(data.assume_init())
}
pub fn get_exe_subsystem<P: AsRef<Path>>(path: P) -> Result<IMAGE_SUBSYSTEM> {
let mut f = File::open(path)?;
let dos: IMAGE_DOS_HEADER = unsafe { read_struct_at(&mut f, 0)? };
if dos.e_magic != IMAGE_DOS_SIGNATURE {
return Err(ERROR_BAD_EXE_FORMAT.into());
}
// IMAGE_NT_HEADERS32 and IMAGE_NT_HEADERS64 have different sizes,
// but the offset of the .OptionalHeader.Subsystem member is identical.
let nt: IMAGE_NT_HEADERS32 = unsafe { read_struct_at(&mut f, dos.e_lfanew as u64)? };
if nt.Signature != IMAGE_NT_SIGNATURE {
return Err(ERROR_BAD_EXE_FORMAT.into());
}
Ok(nt.OptionalHeader.Subsystem)
}
#[cfg(test)]
mod tests {
use super::*;
use windows::Win32::System::Diagnostics::Debug::{
IMAGE_SUBSYSTEM_WINDOWS_CUI, IMAGE_SUBSYSTEM_WINDOWS_GUI,
};
#[test]
fn test_env_from_raw_string() {
let raw_string = "foo=bar\0baz=qux\0\0";
let env_map: Vec<_> = env_from_raw_bytes(raw_string).collect();
assert_eq!(env_map.len(), 2);
assert_eq!(env_map[0], (OsStr::new("foo"), OsStr::new("bar")));
assert_eq!(env_map[1], (OsStr::new("baz"), OsStr::new("qux")));
}
#[test]
fn test_env_with_drive_vars() {
let raw_string = "foo=bar\0=D:=D:\\qux\0\0";
let env_map: Vec<_> = env_from_raw_bytes(raw_string).collect();
assert_eq!(env_map.len(), 2);
assert_eq!(env_map[0], (OsStr::new("foo"), OsStr::new("bar")));
assert_eq!(env_map[1], (OsStr::new("=D:"), OsStr::new("D:\\qux")));
}
#[test]
fn test_join_args() {
assert_eq!(join_args(&[""; 0]), "");
assert_eq!(join_args(&["foo", "bar"]), "foo bar");
assert_eq!(join_args(&["f \too", " bar\t"]), "\"f \too\" \" bar\t\"");
assert_eq!(join_args(&["f\\\"oo", "\"bar\""]), r#"f\\\"oo \"bar\""#);
}
#[test]
fn test_pack_args() {
assert_eq!(pack_string_list_for_rpc(&[""; 0]), "");
assert_eq!(pack_string_list_for_rpc(&["foo"]), "foo\0");
assert_eq!(pack_string_list_for_rpc(&["foo", "bar"]), "foo\0bar\0");
}
#[test]
fn test_unpack_args() {
assert_eq!(unpack_string_list_from_rpc("".into()).unwrap(), [""; 0]);
assert_eq!(unpack_string_list_from_rpc("foo".into()).unwrap(), ["foo"]);
assert_eq!(
unpack_string_list_from_rpc("foo\0".into()).unwrap(),
["foo"]
);
assert_eq!(
unpack_string_list_from_rpc("foo\0bar".into()).unwrap(),
["foo", "bar"]
);
assert_eq!(
unpack_string_list_from_rpc("foo\0bar\0".into()).unwrap(),
["foo", "bar"]
);
assert_eq!(
unpack_string_list_from_rpc("\0\0".into()).unwrap(),
["", ""]
);
assert_eq!(
unpack_string_list_from_rpc("foo\0\0bar\0\0".into()).unwrap(),
["foo", "", "bar", ""]
);
}
#[test]
fn test_get_exe_subsystem() {
assert_eq!(
Ok(IMAGE_SUBSYSTEM_WINDOWS_CUI),
get_exe_subsystem(r"C:\Windows\System32\nslookup.exe")
);
assert_eq!(
Ok(IMAGE_SUBSYSTEM_WINDOWS_GUI),
get_exe_subsystem(r"C:\Windows\notepad.exe")
);
}
/// config tests
struct TestConfigProvider {
setting_mode: Result<u32>,
policy_mode: Result<u32>,
}
impl ConfigProvider for TestConfigProvider {
fn get_setting_mode(&self) -> Result<u32> {
self.setting_mode.clone()
}
fn get_policy_mode(&self) -> Result<u32> {
self.policy_mode.clone()
}
}
#[test]
fn test_get_allowed_mode_from_policy() {
// no setting at all
let config = TestConfigProvider {
setting_mode: Err(E_FILENOTFOUND.into()),
policy_mode: Err(E_FILENOTFOUND.into()),
};
assert_eq!(get_setting_mode(&config).unwrap(), SudoMode::Disabled);
assert_eq!(
get_allowed_mode_from_policy(&config).unwrap(),
SudoMode::Normal
);
assert_eq!(get_allowed_mode(&config).unwrap(), SudoMode::Disabled);
// Setting set to 3 (normal), but policy only allows 2 (disable input)
let config = TestConfigProvider {
setting_mode: Ok(3),
policy_mode: Ok(2),
};
assert_eq!(get_setting_mode(&config).unwrap(), SudoMode::Normal);
assert_eq!(
get_allowed_mode_from_policy(&config).unwrap(),
SudoMode::DisableInput
);
assert_eq!(get_allowed_mode(&config).unwrap(), SudoMode::DisableInput);
// policy is out of range
let config = TestConfigProvider {
setting_mode: Ok(3),
policy_mode: Ok(4),
};
assert_eq!(get_setting_mode(&config).unwrap(), SudoMode::Normal);
assert_eq!(
get_allowed_mode_from_policy(&config).unwrap(),
SudoMode::Normal
);
assert_eq!(get_allowed_mode(&config).unwrap(), SudoMode::Normal);
// entirely disabled by policy
let config = TestConfigProvider {
setting_mode: Ok(3),
policy_mode: Ok(0),
};
assert_eq!(get_setting_mode(&config).unwrap(), SudoMode::Normal);
assert_eq!(
get_allowed_mode_from_policy(&config).unwrap(),
SudoMode::Disabled
);
assert_eq!(
get_allowed_mode(&config),
Err(E_ACCESS_DISABLED_BY_POLICY.into())
);
// No policy config found
let config = TestConfigProvider {
setting_mode: Ok(3),
policy_mode: Err(E_FILENOTFOUND.into()),
};
assert_eq!(get_setting_mode(&config).unwrap(), SudoMode::Normal);
assert_eq!(
get_allowed_mode_from_policy(&config).unwrap(),
SudoMode::Normal
);
assert_eq!(get_allowed_mode(&config).unwrap(), SudoMode::Normal);
// not set, but disabled by policy
let config = TestConfigProvider {
setting_mode: Err(E_FILENOTFOUND.into()),
policy_mode: Ok(0),
};
assert_eq!(get_setting_mode(&config).unwrap(), SudoMode::Disabled);
assert_eq!(
get_allowed_mode_from_policy(&config).unwrap(),
SudoMode::Disabled
);
assert_eq!(
get_allowed_mode(&config),
Err(E_ACCESS_DISABLED_BY_POLICY.into())
);
}
}

View File

@ -0,0 +1,146 @@
use crate::helpers::join_args;
use crate::messages::ElevateRequest;
use std::env;
use std::ffi::CString;
use std::mem::size_of_val;
use std::ops::{Deref, DerefMut};
use std::ptr::addr_of;
use windows::core::*;
use windows::Win32::System::Diagnostics::Etw::*;
// These come from cpp/logging/EventViewerLogging.c
extern "C" {
static PROVIDER_GUID: GUID;
static SudoRequestRunEvent: EVENT_DESCRIPTOR;
static SudoRecieveRunRequestEvent: EVENT_DESCRIPTOR;
}
#[repr(transparent)]
#[derive(Default)]
struct OwnedReghandle(pub u64);
impl Drop for OwnedReghandle {
fn drop(&mut self) {
if self.0 != 0 {
unsafe {
EventUnregister(self.0);
}
}
}
}
impl Deref for OwnedReghandle {
type Target = u64;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for OwnedReghandle {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
fn str_to_cstr_vec<T: Into<Vec<u8>>>(s: T) -> Vec<u8> {
CString::new(s)
.expect("strings should not have nulls")
.into_bytes_with_nul()
}
fn create_descriptor<T, U>(ptr: *const T, len: U) -> EVENT_DATA_DESCRIPTOR
where
U: TryInto<u32>,
<U as TryInto<u32>>::Error: std::fmt::Debug,
{
EVENT_DATA_DESCRIPTOR {
Ptr: ptr as _,
Size: len.try_into().unwrap(),
Anonymous: Default::default(),
}
}
/// Writes this request to the Windows Event Log. We do this for admins to be
/// able to audit who's calling what with sudo.
/// We log our messages to "Applications and Services Logs" -> "Microsoft" ->
/// "Windows" -> "Sudo" -> "Admin".
///
/// Alternatively, you can view this log with
/// `wevtutil qe Microsoft-Windows-Sudo/Admin /c:3 /rd:true /f:text`
pub fn event_log_request(is_client: bool, req: &ElevateRequest) {
let mut registration_handle = OwnedReghandle::default();
// The error code returned by EventRegister is primarily intended for use in debugging and diagnostic scenarios.
// Most production code should continue to run even if an ETW provider failed to register,
// so release builds should usually ignore the error code returned by EventRegister.
unsafe { EventRegister(&PROVIDER_GUID, None, None, &mut *registration_handle) };
let application = str_to_cstr_vec(req.application.as_str());
let args_len = req.args.len() as u32;
let args: Vec<_> = req
.args
.iter()
.map(|arg| str_to_cstr_vec(arg.as_str()))
.collect();
let cwd = str_to_cstr_vec(req.target_dir.as_str());
let mode = req.sudo_mode as u32;
let inherit_env = !req.env_vars.is_empty();
let redirected = req.handles.iter().any(|h| !h.is_invalid());
let commandline = str_to_cstr_vec(format!(
"{} {} {}",
env::current_exe().unwrap().display(),
req.application,
join_args(&req.args)
));
let request_id = req.event_id;
let mut descriptors = Vec::with_capacity(9 + args.len());
// <data name="Application" inType="win:AnsiString" outType="win:Utf8" />
descriptors.push(create_descriptor(application.as_ptr(), application.len()));
// <data name="ArgsCount" inType="win:UInt32" />
descriptors.push(create_descriptor(
addr_of!(args_len),
size_of_val(&args_len),
));
// <data name="Argument" inType="win:AnsiString" outType="win:Utf8" count="ArgsCount" />
for arg in &args {
descriptors.push(EVENT_DATA_DESCRIPTOR {
Ptr: arg.as_ptr() as _,
Size: arg.len() as u32,
Anonymous: Default::default(),
});
}
// <data name="CurrentWorkingDirectory" inType="win:AnsiString" outType="win:Utf8" />
descriptors.push(create_descriptor(cwd.as_ptr(), cwd.len()));
// <data name="Mode" inType="win:UInt32" />
descriptors.push(create_descriptor(addr_of!(mode), size_of_val(&mode)));
// <data name="InheritEnvironment" inType="win:UInt8" outType="win:Boolean" />
descriptors.push(create_descriptor(
addr_of!(inherit_env),
size_of_val(&inherit_env),
));
// <data name="Redirected" inType="win:UInt8" outType="win:Boolean" />
descriptors.push(create_descriptor(
addr_of!(redirected),
size_of_val(&redirected),
));
// <data name="FullCommandline" inType="win:AnsiString" outType="win:Utf8" />
descriptors.push(create_descriptor(commandline.as_ptr(), commandline.len()));
// <data name="RequestID" inType="win:GUID"/>
descriptors.push(create_descriptor(
addr_of!(request_id),
size_of_val(&request_id),
));
unsafe {
// We're literally using the same data template for both requests and
// responses. The only difference is the event ID's have different keywords
// (to ID who sent the event).
let event_id = if is_client {
&SudoRequestRunEvent
} else {
&SudoRecieveRunRequestEvent
};
EventWrite(*registration_handle, event_id, Some(&descriptors));
}
}

461
sudo/src/main.rs Normal file
View File

@ -0,0 +1,461 @@
mod elevate_handler;
mod helpers;
mod logging_bindings;
mod messages;
mod r;
mod rpc_bindings;
mod rpc_bindings_client;
mod rpc_bindings_server;
mod run_handler;
mod tests;
mod tracing;
use clap::{Arg, ArgAction, ArgMatches, Command};
use elevate_handler::start_rpc_server;
use helpers::*;
use run_handler::run_target;
use std::env;
use tracing::*;
use windows::{core::*, Win32::Foundation::*, Win32::System::Console::*};
// Clap does provide a nice macro for args, which defines args with a snytax
// close to what the actual help text would be. Unfortunately, we're not using
// that macro, because it doesn't play well with localization. The comments
// throughout here help show how the macro would have worked.
fn sudo_cli(allowed_mode: i32) -> Command {
const POLICY_DENIED_LABEL: i32 = E_ACCESS_DISABLED_BY_POLICY.0;
let mut app = Command::new(env!("CARGO_CRATE_NAME"));
match allowed_mode {
0 => {
// In this case, our error message has some VT in it, so we need to
// turn VT on before we eventually print the message. Fortunately,
// the `check_enabled_or_bail` will make sure to enable & restore VT
// mode before printing that error.
// Sudo is disabled. We want to inform them when they see the help text.
app = app
.about(r::IDS_SUDONAME.get())
.long_about(r::IDS_DISABLEDLONGABOUT.get())
.override_help(r::IDS_DISABLEDLONGABOUT.get());
}
POLICY_DENIED_LABEL => {
// Sudo is disabled by policy. The help text will be more specific.
app = app
.about(r::IDS_SUDONAME.get())
.long_about(r::IDS_DISABLEDBYPOLICY.get())
.override_help(r::IDS_DISABLEDBYPOLICY.get());
}
_ => {
// Sudo is enabled. The help text will be standard.
app = app
.about(r::IDS_SUDONAME.get())
.long_about(r::IDS_LONGABOUT.get());
}
}
app = app
.subcommand_required(false)
.arg_required_else_help(true)
.allow_external_subcommands(true)
.version(env!("CARGO_PKG_VERSION"))
.args(run_args())
.subcommand(run_builder())
.subcommand(
// The elevate command is hidden, and not documented in the help text.
Command::new("elevate")
.about(r::IDS_ELEVATE_ABOUT.get())
.hide(true)
.disable_help_flag(true)
// .arg(arg!(-p <PARENT> "Parent process ID").required(true))
.arg(
Arg::new("PARENT")
.short('p')
.help(r::IDS_ELEVATE_PARENT.get())
.required(true),
)
// .arg(arg!([COMMANDLINE] ... "")),
.arg(
Arg::new("COMMANDLINE")
.help(r::IDS_ELEVATE_COMMANDLINE.get())
.action(ArgAction::Append)
.trailing_var_arg(true),
),
)
.disable_help_flag(true)
.disable_version_flag(true)
.arg(
Arg::new("help")
.action(ArgAction::Help)
.short('h')
.long("help")
.help(r::IDS_BASE_HELP_HELP_SHORT.get())
.long_help(r::IDS_BASE_HELP_HELP_LONG.get()),
)
.arg(
Arg::new("version")
.action(ArgAction::Version)
.short('V')
.long("version")
.help(r::IDS_BASE_VERSION_HELP.get()),
);
let config = Command::new("config").about(r::IDS_CONFIG_ABOUT.get()).arg(
Arg::new("enable")
.long("enable")
.value_parser([
"disable",
"enable",
"forceNewWindow",
"disableInput",
"normal",
"default",
])
.default_missing_value_os("default")
.required(false)
.action(ArgAction::Set),
);
app = app.subcommand(config);
app
}
fn run_builder() -> Command {
Command::new("run")
.about(r::IDS_RUN_ABOUT.get())
.arg_required_else_help(true)
.args(run_args())
}
fn run_args() -> Vec<clap::Arg> {
// trailing_var_arg and allow_hyphen_values are needed to allow passing in a
// command like `sudo netstat -ab` to work as expected, instead of having
// the parser attempt to treat the `-ab` as args to sudo itself.
let args = vec![
// arg!(-E --"preserve-env" "pass the current environment variables to the command")
Arg::new("copyEnv")
.short('E')
.long("preserve-env")
.help(r::IDS_RUN_COPYENV_HELP.get())
.action(ArgAction::SetTrue),
// arg!(-N --"new-window" "Use a new window for the command.")
Arg::new("newWindow")
.short('N')
.long("new-window")
.help(r::IDS_RUN_NEWWINDOW_HELP.get())
.action(ArgAction::SetTrue)
.group("mode"),
// arg!(--"disable-input" "Disable input to the target application")
Arg::new("disableInput")
.long("disable-input")
.help(r::IDS_RUN_DISABLEINPUT_HELP.get())
.action(ArgAction::SetTrue)
.group("mode"),
// arg!(--"inline" "Run in the current terminal")
Arg::new("inline")
.long("inline")
.help(r::IDS_RUN_INLINE_HELP.get())
.action(ArgAction::SetTrue)
.group("mode"),
// arg!(--"chdir"=<DIR> "Change the working directory to DIR before running the command.")
Arg::new("chdir")
.short('D')
.long("chdir")
.help(r::IDS_RUN_CHDIR_HELP.get())
.action(ArgAction::Set),
// arg!([COMMANDLINE] ... "Command-line to run")
Arg::new("COMMANDLINE")
.help(r::IDS_RUN_COMMANDLINE_HELP.get())
.action(ArgAction::Append)
.trailing_var_arg(true),
];
// The following is a demo of how feature flags might work in the sudo
// codebase. You can add a `Feature_test_flag` feature to the Dev branding
// in cargo.toml, and then add conditionally enabled code, like so:
//
// if cfg!(feature = "Feature_test_flag") {
// args.append(&mut vec![Arg::new("setHome")
// .short('H')
// .long("set-home")
// .help(r::IDS_RUN_SETHOME_HELP.get())
// .action(ArgAction::SetTrue)]);
// }
args
}
fn log_modes(requested_mode: Option<SudoMode>) {
let config: RegistryConfigProvider = Default::default();
let setting_mode = get_setting_mode(&config).unwrap_or(SudoMode::Disabled) as u32;
let policy_mode = {
let policy_enabled = windows_registry::LOCAL_MACHINE
.open("SOFTWARE\\Policies\\Microsoft\\Windows\\Sudo")
.and_then(|key| key.get_u32("Enabled"));
if let Err(e) = &policy_enabled {
if e.code() == E_FILENOTFOUND {
0xffffffff
} else {
0
}
} else {
policy_enabled.unwrap_or(0)
}
};
// Trace "disabled" as "they didn't pass a mode manually".
trace_modes(
requested_mode.unwrap_or(SudoMode::Disabled) as u32,
setting_mode,
policy_mode,
);
}
fn check_enabled_or_bail() -> SudoMode {
let config: RegistryConfigProvider = Default::default();
// First things first: Make sure we're enabled.
match get_allowed_mode(&config) {
Err(e) => {
if e.code() == E_ACCESSDENIED {
// Any time you want to use IDS_DISABLEDLONGABOUT, make sure you turned on VT first
let mode = enable_vt();
eprintln!("{}", r::IDS_DISABLEDLONGABOUT.get());
_ = restore_console_mode(mode);
} else if e.code() == ERROR_ACCESS_DISABLED_BY_POLICY.into() {
eprintln!("{}", r::IDS_DISABLEDBYPOLICY.get());
} else {
eprintln!("{} {}", r::IDS_UNKNOWNERROR.get(), e);
}
std::process::exit(e.code().0);
}
Ok(SudoMode::Disabled) => {
// Any time you want to use IDS_DISABLEDLONGABOUT, make sure you turned on VT first
let mode = enable_vt();
eprintln!("{}", r::IDS_DISABLEDLONGABOUT.get());
_ = restore_console_mode(mode);
std::process::exit(E_ACCESSDENIED.0);
}
Ok(mode) => mode,
}
}
/// We want to be able to conditionally control what the help text shows,
/// depending on if sudo is enabled, disabled, or disabled by policy. This
/// helper lets us do that more easily. This will return:
/// * 0 if sudo is disabled
/// * E_ACCESS_DISABLED_BY_POLICY if sudo is disabled by policy
/// * or the current mode (>0), if sudo is enabled.
fn allowed_mode_for_help() -> i32 {
let config: RegistryConfigProvider = Default::default();
match get_allowed_mode(&config) {
Err(e) => {
if e.code() == E_ACCESSDENIED {
0
} else if e.code() == E_ACCESS_DISABLED_BY_POLICY {
E_ACCESS_DISABLED_BY_POLICY.0
} else {
0
}
}
Ok(SudoMode::Disabled) => 0,
Ok(mode) => mode.into(),
}
}
/// Try to enable VT processing in the console, but also ignore any errors.
fn enable_vt() -> CONSOLE_MODE {
enable_vt_or_err().unwrap_or_default()
}
fn enable_vt_or_err() -> Result<CONSOLE_MODE> {
unsafe {
let mut console_mode = CONSOLE_MODE::default();
let console_handle = GetStdHandle(STD_OUTPUT_HANDLE)?;
GetConsoleMode(console_handle, &mut console_mode)?;
SetConsoleMode(
console_handle,
console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING,
)?;
Ok(console_mode)
}
}
fn restore_console_mode(mode: CONSOLE_MODE) -> Result<()> {
unsafe {
let console_handle = GetStdHandle(STD_OUTPUT_HANDLE)?;
SetConsoleMode(console_handle, mode)?;
}
Ok(())
}
fn main() {
enable_tracing();
trace_log_message(&format!("raw commandline: {:?}", env::args_os()));
let mode_for_help = allowed_mode_for_help();
let matches = sudo_cli(mode_for_help).get_matches();
let res = match matches.subcommand() {
Some(("elevate", sub_matches)) => do_elevate(sub_matches),
Some(("run", sub_matches)) => do_run(sub_matches),
Some(("config", sub_matches)) => do_config(sub_matches),
_ => do_run(&matches),
};
let code = res.unwrap_or_else(|err| {
let hr = err.code();
let mut code = hr.0;
match hr {
E_DIR_BAD_COMMAND_OR_FILE => {
eprintln!("{}", r::IDS_COMMANDNOTFOUND.get());
code = MSG_DIR_BAD_COMMAND_OR_FILE.0 as i32;
}
E_CANCELLED => {
eprintln!("{}", r::IDS_CANCELLED.get());
}
_ if hr == HRESULT::from_win32(ERROR_REQUEST_REFUSED.0) => {
eprintln!("{}", r::IDS_SUDO_DISALLOWED.get());
}
_ => {
eprintln!("{} {}", r::IDS_UNKNOWNERROR.get(), err);
}
};
code
});
// Normally this is where we'd construct an ExitCode and return it from main(),
// but it only supports u8 (...why?) and windows_process_exit_code_from is an unstable feature.
std::process::exit(code)
}
fn do_run(matches: &ArgMatches) -> Result<i32> {
let commandline = matches
.get_many::<String>("COMMANDLINE")
.into_iter()
.flatten()
.collect::<Vec<_>>();
// Didn't pass a commandline or just "/?"? Print the help text and bail, BEFORE checking the mode.
if commandline.is_empty() || (commandline.len() == 1 && commandline[0] == "/?") {
_ = run_builder().print_long_help();
// return exit status 1 if the commandline was empty, 0 otherwise
return Ok(commandline.is_empty().into());
}
let requested_dir: Option<String> = matches.get_one::<String>("chdir").map(|s| s.into());
let allowed_mode = check_enabled_or_bail();
let copy_env = matches.get_flag("copyEnv");
if !can_current_user_elevate()? {
// Bail out with an error. main(0) will then print the error message to
// the user to let them know they aren't allowed to run sudo.
return Err(ERROR_REQUEST_REFUSED.into());
}
let requested_mode = if matches.get_flag("newWindow") {
Some(SudoMode::ForceNewWindow)
} else if matches.get_flag("disableInput") {
Some(SudoMode::DisableInput)
} else if matches.get_flag("inline") {
Some(SudoMode::Normal)
} else {
None
};
log_modes(requested_mode);
if let Some(mode) = requested_mode {
if mode > allowed_mode {
match allowed_mode {
SudoMode::Disabled => {} // This is already handled by check_enabled_or_bail
SudoMode::ForceNewWindow => eprintln!("{}", r::IDS_MAXRUNMODE_FORCENEWWINDOW.get()),
SudoMode::DisableInput => eprintln!("{}", r::IDS_MAXRUNMODE_DISABLEINPUT.get()),
SudoMode::Normal => {} // not possible to exceed normal mode
}
std::process::exit(-1);
}
}
let actual_mode = std::cmp::min(allowed_mode, requested_mode.unwrap_or(allowed_mode));
run_target(copy_env, &commandline, actual_mode, requested_dir)
}
fn do_elevate(matches: &ArgMatches) -> Result<i32> {
_ = check_enabled_or_bail();
let parent_pid = matches.get_one::<String>("PARENT").unwrap().parse::<u32>();
if let Err(e) = &parent_pid {
eprintln!("{} {}", r::IDS_UNKNOWNERROR.get(), e);
std::process::exit(-1);
}
trace_log_message(&format!("elevate starting for parent: {parent_pid:?}"));
trace_log_message(&format!("matches: {matches:?}"));
let commandline = matches
.get_many::<String>("COMMANDLINE")
.into_iter()
.flatten()
.collect::<Vec<_>>();
let result = start_rpc_server(parent_pid.ok().unwrap(), None, &commandline);
trace_log_message(&format!("elevate result: {result:?}"));
result
}
fn do_config(matches: &ArgMatches) -> Result<i32> {
let mode = match matches.get_one::<String>("enable") {
Some(mode) => {
let mode = match mode.as_str() {
"disable" => SudoMode::Disabled,
"enable" => SudoMode::Normal,
"forceNewWindow" => SudoMode::ForceNewWindow,
"disableInput" => SudoMode::DisableInput,
"normal" => SudoMode::Normal,
"default" => SudoMode::Normal,
_ => {
eprintln!("{} {}", r::IDS_INVALIDMODE.get(), mode);
std::process::exit(-1);
}
};
try_enable_sudo(mode)?;
mode
}
None => check_enabled_or_bail(),
};
match mode {
SudoMode::Disabled => println!("{}", r::IDS_DISABLEDMESSAGE.get()),
SudoMode::ForceNewWindow => println!("{}", r::IDS_CURRENTMODE_FORCENEWWINDOW.get()),
SudoMode::DisableInput => println!("{}", r::IDS_CURRENTMODE_DISABLEINPUT.get()),
SudoMode::Normal => println!("{}", r::IDS_CURRENTMODE_INLINE.get()),
}
Ok(0)
}
fn try_enable_sudo(requested_mode: SudoMode) -> Result<()> {
let elevated = is_running_elevated()?;
if !elevated {
eprintln!("{}", r::IDS_REQUIREADMINTOCONFIG.get());
std::process::exit(-1);
}
let config: RegistryConfigProvider = Default::default();
let max_mode = get_allowed_mode_from_policy(&config)?;
if requested_mode > max_mode {
match max_mode {
SudoMode::Disabled => eprintln!("{}", r::IDS_DISABLEDBYPOLICY.get()),
SudoMode::ForceNewWindow => eprintln!("{}", r::IDS_MAXPOLICYMODE_FORCENEWWINDOW.get()),
SudoMode::DisableInput => eprintln!("{}", r::IDS_MAXPOLICYMODE_DISABLEINPUT.get()),
SudoMode::Normal => eprintln!("{}", r::IDS_MAXPOLICYMODE_INLINE.get()),
}
std::process::exit(-1);
}
let result = windows_registry::LOCAL_MACHINE
.create("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Sudo")
.and_then(|key| key.set_u32("Enabled", requested_mode.into()));
if let Err(err) = result {
eprintln!("{} {}", r::IDS_ERRORSETTINGMODE.get(), err);
return Err(err);
}
Ok(())
}

13
sudo/src/messages.rs Normal file
View File

@ -0,0 +1,13 @@
use crate::helpers::SudoMode;
use windows::{core::GUID, Win32::Foundation::HANDLE};
pub struct ElevateRequest {
pub parent_pid: u32,
pub handles: [HANDLE; 3], // in, out, err
pub sudo_mode: SudoMode,
pub application: String,
pub args: Vec<String>,
pub target_dir: String,
pub env_vars: String,
pub event_id: GUID,
}

23
sudo/src/r.rs Normal file
View File

@ -0,0 +1,23 @@
//! This file includes all our resource IDs, and the code to load them. The
//! handy string_resources macro does the magic to create a StaticStringResource
//! for each of the resource IDs, and then we can use them in code.
//!
//! Example usage:
//! let world = r::IDS_WORLD.get();
//! println!("Hello: {}", world);
#![allow(dead_code)]
use win32resources::StaticStringResource;
macro_rules! string_resources {
(
$(
$name:ident = $value:expr ;
)*
) => {
$(
pub static $name: StaticStringResource = StaticStringResource::new($value, stringify!($name));
)*
}
}
include!("../../Generated Files/resource_ids.rs");

34
sudo/src/rpc_bindings.rs Normal file
View File

@ -0,0 +1,34 @@
use std::cmp::min;
use std::marker::PhantomData;
use std::slice::from_raw_parts;
use std::str::from_utf8;
use windows::{core::*, Win32::Foundation::*};
#[repr(C)]
#[derive(Copy, Clone)]
pub struct Utf8Str<'a> {
length: u32,
data: *const u8,
_marker: PhantomData<&'a str>,
}
impl<'a> Utf8Str<'a> {
pub fn new(s: &str) -> Utf8Str {
Utf8Str {
length: min(s.len(), u32::MAX as usize) as u32,
data: s.as_ptr(),
_marker: PhantomData,
}
}
pub fn as_str(&self) -> Result<&str> {
from_utf8(unsafe { from_raw_parts(self.data, self.length as usize) })
.map_err(|_| ERROR_NO_UNICODE_TRANSLATION.to_hresult().into())
}
}
impl<'a> From<&'a str> for Utf8Str<'a> {
fn from(value: &'a str) -> Self {
Utf8Str::new(value)
}
}

View File

@ -0,0 +1,109 @@
use crate::helpers::SudoMode;
use crate::rpc_bindings::Utf8Str;
use std::ffi::{c_void, CStr};
use windows::core::{s, GUID, HRESULT, PCSTR, PSTR};
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Storage::FileSystem::{GetFileType, FILE_TYPE_DISK, FILE_TYPE_PIPE};
use windows::Win32::System::Rpc::{
RpcBindingFree, RpcBindingFromStringBindingA, RpcMgmtIsServerListening,
RpcStringBindingComposeA, RpcStringFreeA, RPC_STATUS, RPC_S_OK,
};
extern "C" {
static mut client_sudo_rpc_ClientIfHandle: *mut c_void;
fn seh_wrapper_client_DoElevationRequest(
binding: *mut c_void,
parent_handle: HANDLE,
pipe_handles: *const [HANDLE; 3], // in, out, err
file_handles: *const [HANDLE; 3], // in, out, err
sudo_mode: u32,
application: Utf8Str,
args: Utf8Str,
target_dir: Utf8Str,
env_vars: Utf8Str,
event_id: GUID,
child: *mut HANDLE,
) -> HRESULT;
fn seh_wrapper_client_Shutdown(binding: *mut c_void) -> HRESULT;
}
pub fn rpc_client_setup(endpoint: &CStr) -> RPC_STATUS {
unsafe {
let mut string_binding = PSTR::null();
let status = RpcStringBindingComposeA(
/* ObjUuid */ None,
/* ProtSeq */ s!("ncalrpc"),
/* NetworkAddr */ None,
/* Endpoint */ PCSTR(endpoint.as_ptr() as _),
/* Options */ None,
/* StringBinding */ Some(&mut string_binding),
);
if status != RPC_S_OK {
return status;
}
let status = RpcBindingFromStringBindingA(
PCSTR(string_binding.0),
std::ptr::addr_of_mut!(client_sudo_rpc_ClientIfHandle),
);
// Don't forget to free the previously allocated string before potentially returning. :)
RpcStringFreeA(&mut string_binding);
if status != RPC_S_OK {
return status;
}
RpcMgmtIsServerListening(Some(client_sudo_rpc_ClientIfHandle))
}
}
/// Cleans up the RPC server. This is implemented on the server-side in
/// server_Shutdown. It will TerminateProcess the RPC server, really really
/// making sure no one can use it anymore.
pub fn rpc_client_cleanup() {
unsafe {
_ = seh_wrapper_client_Shutdown(client_sudo_rpc_ClientIfHandle);
_ = RpcBindingFree(std::ptr::addr_of_mut!(client_sudo_rpc_ClientIfHandle));
}
}
#[allow(clippy::too_many_arguments)]
pub fn rpc_client_do_elevation_request(
parent_handle: HANDLE,
handles: &[HANDLE; 3], // in, out, err
sudo_mode: SudoMode,
application: Utf8Str,
args: Utf8Str,
target_dir: Utf8Str,
env_vars: Utf8Str,
event_id: GUID,
child: *mut HANDLE,
) -> HRESULT {
let mut pipe_handles = [HANDLE::default(); 3];
let mut file_handles = [HANDLE::default(); 3];
for i in 0..3 {
match unsafe { GetFileType(handles[i]) } {
FILE_TYPE_PIPE => pipe_handles[i] = handles[i],
FILE_TYPE_DISK => file_handles[i] = handles[i],
_ => {}
}
}
unsafe {
seh_wrapper_client_DoElevationRequest(
client_sudo_rpc_ClientIfHandle,
parent_handle,
&pipe_handles,
&file_handles,
sudo_mode.into(),
application,
args,
target_dir,
env_vars,
event_id,
child,
)
}
}

View File

@ -0,0 +1,295 @@
use crate::helpers::*;
use crate::{
elevate_handler::handle_elevation_request, messages::ElevateRequest, rpc_bindings::Utf8Str,
};
use std::ffi::{c_void, CStr};
use std::mem::{size_of, take};
use std::ptr::null_mut;
use std::sync::atomic::{AtomicBool, Ordering};
use windows::Win32::Foundation::{ERROR_BUSY, GENERIC_ALL, HANDLE, PSID};
use windows::{
core::*, Win32::Security::Authorization::*, Win32::Security::*, Win32::System::Memory::*,
Win32::System::Rpc::*, Win32::System::SystemServices::*, Win32::System::Threading::*,
};
extern "C" {
static mut server_sudo_rpc_ServerIfHandle: *mut c_void;
}
static mut EXPECTED_CLIENT_PID: u32 = 0;
// Process-wide mutex to ensure that only one request is handled at a time. The
// bool inside the atomic is true if we've already started handling a request.
static RPC_SERVER_IN_USE: AtomicBool = AtomicBool::new(false);
// * Context: The callback function may pass this handle to
// RpcImpersonateClient, RpcBindingServerFromClient,
// RpcGetAuthorizationContextForClient, or any other server side function that
// accepts a client binding handle to obtain information about the client.
//
// The callback function should return RPC_S_OK, if the client is allowed to
// call methods in this interface.
unsafe extern "system" fn rpc_server_callback(
_interface_uuid: *const c_void,
context: *const c_void,
) -> RPC_STATUS {
let mut client_handle = OwnedHandle::default();
let status = I_RpcOpenClientProcess(
Some(context),
PROCESS_QUERY_LIMITED_INFORMATION.0,
&mut *client_handle as *mut _ as _,
);
if status != RPC_S_OK {
return status;
}
// Check #1: We'll check that the client process is the one we expected,
// when we were first started.
let client_pid = GetProcessId(*client_handle); // if this fails, it returns 0
if client_pid == 0 || client_pid != EXPECTED_CLIENT_PID {
return RPC_S_ACCESS_DENIED;
}
// Check #2: Check that the client process is the same as the server process.
if check_client(*client_handle).is_err() {
return RPC_S_ACCESS_DENIED;
}
RPC_S_OK
}
// MSDN regarding SetSecurityDescriptorSacl:
// > The SACL is referenced by, not copied into, the security descriptor.
// --> To return a SD we need to hold onto everything we allocated. Yay.
#[derive(Default)]
struct OwnedSecurityDescriptor {
pub sd: SECURITY_DESCRIPTOR,
sacl: OwnedLocalAlloc<*mut ACL>,
dacl: OwnedLocalAlloc<*mut ACL>,
}
// Creates a descriptor of form
// D:(A;;GA;;;<pid's sid>)S:(ML;;NWNRNX;;;ME)
fn create_security_descriptor_for_process(pid: u32) -> Result<OwnedSecurityDescriptor> {
unsafe {
let mut s: OwnedSecurityDescriptor = Default::default();
let psd = PSECURITY_DESCRIPTOR(&mut s.sd as *mut _ as _);
InitializeSecurityDescriptor(psd, SECURITY_DESCRIPTOR_REVISION)?;
// SACL
{
let user = {
let process =
OwnedHandle::new(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid)?);
get_sid_for_process(*process)?
};
let ea = [EXPLICIT_ACCESS_W {
grfAccessPermissions: GENERIC_ALL.0,
grfAccessMode: SET_ACCESS,
grfInheritance: NO_INHERITANCE,
Trustee: TRUSTEE_W {
pMultipleTrustee: null_mut(),
MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE,
TrusteeForm: TRUSTEE_IS_SID,
TrusteeType: TRUSTEE_IS_USER,
ptstrName: PWSTR(&user.Sid as *const _ as _),
},
}];
SetEntriesInAclW(Some(&ea), None, &mut *s.dacl).ok()?;
SetSecurityDescriptorDacl(psd, true, Some(*s.dacl), false)?;
}
// DACL
{
// windows-rs doesn't have a definition for this macro.
const SECURITY_MAX_SID_SIZE: usize = 88;
let mut sid_buffer = [0u8; SECURITY_MAX_SID_SIZE];
let mut sid_len = sid_buffer.len() as u32;
let sid = PSID(&mut sid_buffer as *mut _ as _);
CreateWellKnownSid(WinMediumLabelSid, None, sid, &mut sid_len)?;
const SACL_BUFFER_PREFIX_LEN: usize =
size_of::<ACL>() + size_of::<SYSTEM_MANDATORY_LABEL_ACE>();
let sacl_len = SACL_BUFFER_PREFIX_LEN as u32 + sid_len;
s.sacl.0 = LocalAlloc(LMEM_FIXED, sacl_len as usize)?.0 as _;
InitializeAcl(*s.sacl, sacl_len, ACL_REVISION)?;
AddMandatoryAce(
*s.sacl,
ACL_REVISION,
ACE_FLAGS(0),
SYSTEM_MANDATORY_LABEL_NO_READ_UP
| SYSTEM_MANDATORY_LABEL_NO_WRITE_UP
| SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP,
sid,
)?;
SetSecurityDescriptorSacl(psd, true, Some(*s.sacl), false)?;
}
Ok(s)
}
}
pub fn rpc_server_setup(endpoint: &CStr, expected_client_pid: u32) -> Result<()> {
let owned_sd = create_security_descriptor_for_process(expected_client_pid)?;
unsafe {
RpcServerUseProtseqEpA(
/* Protseq */ s!("ncalrpc"),
/* MaxCalls */ RPC_C_LISTEN_MAX_CALLS_DEFAULT,
/* Endpoint */ PCSTR(endpoint.as_ptr() as _),
/* SecurityDescriptor */ Some(&owned_sd.sd as *const _ as _),
)
.ok()?;
RpcServerRegisterIf3(
/* IfSpec */ server_sudo_rpc_ServerIfHandle,
/* MgrTypeUuid */ None,
/* MgrEpv */ None,
/* Flags */ RPC_IF_ALLOW_LOCAL_ONLY | RPC_IF_ALLOW_SECURE_ONLY,
/* MaxCalls */ RPC_C_LISTEN_MAX_CALLS_DEFAULT,
/* MaxRpcSize */ u32::MAX,
/* IfCallback */ Some(rpc_server_callback),
/* SecurityDescriptor */ Some(&owned_sd.sd as *const _ as _),
)
.ok()?;
EXPECTED_CLIENT_PID = expected_client_pid;
let res = RpcServerListen(
/* MinimumCallThreads */ 1,
/* MaxCalls */ RPC_C_LISTEN_MAX_CALLS_DEFAULT,
/* DontWait */ 0,
);
if res.is_err() {
_ = RpcServerUnregisterIf(None, None, 0);
}
res.ok()
}
}
// This is the RPC's sudo_rpc::Shutdown callback function.
#[no_mangle]
unsafe extern "C" fn server_Shutdown(_binding: *const c_void) {
_ = TerminateProcess(GetCurrentProcess(), 0);
}
// This is the RPC's sudo_rpc::DoElevationRequest callback function.
#[no_mangle]
pub extern "C" fn server_DoElevationRequest(
_binding: *const c_void,
parent_handle: HANDLE,
pipe_handles: *const [HANDLE; 3], // in, out, err
file_handles: *const [HANDLE; 3], // in, out, err
sudo_mode: u32,
application: Utf8Str,
args: Utf8Str,
target_dir: Utf8Str,
env_vars: Utf8Str,
event_id: GUID,
child: *mut HANDLE,
) -> HRESULT {
// Only the first caller will get their request handled. Everyone else will
// be forced to bail out.
if RPC_SERVER_IN_USE.swap(true, Ordering::Relaxed) {
// We're already in the middle of handling a request.
return ERROR_BUSY.to_hresult();
}
// Here, we've locked the mutex and we're the only ones handling a request.
//
// And, we've set the atom to true, so if someone _does_ connect to us after
// this function releases the lock, then they'll also bail out.
// Immediately unregister ourself. This will prevent a future caller from
// getting to us (but won't cancel the current request we're already in the
// middle of replying to).
unsafe {
_ = RpcMgmtStopServerListening(None);
_ = RpcServerUnregisterIf(None, None, 0);
}
let result = wrap_elevate_request(
parent_handle,
pipe_handles,
file_handles,
sudo_mode,
application,
args,
target_dir,
env_vars,
event_id,
)
.and_then(|req| handle_elevation_request(&req));
match result {
Ok(mut handle) => {
unsafe { child.write(take(&mut handle.0)) };
HRESULT::default()
}
Err(err) => err.into(),
}
}
#[allow(clippy::too_many_arguments)]
fn wrap_elevate_request(
parent_handle: HANDLE,
pipe_handles: *const [HANDLE; 3], // in, out, err
file_handles: *const [HANDLE; 3], // in, out, err
sudo_mode: u32,
application: Utf8Str,
args: Utf8Str,
target_dir: Utf8Str,
env_vars: Utf8Str,
event_id: GUID,
) -> Result<ElevateRequest> {
let parent_pid = unsafe { GetProcessId(parent_handle) };
let handles = unsafe {
let pipes = &*pipe_handles;
let files = &*file_handles;
std::array::from_fn(|i| if pipes[i].0 != 0 { pipes[i] } else { files[i] })
};
Ok(ElevateRequest {
parent_pid,
handles,
sudo_mode: sudo_mode.try_into()?,
application: application.as_str()?.to_owned(),
args: unpack_string_list_from_rpc(args)?,
target_dir: target_dir.as_str()?.to_owned(),
env_vars: env_vars.as_str()?.to_owned(),
event_id,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_security_descriptor_for_process() {
fn sd_to_string(sd: PSECURITY_DESCRIPTOR) -> Result<String> {
unsafe {
let mut buffer = PSTR::null();
ConvertSecurityDescriptorToStringSecurityDescriptorA(
sd,
SDDL_REVISION,
DACL_SECURITY_INFORMATION
| LABEL_SECURITY_INFORMATION
| OWNER_SECURITY_INFORMATION,
&mut buffer,
None,
)?;
Ok(buffer.to_string()?)
}
}
let s = create_security_descriptor_for_process(unsafe { GetCurrentProcessId() }).unwrap();
let str = sd_to_string(PSECURITY_DESCRIPTOR(&s.sd as *const _ as _)).unwrap();
assert!(str.starts_with("D:(A;;GA;;;"));
assert!(str.ends_with(")S:(ML;;NWNRNX;;;ME)"));
}
}

549
sudo/src/run_handler.rs Normal file
View File

@ -0,0 +1,549 @@
use crate::elevate_handler::spawn_target_for_request;
use crate::helpers::*;
use crate::logging_bindings::event_log_request;
use crate::messages::ElevateRequest;
use crate::rpc_bindings::Utf8Str;
use crate::rpc_bindings_client::{
rpc_client_cleanup, rpc_client_do_elevation_request, rpc_client_setup,
};
use crate::{r, tracing};
use std::env;
use std::ffi::{CString, OsStr};
use std::path::Path;
use windows::Wdk::Foundation::{NtQueryObject, ObjectBasicInformation};
use windows::Win32::System::WindowsProgramming::PUBLIC_OBJECT_BASIC_INFORMATION;
use windows::{
core::*, Wdk::System::Threading::*, Win32::Foundation::*, Win32::Storage::FileSystem::*,
Win32::System::Console::*, Win32::System::Diagnostics::Debug::*, Win32::System::Rpc::*,
Win32::System::SystemInformation::*, Win32::System::Threading::*, Win32::UI::Shell::*,
Win32::UI::WindowsAndMessaging::*,
};
fn current_elevation_matches_request(is_admin: bool, _req: &ElevateRequest) -> bool {
// FUTURE TODO: actually support running as another user.
is_admin
}
/// helper to find the process creation time for a given process handle
/// process_handle: handle to the process to get the creation time for. This is a non-owning handle.
fn get_process_creation_time(process_handle: HANDLE) -> Result<FILETIME> {
unsafe {
// You actually have to pass in valid pointers to these, even if we don't need them.
let mut creation_time = FILETIME::default();
let mut exit_time = FILETIME::default();
let mut kernel_time = FILETIME::default();
let mut user_time = FILETIME::default();
GetProcessTimes(
process_handle,
&mut creation_time,
&mut exit_time,
&mut kernel_time,
&mut user_time,
)?;
Ok(creation_time)
}
}
fn is_in_windows_dir(path: &Path) -> bool {
let path = HSTRING::from(path);
if path.len() >= MAX_PATH as usize {
return false;
}
let mut win_dir = [0u16; MAX_PATH as usize];
let len = unsafe { GetWindowsDirectoryW(Some(&mut win_dir)) };
if len == 0 || len >= MAX_PATH {
return false;
}
unsafe { PathIsPrefixW(PCWSTR(win_dir.as_ptr()), PCWSTR(path.as_ptr())).as_bool() }
}
/// Attempts to modify this request to run the command in CMD, if the
/// "application" that was passed to us was really just a CMD intrinsic. This is
/// used to support things like `sudo dir.
///
/// We don't do any modification if the parent process was some variety of
/// PowerShell. There's impossible to resolve issues repackaging the args back
/// into a PowerShell command, so we're hoping that the sudo.ps1 script will
/// handle that case instead.
///
/// * Returns an error if we failed to get the parent pid, or otherwise lookup
/// info we needed.
/// * Returns true if the application was a CMD intrinsic AND we were spawned
/// from CMD, and we adjusted the args accordingly.
/// * Returns false if the application was not an intrinsic or cmdlet
fn adjust_args_for_intrinsics_and_cmdlets(req: &mut ElevateRequest) -> Result<bool> {
// First things first: Get our parent process PID, with NtQueryInformationProcess
let parent_pid = unsafe {
let mut process_info = PROCESS_BASIC_INFORMATION::default();
let mut return_len = 0u32;
let get_parent_pid = NtQueryInformationProcess(
GetCurrentProcess(),
ProcessBasicInformation,
&mut process_info as *mut _ as _,
std::mem::size_of::<PROCESS_BASIC_INFORMATION>() as u32,
&mut return_len,
)
.ok();
if let Err(err) = get_parent_pid {
tracing::trace_log_message(&format!("Error getting parent pid: {:?}", err.code().0));
return Err(err);
}
process_info.InheritedFromUniqueProcessId
};
tracing::trace_log_message(&format!("parent_pid: {parent_pid:?}"));
// Now, open that process so we can query some more information about it.
// (stick it in an OwnedHandle so it gets closed when we're done with it)
let parent_process_handle = unsafe {
OwnedHandle::new(OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION,
false,
parent_pid.try_into().unwrap(),
)?)
};
// Sanity check time!
// Was the parent process started _before us_?
// Compare the two. If the parent process was created _after_ us, then we want to bail (with Ok(false))
unsafe {
let parent_process_creation_time = get_process_creation_time(*parent_process_handle)?;
let our_creation_time = get_process_creation_time(GetCurrentProcess())?;
if CompareFileTime(&parent_process_creation_time, &our_creation_time) == 1 {
// Parent process was created after us. Bail.
return Ok(false);
}
}
// Now, get the full path to the parent process
let parent_process_path =
get_process_path_from_handle(*parent_process_handle).unwrap_or_default();
tracing::trace_log_message(&format!("parent_process_str: {:?}", parent_process_path));
if !parent_process_path.ends_with("cmd.exe") {
// It's not. Bail.
return Ok(false);
}
// We're using the Windows dir here, because we might be a x64 sudo
// that's being run from a x86 cmd.exe (which is _actually_ in syswow64).
if !is_in_windows_dir(&parent_process_path) {
// It's not. Bail.
return Ok(false);
}
// Here, our parent is in fact cmd.exe (any arch).
if is_cmd_intrinsic(&req.application) {
tracing::trace_cmd_builtin_found(&req.application);
req.args
.splice(0..0, ["/c".to_string(), req.application.clone()]);
// Toss this back at _exactly our parent process_. This makes sure we
// don't try to invoke the x64 cmd.exe from a x86 cmd.exe
req.application = parent_process_path.to_string_lossy().to_string();
return Ok(true);
}
Ok(false)
}
fn adjust_args_for_gui_exes(req: &mut ElevateRequest) {
// We did find the command. We're now gonna try to find out if the file
// is:
// - An command line exe
// - A GUI exe
// - Just a plain old file (not an exe)
//
// Depending on what it is, we'll need to modify our request to run it.
// A Windows GUI exe can just be shell executed directly.
// TODO: We may want to do other work in the future for plain, non-executable files.
let (is_exe, is_gui) = match get_exe_subsystem(&req.application) {
Ok(subsystem) => (true, subsystem == IMAGE_SUBSYSTEM_WINDOWS_GUI),
Err(..) => (false, false),
};
tracing::trace_log_message(&format!("is_exe: {is_exe}"));
tracing::trace_log_message(&format!("is_gui: {is_gui}"));
// TODO: figure out how to handle non-exe files. ShellExecute(runas,
// ...) doesn't do anything for them, and I'm not sure we can trivially
// have the service find out what the right verb is for an arbitrary
// extension. (this is the kind of comment I'm sure to be proven wrong
// about)
if is_gui {
tracing::trace_log_message("not cli exe. Force new window");
req.sudo_mode = SudoMode::ForceNewWindow;
}
}
pub fn run_target(
copy_env: bool,
args: &[&String],
sudo_mode: SudoMode,
requested_dir: Option<String>,
) -> Result<i32> {
let manually_requested_dir = requested_dir.is_some();
let req = prepare_request(copy_env, args, sudo_mode, requested_dir)?;
do_request(req, copy_env, manually_requested_dir)
}
/// Constructs an ElevateRequest from the given arguments. We'll package up
/// handles, we'll separate out the application and args, and we'll do some
/// other work to make sure the request is ready to go.
///
/// This also includes getting the absolute path to the requested application
/// (which involves hitting up the file system). If the target app is a GUI app,
/// we'll convert the request to run in a new window.
///
/// If the app isn't actually an app, and it's instead a CMD intrinsic, we'll
/// convert the request to run in CMD (if we were _ourselves_ ran from CMD).
fn prepare_request(
copy_env: bool,
args: &[&String],
sudo_mode: SudoMode,
requested_dir: Option<String>,
) -> Result<ElevateRequest> {
let handle_indices = [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE];
// Get our stdin and stdout handles
let handles = handle_indices.map(|idx| unsafe { GetStdHandle(idx).ok().unwrap_or_default() });
// Is stdin or stdout a console?
let is_console = handles.map(|h| unsafe {
let mut mode = CONSOLE_MODE::default();
GetConsoleMode(h, &mut mode).is_ok()
});
// Pass invalid handles if the handle is a console handle. If you don't,
// then RPC will explode trying to duplicate the console handle to the
// elevated process (because a console isn't a "pipe", but a file is)
let mut filtered_handles: [HANDLE; 3] = Default::default();
for i in 0..3 {
if !is_console[i] {
filtered_handles[i] = handles[i];
}
}
// If they passed a directory, use that. Otherwise, "use the current dir"
// (with the known caveats about new window mode)
let actual_dir = match requested_dir {
Some(dir) => {
// If they passed a directory, we need to canonicalize it. This is
// because the elevated sudo will start in system32 (because we are
// ourselves, an exe that's in the Windows directory). This will
// make sure the elevated sudo gets the real path they asked for.
//
// DON'T use std::canonicalize though. That'll give us a UNC path
// and just about nothing actaully accepts those (CMD.EXE included)
absolute_path(Path::new(&dir))?
}
None => env::current_dir()?,
}
.to_string_lossy()
.into_owned();
// Build our request
let mut req = ElevateRequest {
parent_pid: std::process::id(),
handles: filtered_handles,
sudo_mode,
application: args[0].clone(),
args: args.iter().skip(1).map(|arg| arg.to_string()).collect(),
target_dir: actual_dir,
env_vars: copy_env.then(env_as_string).unwrap_or_default(),
event_id: GUID::new().unwrap(),
};
tracing::trace_run(&req, !is_console[0], !is_console[1]);
event_log_request(true, &req);
// Does the application exist somewhere on the path?
let where_result = which::which(&req.application);
if let Ok(path) = where_result {
// It's a real file that exists on the PATH.
// Replace the request's application with the full path to the exe. This
// ensures that the elevated sudo will execute the same thing that was
// found here in the unelevated context.
req.application = absolute_path(&path)?.to_string_lossy().to_string();
adjust_args_for_gui_exes(&mut req);
} else {
tracing::trace_command_not_found(&req.application);
// Maybe, it's a CMD intrinsic. If it is, we'll need to adjust the args
// to make a new CMD to run the command
//
// This will return
// * an error if we couldn't get at our parent PID (very unexpected) or
// open the parent handle
// * Ok(false):
// - if the parent was created after us, so we can't tell if it's CMD or not
// - if the parent wasn't CMD or it wasn't an intrinsic
// * Ok(true): The parent was CMD, it was an intrinsic, and the args
// were adjusted to account for this.
if !adjust_args_for_intrinsics_and_cmdlets(&mut req)? {
return Err(E_DIR_BAD_COMMAND_OR_FILE.into());
}
}
Ok(req)
}
fn do_request(req: ElevateRequest, copy_env: bool, manually_requested_dir: bool) -> Result<i32> {
// Are we already running as admin? If we are, we don't need to do a whole
// bunch of ShellExecute. We can just spawn the target exe.]
let is_admin = is_running_elevated()?;
if current_elevation_matches_request(is_admin, &req) {
// println!("We're already running as admin. Just run the command.");
let mut child = spawn_target_for_request(&req)?;
match child.wait() {
Ok(status) => Ok(status.code().unwrap_or_default()),
Err(err) => Err(err.into()),
}
} else {
// We're not running elevated here. We need to start the
// elevated sudo and send it our request to handle.
// In ForceNewWindow mode, we want to use ShellExecuteEx to create the
// target process, whenever possible. This has the benefit of having the
// UAC display the target app directly, and also avoiding any RPC calls
// at all.
//
// However, there are caveats which prevent us from using ShellExecuteEx
// in all cases:
// * We can't use ShellExecuteEx if we need to copy the environment,
// because ShellExecuteEx doesn't allow us to set the environment of
// the target process. So if they want environment variables copied,
// we need to use RPC.
// * ShellExecuteEx will always set the CWD to system32, if the target
// exe is in the Windows dir. It does this _deep_ in the OS and
// there's nothing we can do to avoid it. So, if the user has
// requested a CWD, we need to use RPC.
// - Theoretically, we only need to use RPC if the target app is in
// the Windows dir, but we'd need to recreate the internal logic of
// CreateProcess to resolve the commandline we've been given here
// to determine that.
let should_use_runas =
req.sudo_mode == SudoMode::ForceNewWindow && !copy_env && !manually_requested_dir;
if should_use_runas {
tracing::trace_log_message("Direct ShellExecute");
runas_admin(&req.application, &join_args(&req.args), SW_NORMAL)?;
Ok(0)
} else {
tracing::trace_log_message("starting RPC handoff");
handoff_to_elevated(&req)
}
}
}
fn handoff_to_elevated(req: &ElevateRequest) -> Result<i32> {
// Build a single string from the request's application and args
let parent_pid = std::process::id();
tracing::trace_log_message(&format!(
"running as user: '{}'",
get_current_user().as_ref().unwrap_or(h!("unknown"))
));
let path = env::current_exe().unwrap();
let target_args = format!(
"elevate -p {parent_pid} {} {}",
req.application,
join_args(&req.args)
);
tracing::trace_log_message(&format!("elevate request: '{target_args:?}'"));
runas_admin(&path, &target_args, SW_HIDE)?;
// Subtle: Add our own CtrlC handler, so that we can ignore it.
// Otherwise, the console gets into a weird state, where we return
// control to the parent shell, but the child process is still
// running. Two clients in the same console is always weird.
unsafe {
_ = SetConsoleCtrlHandler(Some(ignore_ctrl_c), true);
}
send_request_via_rpc(req)
}
/// Connects to the elevated sudo instance via RPC, then makes a couple RPC
/// calls to send the request to the elevated sudo.
///
/// In the case of success, this might not return until our target process
/// actually exits.
///
/// This will return an error if we can't connect to the RPC server. However,
/// we'll return Ok regardless if the RPC call itself succeeded or not. The Ok()
/// value will be:
/// - 0 if the _target_ process exited successfully
/// - Anything else to indicate either an error in the RPC call, or the target
/// process exited with an error.
/// - Specifically be on the lookout for 1764 here, which is
/// RPC_S_CANNOT_SUPPORT
fn send_request_via_rpc(req: &ElevateRequest) -> Result<i32> {
let endpoint = generate_rpc_endpoint_name(unsafe { GetCurrentProcessId() });
let endpoint = CString::new(endpoint).unwrap();
// Attempt to connect to our RPC server, with a backoff. This will try 10
// times, backing off by 100ms each time (a total of 5 seconds)
let mut tries = 0;
loop {
if tries > 10 {
return Err(ERROR_TIMEOUT.into());
}
// Casting this name to a *const u8 is a little unsafe, but our
// endpoint names aren't gonna be running abreast of weird encoding
// edge cases, and RpcStringBindingCompose ultimately wants an
// unsigned char string.
let connect_result = rpc_client_setup(&endpoint);
match connect_result {
RPC_STATUS(0) => break,
RPC_S_NOT_LISTENING => {
tries += 1;
std::thread::sleep(std::time::Duration::from_millis(100 * tries))
}
_ => std::process::exit(connect_result.0),
}
}
// The GetCurrentProcess() is not a "real" handle and unsuitable to be used with COM.
// -> We need to clone it first.
let h_real = unsafe {
let mut process = OwnedHandle::default();
let current_process = GetCurrentProcess();
DuplicateHandle(
current_process,
current_process,
current_process,
&mut *process,
0,
true,
DUPLICATE_SAME_ACCESS,
)?;
process
};
tracing::trace_log_message(&format!("sending i/o/e handles: {:?}", req.handles));
let mut child_handle = OwnedHandle::default();
let rpc_elevate = rpc_client_do_elevation_request(
*h_real,
&req.handles,
req.sudo_mode,
Utf8Str::new(&req.application),
Utf8Str::new(&pack_string_list_for_rpc(&req.args)),
Utf8Str::new(&req.target_dir),
Utf8Str::new(&req.env_vars),
req.event_id,
&mut *child_handle,
);
tracing::trace_log_message(&format!("RequestElevation result {rpc_elevate:?}"));
// Clean up (terminate) the RPC server we made.
rpc_client_cleanup();
rpc_elevate.ok()?;
// Assert that handle_elevation_request() properly limited the handle access rights to just the bits that we needed.
if cfg!(debug_assertions) {
unsafe {
let mut info: PUBLIC_OBJECT_BASIC_INFORMATION = Default::default();
NtQueryObject(
*child_handle,
ObjectBasicInformation,
Some(&mut info as *mut _ as _),
std::mem::size_of_val(&info) as _,
None,
)
.ok()?;
let expected =
PROCESS_DUP_HANDLE | PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE;
debug_assert!(info.GrantedAccess == expected.0);
}
}
// If we were in new window mode, and we're here, then we're
// ShellExecuting sudo.exe, and then using the elevated sudo to create a
// new console window. In that case, we want to print an error message
// here - the elevated sudo will have exited as soon as the child is
// launched.
if req.sudo_mode == SudoMode::ForceNewWindow {
let translated_msg = r::IDS_LAUNCHEDNEWWINDOW.get();
let replaced = translated_msg.replace("{0}", &req.application);
println!("{}", replaced);
Ok(0)
} else {
unsafe {
let mut status = 0u32;
_ = WaitForSingleObject(*child_handle, INFINITE);
GetExitCodeProcess(*child_handle, &mut status)?;
Ok(status as _)
}
}
}
fn runas_admin<Exe, Args>(exe: &Exe, args: &Args, show: SHOW_WINDOW_CMD) -> Result<()>
where
Exe: AsRef<OsStr> + ?Sized,
Args: AsRef<OsStr> + ?Sized,
{
runas_admin_impl(exe.as_ref(), args.as_ref(), show)
}
fn runas_admin_impl(exe: &OsStr, args: &OsStr, show: SHOW_WINDOW_CMD) -> Result<()> {
let cwd = env::current_dir()?;
let h_exe = HSTRING::from(exe);
let h_commandline = HSTRING::from(args);
let h_cwd = HSTRING::from(cwd.as_os_str());
let mut sei = SHELLEXECUTEINFOW {
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as u32,
fMask: SEE_MASK_NOCLOSEPROCESS,
lpVerb: w!("runas"),
lpFile: PCWSTR(h_exe.as_ptr()),
lpParameters: PCWSTR(h_commandline.as_ptr()),
lpDirectory: PCWSTR(h_cwd.as_ptr()),
nShow: show.0,
..Default::default()
};
unsafe { ShellExecuteExW(&mut sei) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cmd_is_cui() {
let app_name = "cmd".to_string();
let req = prepare_request(false, &[&app_name], SudoMode::Normal, None).unwrap();
assert_eq!(req.sudo_mode, SudoMode::Normal);
}
#[test]
fn test_notepad_is_gui() {
let req =
prepare_request(false, &[&("notepad".to_string())], SudoMode::Normal, None).unwrap();
// If we did in fact find notepad, then we should have set the mode to
// ForceNewWindow, since it's a GUI app.
assert_eq!(req.sudo_mode, SudoMode::ForceNewWindow);
// I found in the past that `notepad.exe` worked, while `notepad`
// didn't. Just make sure they both do, for sanity's sake.
let req_exe = prepare_request(
false,
&[&("notepad.exe".to_string())],
SudoMode::Normal,
None,
)
.unwrap();
assert_eq!(req_exe.sudo_mode, SudoMode::ForceNewWindow);
}
}

26
sudo/src/tests/mod.rs Normal file
View File

@ -0,0 +1,26 @@
#[cfg(test)]
mod tests {
use crate::helpers::*;
use windows::Win32::Foundation::*;
#[test]
fn test_try_from_u32_for_sudo_mode() {
assert_eq!(SudoMode::try_from(0), Ok(SudoMode::Disabled));
assert_eq!(SudoMode::try_from(1), Ok(SudoMode::ForceNewWindow));
assert_eq!(SudoMode::try_from(2), Ok(SudoMode::DisableInput));
assert_eq!(SudoMode::try_from(3), Ok(SudoMode::Normal));
assert_eq!(SudoMode::try_from(4), Err(ERROR_INVALID_PARAMETER.into()));
}
#[test]
fn test_try_sudo_mode_to_u32() {
assert_eq!(u32::from(SudoMode::Disabled), 0);
assert_eq!(u32::from(SudoMode::ForceNewWindow), 1);
assert_eq!(u32::from(SudoMode::DisableInput), 2);
assert_eq!(u32::from(SudoMode::Normal), 3);
}
#[test]
fn test_generate_rpc_endpoint_name() {
assert_eq!(generate_rpc_endpoint_name(1234), r"sudo_elevate_1234");
}
}

52
sudo/src/tracing.rs Normal file
View File

@ -0,0 +1,52 @@
use sudo_events::SudoEvents;
// tl:{6ffdd42d-46d9-5efe-68a1-3b18cb73a607}
static SUDO_EVENTS: std::sync::OnceLock<SudoEvents> = std::sync::OnceLock::new();
const PDT_PRODUCT_AND_SERVICE_PERFORMANCE: u64 = 0x0000000001000000;
// const PDT_PRODUCT_AND_SERVICE_USAGE: u64 = 0x0000000002000000;
pub fn sudo_events() -> &'static SudoEvents {
SUDO_EVENTS.get_or_init(SudoEvents::new)
}
use crate::messages::*;
pub fn enable_tracing() {
sudo_events();
}
pub fn trace_log_message(message: &str) {
sudo_events().message(None, message);
}
pub fn trace_command_not_found(exe_name: &str) {
sudo_events().command_not_found(None, exe_name);
}
pub fn trace_cmd_builtin_found(exe_name: &str) {
sudo_events().cmd_builtin_found(None, exe_name);
}
pub fn trace_run(req: &ElevateRequest, redirected_input: bool, redirected_output: bool) {
sudo_events().run(
None,
&req.application,
req.sudo_mode as u32,
req.parent_pid,
redirected_input,
redirected_output,
);
}
pub fn trace_modes(requested_mode: u32, allowed_mode: u32, policy_mode: u32) {
// We manually set the privacy tag to PDT_PRODUCT_AND_SERVICE_PERFORMANCE so
// that callers don't need to know that
sudo_events().modes(
None,
requested_mode,
allowed_mode,
policy_mode,
PDT_PRODUCT_AND_SERVICE_PERFORMANCE,
);
}

9
sudo/sudo.manifest Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application>
<windowsSettings>
<!-- <consoleAllocationPolicy xmlns="http://schemas.microsoft.com/SMI/2024/WindowsSettings">detached</consoleAllocationPolicy> -->
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>

24
sudo/sudo.rc Normal file
View File

@ -0,0 +1,24 @@
// Resource file (.rc) template for sudo.
//
// Our string resources are all auto-generated from our resw file. They'll be
// consumed from a generated .rc file, which will start with the content of this file.
#include <windows.h>
// Here, you'd usually want to
//
// #include "resource.h"
//
// To include your resource ID's. That doesn't work for us!
// We want to use the winres crate to auto-generate FILEVERSION et. al. If we
// want that to work, then we need to append the content of our generated file
// to the content winres generates (using append_rc_content).
// However, when we do it that way, the ultimate .rc file we end up using is one
// that's generated in our OUT_DIR, and that means it can't find the relative
// resource.h
//
// Instead, we'll use append_rc_content to _also_ include the header literally
// in the output .rc file.
// Make sure to declare our exe icon here
IDI_APPICON ICON "..\\img\\ico\\sudo.ico"

8
sudo_events/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "sudo_events"
version = "0.1.0"
edition = "2021"
[dependencies]
win_etw_macros.workspace = true
win_etw_provider.workspace = true

31
sudo_events/build.rs Normal file
View File

@ -0,0 +1,31 @@
use std::{io, path::Path};
// BODGY
//
// * As a part of the build process, we need to replace the fake GUID in our
// tracing lib with the real one.
// * This build script here will take the value out of the env var
// MAGIC_TRACING_GUID, and replace the fake GUID in the events_template.rs
// file with that one.
// * We'll write that file out to %OUT_DIR%/mangled_events.rs, and then include
// _that mangled file_ in our lib.rs file.
fn main() -> io::Result<()> {
let input = std::fs::read_to_string("src/events_template.rs")?;
// Is the MAGIC_TRACING_GUID env var set? If it is...
let output = match std::env::var("MAGIC_TRACING_GUID") {
Ok(guid) => {
println!("MAGIC_TRACING_GUID: {}", guid);
// Replace the fake guid (ffffffff-ffff-ffff-ffff-ffffffffffff) with this one.
input.replace("ffffffff-ffff-ffff-ffff-ffffffffffff", &guid)
}
Err(_) => input,
};
let path = Path::new(&std::env::var("OUT_DIR").unwrap()).join("mangled_events.rs");
println!(
"cargo:rerun-if-changed={}",
path.as_path().to_str().unwrap()
);
std::fs::write(path.as_path(), output)
}

View File

@ -0,0 +1,47 @@
use win_etw_macros::trace_logging_provider;
// Note: Generate GUID using TlgGuid.exe tool
#[trace_logging_provider(
name = "Microsoft.Windows.Sudo",
guid = "6ffdd42d-46d9-5efe-68a1-3b18cb73a607",
provider_group_guid = "ffffffff-ffff-ffff-ffff-ffffffffffff"
)]
// tl:{6ffdd42d-46d9-5efe-68a1-3b18cb73a607}
pub trait SudoEvents {
fn command_not_found(exe_name: &str);
fn cmd_builtin_found(exe_name: &str);
fn message(message: &str);
fn run(
exe_name: &str,
requested_mode: u32,
parent_pid: u32,
redirected_input: bool,
redirected_output: bool,
);
// TRACELOGGING EVENTS:
//
// These events need to add a PartA_PrivTags: u64 parameter to the end of
// the event. That should be filled with PDT_ProductAndServicePerformance or
// PDT_ProductAndServiceUsage. Our wrappers in tracing.rs should abstract
// that away.
//
// Additionally, we manually set the keyword to MICROSOFT_KEYWORD_MEASURES.
// However, we can't use that constant here, because the macro needs an
// actual _literal_. So, we use the value 0x0000400000000000 directly.
// MICROSOFT_KEYWORD_TELEMETRY is 0x0000200000000000, but that... doesn't work?
// requested_mode:
// * 0: Use the allowed mode from the registry / policy
// * 1: Manually request forceNewWindow
// * 2: Manually request disableInput
// * 3: ??? We shouldn't get these. Requested mode is set by the CLI flags
// allowed_mode: Straightforward. The mode in the registry
// policy_mode: The mode set by the policy. If the policy isn't set, this should be 0xffffffff
#[event(keyword = 0x0000400000000000)]
fn modes(requested_mode: u32, allowed_mode: u32, policy_mode: u32, PartA_PrivTags: u64);
}

1
sudo_events/src/lib.rs Normal file
View File

@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/mangled_events.rs"));

153
tools/gen-lang-codes.ps1 Normal file
View File

@ -0,0 +1,153 @@
# This is a list of what I think all the languages we need to support are. At
# the very least, it's all the languages that the Terminal's context menu are
# localized into. If we need more than that, go ahead and add more. This has to
# be the most complete list - the script that actually generates the .rc file
# will use whatever subest of languages are actually available.
$languageCodes = @(
"af-ZA",
"am-ET",
"ar-SA",
"as-IN",
"az-Latn-AZ",
"bg-BG",
"bn-IN",
"bs-Latn-BA",
"ca-ES",
"ca-Es-VALENCIA",
"cs-CZ",
"cy-GB",
"da-DK",
"de-DE",
"el-GR",
"en-GB",
"en-US",
"es-ES",
"es-MX",
"et-EE",
"eu-ES",
"fa-IR",
"fi-FI",
"fil-PH",
"fr-CA",
"fr-FR",
"ga-IE",
"gd-gb",
"gl-ES",
"gu-IN",
"he-IL",
"hi-IN",
"hr-HR",
"hu-HU",
"hy-AM",
"id-ID",
"is-IS",
"it-IT",
"ja-JP",
"ka-GE",
"kk-KZ",
"km-KH",
"kn-IN",
"ko-KR",
"kok-IN",
"lb-LU",
"lo-LA",
"lt-LT",
"lv-LV",
"mi-NZ",
"mk-MK",
"ml-IN",
"mr-IN",
"ms-MY",
"mt-MT",
"nb-NO",
"ne-NP",
"nl-NL",
"nn-NO",
"or-IN",
"pa-IN",
"pl-PL",
"pt-BR",
"pt-PT",
"qps-ploc",
"qps-ploca",
"qps-plocm",
"quz-PE",
"ro-RO",
"ru-RU",
"sk-SK",
"sl-SI",
"sq-AL",
"sr-Cyrl-BA",
"sr-Cyrl-RS",
"sr-Latn-RS",
"sv-SE",
"ta-IN",
"te-IN",
"th-TH",
"tr-TR",
"tt-RU",
"ug-CN",
"uk-UA",
"ur-PK",
"uz-Latn-UZ",
"vi-VN",
"zh-CN",
"zh-TW"
)
function Get-Language-Sublanguage-Constants {
param (
[int]$lcid
)
$language = $lcid -band 0xFFFF
$sublanguage = ($lcid -shr 16) -band 0x3F
# Language Constants
$languageConstant = "LANG_" + ([System.Globalization.CultureInfo]::GetCultureInfo($lcid).TwoLetterISOLanguageName).ToUpper()
# Sublanguage Constants
$sublanguageConstant = "SUBLANG_" + ([System.Globalization.CultureInfo]::GetCultureInfo($lcid).Name.Split('-')[1]).ToUpper()
return $language, $sublanguage, $languageConstant, $sublanguageConstant, ([System.Globalization.CultureInfo]::GetCultureInfo($lcid).ThreeLetterISOLanguageName).ToUpper()
}
$languageHashTable = @{}
foreach ($code in $languageCodes) {
$cultureInfo = New-Object System.Globalization.CultureInfo $code
$displayName = $cultureInfo.DisplayName
$neutralCulture = $cultureInfo.Parent.Name
$lcid = $cultureInfo.LCID
$language, $sublanguage, $languageConstant, $sublanguageConstant, $threeLetter = Get-Language-Sublanguage-Constants -lcid $lcid
# trim out "LANG_" and "SUBLANG_
$languageConstant = $languageConstant.Substring(5)
$sublanguageConstant = $sublanguageConstant.Substring(8)
$hashTableValue = @(
# $cultureInfo.Name.ToUpper(),
# "",
$threeLetter,
$language,
$sublanguage, # $cultureInfo.Name.ToUpper() + '_' + $cultureInfo.Parent.Name.ToUpper(),
$displayName
)
$languageHashTable[$code] = $hashTableValue
}
# write list like:
#
# "eu-ES" = @("EUQ", "BASQUE", "DEFAULT", "Basque (Basque)");
# $languageHashTable | % {
foreach ($code in $languageCodes) {
$lang = $languageHashTable[$code]
# Get language and sublanguage constants
write-host " `"$($code)`" = @(`"$($lang[0])`", `"$($lang[1])`", `"$($lang[2])`", `"$($lang[3])`");"
# if ($_.Value -ne $null ) {
# write-host " `"$($_.Key)`" = @(`"$($_.Value[0])`", `"$($_.Value[1])`", `"$($_.Value[2])`", `"$($_.Value[3])`");" }
}

26
tools/sudo.tvpp Normal file
View File

@ -0,0 +1,26 @@
<TVPP_Application_Settings>
<Configurations>
<Mode>LocalLive</Mode>
<SessionName>sudo for windows</SessionName>
<Items>
<Item>tl:{6ffdd42d-46d9-5efe-68a1-3b18cb73a607}</Item>
</Items>
<ProviderConfigs>
<ProviderConfig>
<Guid>6ffdd42d-46d9-5efe-68a1-3b18cb73a607</Guid>
<Level>0</Level>
<Flag>0</Flag>
</ProviderConfig>
</ProviderConfigs>
<ManifestPath />
</Configurations>
<ViewControlConfiguration>
<Columns>
<Column Name="Provider Name" Width="380.7" />
<Column Name="Task Name" Width="400.73" />
<Column Name="Message/PartC" Width="959.113333333333" />
<Column Name="Process ID" Width="NaN" />
<Column Name="Time" Width="NaN" />
</Columns>
</ViewControlConfiguration>
</TVPP_Application_Settings>

603
tools/tests.ipynb Normal file
View File

@ -0,0 +1,603 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Sudo Test Scenarios\n",
"\n",
"Obviously, automated tests that require going through a UAC are challenging to write as unit tests, or run in CI. This notebook instead provides a way of listing a bunch of manual tests. This is all powered by the VsCode Polyglot Notebooks extension. Make sure you have that installed, so that you can run this notebook. \n",
"\n",
"(You'll need the [Polyglot Notebooks](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.dotnet-interactive-vscode) extension for VsCode installed to run this notebook.)\n",
"\n",
"## Building\n",
"\n",
"First, start by building the code. We're going to stick the output exe into a `_sudo_` alias, to keep tests conscise. \n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"cargo build --target x86_64-pc-windows-msvc\n",
"new-alias -Force _sudo_ ..\\target\\x86_64-pc-windows-msvc\\debug\\sudo.exe"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"new-alias -Force _sudo_ ..\\target\\x86_64-pc-windows-msvc\\debug\\sudo.exe"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Tests\n",
"\n",
"### Simple sanity tests\n",
"Just start py printing the error message:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Dismiss this UAC. We should print an error message."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ cmd"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running `sudo notepad` should elevate Notepad directly without triggering a UAC prompt for `notepad`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ notepad"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running `sudo --newWindow cmd` should elevate \"Command Prompt\" directly and spawn a new conhost."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ --newWindow cmd"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running `sudo netstat -ab` should display network statistics and the process using them."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ netstat -ab"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Exiting `netstat` and `sudo` using Ctrl+C should terminate the processes. This test is going to spawn a new CMD in a new conhost window, ctrl+c in that window to verify it did work. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"conhost -- cmd.exe /k ..\\target\\x86_64-pc-windows-msvc\\debug\\sudo.exe netstat -ab"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Redirecting to pipes and files\n",
"\n",
"This command uses `dir /b` to list the contents of the directory, and `find /c /v \"\"` to count the lines (items) returned.\n",
"This should print a number greater than 0 if there are items in the directory."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ cmd /c dir /b \"C:\\Program Files\\WindowsApps\" `| find /c /v \"\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Env vars\n",
"\n",
"Running `sudo cmd`, without -E, doesn't propogate the environment variables. You should get `FooBar was %FooBar%`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"$env:FooBar = \"Hello World\"\n",
"_sudo_ cmd /c echo FooBar was '%FooBar%'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running `sudo -E ...` should pass the env vars to the child process."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"$env:FooBar = \"Hello World\"\n",
"_sudo_ -E cmd /c echo FooBar was '%FooBar%'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running `sudo -E` with no command should not crash."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ -E"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running `sudo -N -E cmd` should elevate `sudo` directly, spawn a new conhost, and retain env vars. (The output will appear here in the notebook - close the conhost to continue.)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"$env:FooBar = \"Hello new window\"\n",
"_sudo_ --new-window -E cmd /k echo FooBar was '%FooBar%'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running `..\\target\\x86_64-pc-windows-msvc\\debug\\sudo.exe cmd` from an admin prompt should silently launch CMD without triggering UAC. (you'll need to paste this into the conhost that appears manually)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"Start-Process -verb runas cmd -ArgumentList \"/k cd /d $((Get-Location).Path)\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### CMD intrinsics tests\n",
"\n",
"Running `sudo dir`, FROM CMD, should list the files in the current working directory. (Recall, we're in the `/tools` dir)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"cmd /c ..\\target\\x86_64-pc-windows-msvc\\debug\\sudo.exe dir"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Of course, the notebook is running PowerShell. So, running `sudo dir` from PowerShell should not list files in the current working directory."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ dir"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running the command `target\\x86_64-pc-windows-msvc\\debug\\sudo.exe fsutil volume allocationReport C: > out.txt`, then typing `out.txt` should display content in the text file.\n",
"\n",
"Ensure that the text file is not empty. This usually takes like 15-20 seconds to run, so patience. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"if (Test-Path -Path \"out.txt\") { Remove-Item -Path \"out.txt\" }\n",
"_sudo_ fsutil volume allocationReport C: > out.txt\n",
"(get-childItem -path out.txt).Length -gt 0\n",
"Remove-Item -Path \"out.txt\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Setting Modes\n",
"\n",
"\n",
"Running `sudo --inline cmd` should exit with an error when `sudo` is set to disable input mode."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ ..\\target\\x86_64-pc-windows-msvc\\debug\\sudo.exe config --enable disableInput\n",
"_sudo_ --inline cmd"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Same as above, but with `sudo --disable-input cmd` should exit with an error when `sudo` is set to force new window mode."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ ..\\target\\x86_64-pc-windows-msvc\\debug\\sudo.exe config --enable forceNewWindow\n",
"_sudo_ --disable-input cmd"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, back to normal mode"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ ..\\target\\x86_64-pc-windows-msvc\\debug\\sudo.exe config --enable normal"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Working directories\n",
"\n",
"Running `sudo -N cmd` in any path should start `cmd` in `C:\\windows\\system32`. (this will open a conhost in a new window)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ -N cmd"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running `sudo -D . -N cmd` in any path should start in the original path. (the `/tools` directory)\n",
"\n",
"(again, output will actually appear in the notebook, close the conhost to continue)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ -D . -N cmd"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This next one is a bit wacky. Search paths are... complicated. \n",
"\n",
"We're going to copy our built binary into system32 (under a different name, so it doesn't kill your existing sudo). \n",
"Then, we're going to copy `cmd` into a different path, one that's relative to our CWD. \n",
"\n",
"Then, we're going to run `sudo2 ..\\cmd.exe`. This should run the `cmd` from the relative path, not the one in system32. We can verify this by looking at the conhost that's spawned. It should display an error at the start like:\n",
"\n",
"```\n",
"The system cannot find message text for message number 0x2350 in the message file for Application.\n",
"\n",
"(c) Microsoft Corporation. All rights reserved.\n",
"Not enough memory resources are available to process this command.\n",
"```\n",
"\n",
"There will be two UACs to accept. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"dotnet_interactive": {
"language": "pwsh"
},
"polyglot_notebook": {
"kernelName": "pwsh"
}
},
"outputs": [],
"source": [
"_sudo_ cmd /c copy ..\\target\\x86_64-pc-windows-msvc\\debug\\sudo.exe C:\\Windows\\System32\\sudo2.exe /y\n",
"copy-item c:\\windows\\system32\\cmd.exe -destination .. -force\n",
"c:\\windows\\system32\\sudo2.exe -N ..\\cmd.exe\n",
"rm ..\\cmd.exe"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".NET (PowerShell)",
"language": "PowerShell",
"name": ".net-pwsh"
},
"language_info": {
"name": "polyglot-notebook"
},
"polyglot_notebook": {
"kernelInfo": {
"defaultKernelName": "pwsh",
"items": [
{
"aliases": [],
"languageName": "pwsh",
"name": "pwsh"
}
]
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@ -0,0 +1,4 @@
[package]
name = "win32resources"
version = "0.1.0"
edition = "2021"

79
win32resources/src/lib.rs Normal file
View File

@ -0,0 +1,79 @@
//! Provides APIs for accessing Win32 resources, mainly string resources.
use std::borrow::Cow;
use std::ffi::c_void;
use std::ops::Deref;
use std::ptr::null_mut;
use std::slice::from_raw_parts;
use std::sync::OnceLock;
#[allow(clippy::upper_case_acronyms)]
type HINSTANCE = *const c_void;
extern "system" {
fn LoadStringW(hInstance: HINSTANCE, uID: u32, lpBuffer: *mut u16, cchBufferMax: i32) -> i32;
}
extern "C" {
static __ImageBase: [u8; 0];
}
fn module_base() -> *const c_void {
unsafe { (&__ImageBase) as *const [u8; 0] as *const c_void }
}
/*
#[macro_export]
macro_rules! def_image_base {
(
$fn_name:ident
) => {
fn $fn_name() -> &'static ModuleAddress {
extern "C" {
static _ImageBase: ();
}
(&_ImageBase) as *const () as *const core::ffi::c_void
}
}
}
*/
pub struct StaticStringResource {
id: u32,
value: OnceLock<Cow<'static, str>>,
fallback: &'static str,
}
impl StaticStringResource {
pub const fn new(id: u32, fallback: &'static str) -> Self {
Self {
id,
value: OnceLock::new(),
fallback,
}
}
pub fn get(&self) -> &str {
self.value.get_or_init(|| {
let image_base = module_base();
// If this returns 0, then the string was not found.
let mut base: *const u16 = null_mut();
let len = unsafe { LoadStringW(image_base, self.id, &mut base as *mut _ as *mut _, 0) };
if len <= 0 {
return Cow::Borrowed(self.fallback);
}
Cow::Owned(String::from_utf16_lossy(unsafe {
from_raw_parts(base, len as usize)
}))
})
}
}
impl Deref for StaticStringResource {
type Target = str;
fn deref(&self) -> &Self::Target {
self.get()
}
}