Initial release of the sudo source (#71)
Initial release of the sudo source --------- Co-authored-by: Mike Griese <zadjii@gmail.com>
|
@ -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"]
|
|
@ -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"
|
|
@ -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
|
|
@ -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'
|
|
@ -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
|
|
@ -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'
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
]
|
|
@ -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"
|
|
@ -0,0 +1,7 @@
|
|||
#define NOMINMAX
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
#include <evntprov.h>
|
||||
|
||||
#include "instrumentation.h" // Generated from manifest
|
|
@ -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
|
||||
```
|
|
@ -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>
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
[implicit_handle(handle_t sudo_rpc_IfHandle)]
|
||||
interface sudo_rpc
|
||||
{
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 9.7 KiB |
|
@ -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.
|
|
@ -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
|
|
@ -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"]
|
||||
|
|
@ -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>
|
|
@ -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.
|
|
@ -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)
|
||||
}
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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");
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)"));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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>
|
|
@ -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"
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
include!(concat!(env!("OUT_DIR"), "/mangled_events.rs"));
|
|
@ -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])`");" }
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
[package]
|
||||
name = "win32resources"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
|
@ -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()
|
||||
}
|
||||
}
|