chore: initialize qiming workspace repository
This commit is contained in:
2
qimingclaw/crates/windows-sandbox-helper/.gitignore
vendored
Normal file
2
qimingclaw/crates/windows-sandbox-helper/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target/
|
||||
.cargo/
|
||||
809
qimingclaw/crates/windows-sandbox-helper/Cargo.lock
generated
Normal file
809
qimingclaw/crates/windows-sandbox-helper/Cargo.lock
generated
Normal file
@@ -0,0 +1,809 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[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-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sandbox-helper"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"dirs-next",
|
||||
"dunce",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[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-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
49
qimingclaw/crates/windows-sandbox-helper/Cargo.toml
Normal file
49
qimingclaw/crates/windows-sandbox-helper/Cargo.toml
Normal file
@@ -0,0 +1,49 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "windows-sandbox-helper"
|
||||
version = "0.1.0"
|
||||
description = "Windows Restricted Token sandbox helper binary"
|
||||
repository = "https://github.com/qiming/qiming-agent"
|
||||
rust-version = "1.75"
|
||||
|
||||
[[bin]]
|
||||
name = "qiming-sandbox-helper"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
rand = { version = "0.8", default-features = false, features = ["std", "small_rng"] }
|
||||
dirs-next = "2.0"
|
||||
dunce = "1.0"
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
|
||||
[dependencies.windows-sys]
|
||||
version = "0.52"
|
||||
features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Diagnostics_Debug",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Authorization",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_JobObjects",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Environment",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Kernel",
|
||||
"Win32_System_Console",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Com",
|
||||
"Win32_Security_Authentication_Identity",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
259
qimingclaw/crates/windows-sandbox-helper/src/acl.rs
Normal file
259
qimingclaw/crates/windows-sandbox-helper/src/acl.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
//! DACL manipulation (add/deny ACEs).
|
||||
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::Result;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use windows_sys::Win32::Foundation::{
|
||||
CloseHandle, LocalFree, ERROR_SUCCESS, HLOCAL, INVALID_HANDLE_VALUE,
|
||||
};
|
||||
use windows_sys::Win32::Security::{
|
||||
AclSizeInformation, EqualSid, GetAce, GetAclInformation,
|
||||
Authorization::{
|
||||
GetNamedSecurityInfoW, GetSecurityInfo, SetEntriesInAclW, SetNamedSecurityInfoW,
|
||||
SetSecurityInfo, EXPLICIT_ACCESS_W, TRUSTEE_IS_SID, TRUSTEE_IS_UNKNOWN, TRUSTEE_W,
|
||||
},
|
||||
ACL, ACL_SIZE_INFORMATION, ACCESS_ALLOWED_ACE, ACE_HEADER, DACL_SECURITY_INFORMATION,
|
||||
};
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ,
|
||||
FILE_GENERIC_WRITE, FILE_APPEND_DATA, FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA,
|
||||
FILE_WRITE_EA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
|
||||
};
|
||||
|
||||
const SE_KERNEL_OBJECT: u32 = 6;
|
||||
const INHERIT_ONLY_ACE: u8 = 0x08;
|
||||
const GENERIC_WRITE_MASK: u32 = 0x4000_0000;
|
||||
const CONTAINER_INHERIT_ACE: u32 = 0x2;
|
||||
const OBJECT_INHERIT_ACE: u32 = 0x1;
|
||||
|
||||
// ACE access modes
|
||||
const GRANT_ACCESS: i32 = 2;
|
||||
const DENY_ACCESS: i32 = 3;
|
||||
const REVOKE_ACCESS: i32 = 4;
|
||||
|
||||
const DENY_WRITE_MASK: u32 = FILE_GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA
|
||||
| FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES | GENERIC_WRITE_MASK;
|
||||
|
||||
/// Check if a DACL contains an ACE of the given type matching the SID with the specified mask.
|
||||
fn dacl_has_ace_for_sid(
|
||||
p_dacl: *mut ACL,
|
||||
psid: *mut c_void,
|
||||
ace_type: u8, // 0 = ACCESS_ALLOWED, 1 = ACCESS_DENIED
|
||||
mask_check: u32,
|
||||
) -> bool {
|
||||
if p_dacl.is_null() {
|
||||
return false;
|
||||
}
|
||||
let mut info: ACL_SIZE_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
let ok = unsafe {
|
||||
GetAclInformation(
|
||||
p_dacl as *const ACL,
|
||||
&mut info as *mut _ as *mut c_void,
|
||||
std::mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
|
||||
AclSizeInformation,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
return false;
|
||||
}
|
||||
for i in 0..info.AceCount {
|
||||
let mut p_ace: *mut c_void = std::ptr::null_mut();
|
||||
if unsafe { GetAce(p_dacl as *const ACL, i, &mut p_ace) } == 0 {
|
||||
continue;
|
||||
}
|
||||
let hdr = unsafe { &*(p_ace as *const ACE_HEADER) };
|
||||
if hdr.AceType != ace_type {
|
||||
continue;
|
||||
}
|
||||
if (hdr.AceFlags & INHERIT_ONLY_ACE) != 0 {
|
||||
continue;
|
||||
}
|
||||
let ace = unsafe { &*(p_ace as *const ACCESS_ALLOWED_ACE) };
|
||||
let base = p_ace as usize;
|
||||
let sid_ptr = (base + std::mem::size_of::<ACE_HEADER>() + std::mem::size_of::<u32>()) as *mut c_void;
|
||||
if unsafe { EqualSid(sid_ptr, psid) } != 0 && (ace.Mask & mask_check) != 0 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn trustee(psid: *mut c_void) -> TRUSTEE_W {
|
||||
TRUSTEE_W {
|
||||
pMultipleTrustee: std::ptr::null_mut(),
|
||||
MultipleTrusteeOperation: 0,
|
||||
TrusteeForm: TRUSTEE_IS_SID,
|
||||
TrusteeType: TRUSTEE_IS_UNKNOWN,
|
||||
ptstrName: psid as *mut u16,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_dacl(path: &Path) -> Result<(*mut c_void, *mut ACL)> {
|
||||
let mut p_sd: *mut c_void = std::ptr::null_mut();
|
||||
let mut p_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let code = unsafe {
|
||||
GetNamedSecurityInfoW(
|
||||
to_wide(path).as_ptr(),
|
||||
1,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&mut p_dacl,
|
||||
std::ptr::null_mut(),
|
||||
&mut p_sd,
|
||||
)
|
||||
};
|
||||
if code != ERROR_SUCCESS as u32 {
|
||||
return Err(anyhow::anyhow!("GetNamedSecurityInfoW failed: {}", code));
|
||||
}
|
||||
Ok((p_sd, p_dacl))
|
||||
}
|
||||
|
||||
fn free_sd(p_sd: *mut c_void) {
|
||||
if !p_sd.is_null() {
|
||||
unsafe { LocalFree(p_sd as HLOCAL) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal: apply an EXPLICIT_ACCESS_W entry to a path's DACL.
|
||||
unsafe fn set_ace_on_path(
|
||||
path: &Path,
|
||||
psid: *mut c_void,
|
||||
access_mode: i32,
|
||||
permissions: u32,
|
||||
skip_if_present: Option<(u8, u32)>, // (ace_type, mask) to check before adding
|
||||
) -> Result<bool> {
|
||||
let (p_sd, p_dacl) = get_dacl(path)?;
|
||||
|
||||
if let Some((ace_type, mask)) = skip_if_present {
|
||||
if dacl_has_ace_for_sid(p_dacl, psid, ace_type, mask) {
|
||||
free_sd(p_sd);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
let mut explicit: EXPLICIT_ACCESS_W = std::mem::zeroed();
|
||||
explicit.grfAccessPermissions = permissions;
|
||||
explicit.grfAccessMode = access_mode;
|
||||
explicit.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE;
|
||||
explicit.Trustee = trustee(psid);
|
||||
|
||||
let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let code2 = unsafe { SetEntriesInAclW(1, &explicit, p_dacl, &mut p_new_dacl) };
|
||||
let mut added = false;
|
||||
if code2 == ERROR_SUCCESS as u32 {
|
||||
let code3 = unsafe {
|
||||
SetNamedSecurityInfoW(
|
||||
to_wide(path).as_ptr() as *mut u16,
|
||||
1,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
p_new_dacl,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if code3 == ERROR_SUCCESS as u32 {
|
||||
added = true;
|
||||
}
|
||||
if !p_new_dacl.is_null() {
|
||||
unsafe { LocalFree(p_new_dacl as HLOCAL) };
|
||||
}
|
||||
}
|
||||
free_sd(p_sd);
|
||||
Ok(added)
|
||||
}
|
||||
|
||||
pub unsafe fn add_allow_ace(path: &Path, psid: *mut c_void) -> Result<bool> {
|
||||
set_ace_on_path(
|
||||
path,
|
||||
psid,
|
||||
GRANT_ACCESS,
|
||||
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE,
|
||||
Some((0, FILE_GENERIC_WRITE)), // skip if ALLOW ACE with write exists
|
||||
)
|
||||
}
|
||||
|
||||
pub unsafe fn add_deny_write_ace(path: &Path, psid: *mut c_void) -> Result<bool> {
|
||||
set_ace_on_path(
|
||||
path,
|
||||
psid,
|
||||
DENY_ACCESS,
|
||||
DENY_WRITE_MASK,
|
||||
Some((1, DENY_WRITE_MASK)), // skip if DENY ACE with write mask exists
|
||||
)
|
||||
}
|
||||
|
||||
pub unsafe fn revoke_ace(path: &Path, psid: *mut c_void) {
|
||||
let _ = set_ace_on_path(
|
||||
path,
|
||||
psid,
|
||||
REVOKE_ACCESS,
|
||||
0,
|
||||
None, // always revoke
|
||||
);
|
||||
}
|
||||
|
||||
pub unsafe fn allow_null_device(psid: *mut c_void) {
|
||||
let desired = 0x00020000 | 0x00040000;
|
||||
let h = unsafe {
|
||||
CreateFileW(
|
||||
to_wide(r"\\.\NUL").as_ptr(),
|
||||
desired,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
std::ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
0,
|
||||
)
|
||||
};
|
||||
if h == 0 || h == INVALID_HANDLE_VALUE {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut p_sd: *mut c_void = std::ptr::null_mut();
|
||||
let mut p_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let code = unsafe {
|
||||
GetSecurityInfo(
|
||||
h,
|
||||
SE_KERNEL_OBJECT as i32,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&mut p_dacl,
|
||||
std::ptr::null_mut(),
|
||||
&mut p_sd,
|
||||
)
|
||||
};
|
||||
if code == ERROR_SUCCESS as u32 {
|
||||
let mut explicit: EXPLICIT_ACCESS_W = unsafe { std::mem::zeroed() };
|
||||
explicit.grfAccessPermissions = FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE;
|
||||
explicit.grfAccessMode = GRANT_ACCESS;
|
||||
explicit.grfInheritance = 0;
|
||||
explicit.Trustee = trustee(psid);
|
||||
|
||||
let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let code2 = unsafe { SetEntriesInAclW(1, &explicit, p_dacl, &mut p_new_dacl) };
|
||||
if code2 == ERROR_SUCCESS as u32 {
|
||||
let _ = unsafe {
|
||||
SetSecurityInfo(
|
||||
h,
|
||||
SE_KERNEL_OBJECT as i32,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
p_new_dacl,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if !p_new_dacl.is_null() {
|
||||
unsafe { LocalFree(p_new_dacl as HLOCAL) };
|
||||
}
|
||||
}
|
||||
}
|
||||
if !p_sd.is_null() {
|
||||
unsafe { LocalFree(p_sd as HLOCAL) };
|
||||
}
|
||||
unsafe { CloseHandle(h) };
|
||||
}
|
||||
97
qimingclaw/crates/windows-sandbox-helper/src/allow.rs
Normal file
97
qimingclaw/crates/windows-sandbox-helper/src/allow.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
//! Compute allow/deny path sets from a sandbox policy.
|
||||
|
||||
use crate::policy::{SandboxMode, SandboxPolicy};
|
||||
use dunce::canonicalize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct AllowDenyPaths {
|
||||
pub allow: HashSet<PathBuf>,
|
||||
pub deny: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn compute_allow_paths(
|
||||
policy: &SandboxPolicy,
|
||||
policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> AllowDenyPaths {
|
||||
let mut allow: HashSet<PathBuf> = HashSet::new();
|
||||
let mut deny: HashSet<PathBuf> = HashSet::new();
|
||||
|
||||
let mut add_allow_path = |p: PathBuf| {
|
||||
if p.exists() {
|
||||
allow.insert(p);
|
||||
}
|
||||
};
|
||||
let mut add_deny_path = |p: PathBuf| {
|
||||
if p.exists() {
|
||||
deny.insert(p);
|
||||
}
|
||||
};
|
||||
|
||||
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
let add_writable_root =
|
||||
|root: PathBuf, policy_cwd: &Path,
|
||||
add_allow: &mut dyn FnMut(PathBuf), add_deny: &mut dyn FnMut(PathBuf)| {
|
||||
let candidate = if root.is_absolute() {
|
||||
root
|
||||
} else {
|
||||
policy_cwd.join(root)
|
||||
};
|
||||
let canonical = canonicalize(&candidate).unwrap_or(candidate);
|
||||
add_allow(canonical.clone());
|
||||
|
||||
let git_dir = canonical.join(".git");
|
||||
if git_dir.is_dir() {
|
||||
add_deny(git_dir);
|
||||
}
|
||||
};
|
||||
|
||||
add_writable_root(
|
||||
command_cwd.to_path_buf(),
|
||||
policy_cwd,
|
||||
&mut add_allow_path,
|
||||
&mut add_deny_path,
|
||||
);
|
||||
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = policy {
|
||||
for root in writable_roots {
|
||||
add_writable_root(
|
||||
root.clone(),
|
||||
policy_cwd,
|
||||
&mut add_allow_path,
|
||||
&mut add_deny_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always include TEMP/TMP as writable roots
|
||||
for key in ["TEMP", "TMP"] {
|
||||
if let Some(v) = env_map.get(key) {
|
||||
add_allow_path(PathBuf::from(v));
|
||||
} else if let Ok(v) = std::env::var(key) {
|
||||
add_allow_path(PathBuf::from(v));
|
||||
}
|
||||
}
|
||||
|
||||
// Include APPDATA/LOCALAPPDATA as writable roots in compat mode (and permissive).
|
||||
// Strict mode intentionally excludes these — only workspace + TEMP/TMP are writable.
|
||||
// This is the single authority for APPDATA allowance; the TypeScript side does NOT add them.
|
||||
let mode = policy.sandbox_mode();
|
||||
if mode != &SandboxMode::Strict {
|
||||
for key in ["APPDATA", "LOCALAPPDATA"] {
|
||||
if let Some(v) = env_map.get(key) {
|
||||
add_allow_path(PathBuf::from(v));
|
||||
} else if let Ok(v) = std::env::var(key) {
|
||||
add_allow_path(PathBuf::from(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AllowDenyPaths { allow, deny }
|
||||
}
|
||||
265
qimingclaw/crates/windows-sandbox-helper/src/audit.rs
Normal file
265
qimingclaw/crates/windows-sandbox-helper/src/audit.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
//! Simplified world-writable directory audit.
|
||||
//!
|
||||
//! Unlike the macOS/Linux sandbox backends (which are declarative and need no audit),
|
||||
//! Windows still benefits from a lightweight scan of CWD children to prevent sandbox
|
||||
//! escape through world-writable paths. The aggressive global scan (PATH, C:\, Windows)
|
||||
//! has been removed to match the simpler approach of macOS/Linux.
|
||||
|
||||
use crate::acl::add_deny_write_ace;
|
||||
use crate::cap::{cap_sid_file, load_or_create_cap_sids};
|
||||
use crate::logging::log_note;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::token::{convert_string_sid_to_sid, world_sid};
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
use windows_sys::Win32::Foundation::{
|
||||
CloseHandle, ERROR_SUCCESS, HLOCAL, INVALID_HANDLE_VALUE, LocalFree,
|
||||
};
|
||||
use windows_sys::Win32::Security::{
|
||||
EqualSid, GetAce, GetAclInformation, MapGenericMask,
|
||||
Authorization::{GetNamedSecurityInfoW, GetSecurityInfo},
|
||||
DACL_SECURITY_INFORMATION, ACL, GENERIC_MAPPING,
|
||||
ACCESS_ALLOWED_ACE, ACE_HEADER,
|
||||
};
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
CreateFileW, FILE_ALL_ACCESS, FILE_APPEND_DATA, FILE_FLAG_BACKUP_SEMANTICS,
|
||||
FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
|
||||
FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
|
||||
FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, FILE_WRITE_EA, OPEN_EXISTING,
|
||||
};
|
||||
|
||||
const MAX_CWD_CHILDREN: usize = 200;
|
||||
const AUDIT_TIME_LIMIT_SECS: u64 = 1;
|
||||
|
||||
fn normalize_path_key(p: &Path) -> String {
|
||||
let n = dunce::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
|
||||
n.to_string_lossy().replace('\\', "/").to_ascii_lowercase()
|
||||
}
|
||||
|
||||
/// Check if a path has a world-writable ALLOW ACE in its DACL.
|
||||
unsafe fn path_has_world_write_allow(path: &Path) -> Result<bool> {
|
||||
let mut p_sd: *mut c_void = std::ptr::null_mut();
|
||||
let mut p_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let wpath = to_wide(path);
|
||||
|
||||
let h = CreateFileW(
|
||||
wpath.as_ptr(),
|
||||
0x00020000,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
std::ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS,
|
||||
0,
|
||||
);
|
||||
if h == INVALID_HANDLE_VALUE {
|
||||
let code = GetNamedSecurityInfoW(
|
||||
wpath.as_ptr(),
|
||||
1,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&mut p_dacl,
|
||||
std::ptr::null_mut(),
|
||||
&mut p_sd,
|
||||
);
|
||||
if code != ERROR_SUCCESS as u32 {
|
||||
if !p_sd.is_null() { LocalFree(p_sd as HLOCAL); }
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
let code = GetSecurityInfo(
|
||||
h,
|
||||
1,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&mut p_dacl,
|
||||
std::ptr::null_mut(),
|
||||
&mut p_sd,
|
||||
);
|
||||
CloseHandle(h);
|
||||
if code != ERROR_SUCCESS as u32 {
|
||||
if !p_sd.is_null() { LocalFree(p_sd as HLOCAL); }
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
let mut world = world_sid()?;
|
||||
let psid_world = world.as_mut_ptr() as *mut c_void;
|
||||
|
||||
if p_dacl.is_null() {
|
||||
if !p_sd.is_null() { LocalFree(p_sd as HLOCAL); }
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut info: windows_sys::Win32::Security::ACL_SIZE_INFORMATION = std::mem::zeroed();
|
||||
let ok = GetAclInformation(
|
||||
p_dacl as *const ACL,
|
||||
&mut info as *mut _ as *mut c_void,
|
||||
std::mem::size_of::<windows_sys::Win32::Security::ACL_SIZE_INFORMATION>() as u32,
|
||||
4, // AclSizeInformation
|
||||
);
|
||||
let has_write = if ok != 0 {
|
||||
let mapping = GENERIC_MAPPING {
|
||||
GenericRead: FILE_GENERIC_READ,
|
||||
GenericWrite: FILE_GENERIC_WRITE,
|
||||
GenericExecute: FILE_GENERIC_EXECUTE,
|
||||
GenericAll: FILE_ALL_ACCESS,
|
||||
};
|
||||
let write_mask = FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES;
|
||||
let mut found = false;
|
||||
for i in 0..(info.AceCount as usize) {
|
||||
let mut p_ace: *mut c_void = std::ptr::null_mut();
|
||||
if GetAce(p_dacl as *const ACL, i as u32, &mut p_ace) == 0 { continue; }
|
||||
let hdr = &*(p_ace as *const ACE_HEADER);
|
||||
if hdr.AceType != 0 || (hdr.AceFlags & 0x08) != 0 { continue; }
|
||||
let base = p_ace as usize;
|
||||
let sid_ptr = (base + std::mem::size_of::<ACE_HEADER>() + std::mem::size_of::<u32>()) as *mut c_void;
|
||||
if EqualSid(sid_ptr, psid_world) == 0 { continue; }
|
||||
let ace = &*(p_ace as *const ACCESS_ALLOWED_ACE);
|
||||
let mut mask = ace.Mask;
|
||||
MapGenericMask(&mut mask, &mapping);
|
||||
if (mask & write_mask) != 0 { found = true; break; }
|
||||
}
|
||||
found
|
||||
} else { false };
|
||||
|
||||
if !p_sd.is_null() { LocalFree(p_sd as HLOCAL); }
|
||||
Ok(has_write)
|
||||
}
|
||||
|
||||
/// Audit only CWD direct children for world-writable directories.
|
||||
fn audit_cwd_children(cwd: &Path, logs_base_dir: Option<&Path>) -> Result<Vec<PathBuf>> {
|
||||
let start = Instant::now();
|
||||
let mut flagged: Vec<PathBuf> = Vec::new();
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
|
||||
if let Ok(read) = std::fs::read_dir(cwd) {
|
||||
for ent in read.flatten().take(MAX_CWD_CHILDREN) {
|
||||
if start.elapsed() > Duration::from_secs(AUDIT_TIME_LIMIT_SECS) {
|
||||
break;
|
||||
}
|
||||
let ft = match ent.file_type() {
|
||||
Ok(ft) => ft,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if ft.is_symlink() || !ft.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let p = ent.path();
|
||||
if unsafe { path_has_world_write_allow(&p)? } {
|
||||
let key = normalize_path_key(&p);
|
||||
if seen.insert(key) {
|
||||
flagged.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed_ms = start.elapsed().as_millis();
|
||||
if !flagged.is_empty() {
|
||||
let mut list = String::new();
|
||||
for p in &flagged {
|
||||
list.push_str(&format!("\n - {}", p.display()));
|
||||
}
|
||||
log_note(
|
||||
&format!("AUDIT: world-writable scan found; cwd={cwd:?}; duration_ms={elapsed_ms}; flagged:{list}"),
|
||||
logs_base_dir,
|
||||
);
|
||||
} else {
|
||||
log_note(
|
||||
&format!("AUDIT: world-writable scan OK; duration_ms={elapsed_ms}"),
|
||||
logs_base_dir,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(flagged)
|
||||
}
|
||||
|
||||
pub fn apply_world_writable_scan_and_denies(
|
||||
home: &Path,
|
||||
cwd: &Path,
|
||||
_env_map: &std::collections::HashMap<String, String>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
logs_base_dir: Option<&Path>,
|
||||
) -> Result<()> {
|
||||
let flagged = audit_cwd_children(cwd, logs_base_dir)?;
|
||||
if flagged.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(err) = apply_capability_denies_for_world_writable(
|
||||
home, &flagged, sandbox_policy, cwd, logs_base_dir,
|
||||
) {
|
||||
log_note(
|
||||
&format!("AUDIT: failed to apply capability deny ACEs: {}", err),
|
||||
logs_base_dir,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_capability_denies_for_world_writable(
|
||||
home: &Path,
|
||||
flagged: &[PathBuf],
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
cwd: &Path,
|
||||
logs_base_dir: Option<&Path>,
|
||||
) -> Result<()> {
|
||||
if flagged.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
std::fs::create_dir_all(home)?;
|
||||
let cap_path = cap_sid_file(home);
|
||||
let caps = load_or_create_cap_sids(home);
|
||||
std::fs::write(&cap_path, serde_json::to_string(&caps)?)?;
|
||||
|
||||
let (active_sid, workspace_roots): (*mut c_void, Vec<PathBuf>) = match sandbox_policy {
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||
let sid = unsafe { convert_string_sid_to_sid(&caps.workspace) }
|
||||
.ok_or_else(|| anyhow::anyhow!("ConvertStringSidToSidW failed for workspace capability"))?;
|
||||
let mut roots: Vec<PathBuf> = vec![dunce::canonicalize(cwd).unwrap_or_else(|_| cwd.to_path_buf())];
|
||||
for root in writable_roots {
|
||||
let candidate = if root.is_absolute() {
|
||||
root.clone()
|
||||
} else {
|
||||
cwd.join(root)
|
||||
};
|
||||
roots.push(dunce::canonicalize(&candidate).unwrap_or(candidate));
|
||||
}
|
||||
(sid, roots)
|
||||
}
|
||||
SandboxPolicy::ReadOnly => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.readonly) }.ok_or_else(|| {
|
||||
anyhow::anyhow!("ConvertStringSidToSidW failed for readonly capability")
|
||||
})?,
|
||||
Vec::new(),
|
||||
),
|
||||
};
|
||||
|
||||
for path in flagged {
|
||||
if workspace_roots.iter().any(|root| path.starts_with(root)) {
|
||||
continue;
|
||||
}
|
||||
let res = unsafe { add_deny_write_ace(path, active_sid) };
|
||||
match res {
|
||||
Ok(true) => log_note(
|
||||
&format!("AUDIT: applied capability deny ACE to {}", path.display()),
|
||||
logs_base_dir,
|
||||
),
|
||||
Ok(false) => {}
|
||||
Err(err) => log_note(
|
||||
&format!("AUDIT: failed to apply capability deny ACE to {}: {}", path.display(), err),
|
||||
logs_base_dir,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
51
qimingclaw/crates/windows-sandbox-helper/src/cap.rs
Normal file
51
qimingclaw/crates/windows-sandbox-helper/src/cap.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! Capability SID generation and persistence.
|
||||
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::RngCore;
|
||||
use rand::SeedableRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct CapSids {
|
||||
pub workspace: String,
|
||||
pub readonly: String,
|
||||
}
|
||||
|
||||
pub fn cap_sid_file(home: &Path) -> PathBuf {
|
||||
home.join("cap_sid")
|
||||
}
|
||||
|
||||
fn make_random_cap_sid_string() -> String {
|
||||
let mut rng = SmallRng::from_entropy();
|
||||
let a = rng.next_u32();
|
||||
let b = rng.next_u32();
|
||||
let c = rng.next_u32();
|
||||
let d = rng.next_u32();
|
||||
format!("S-1-5-21-{}-{}-{}-{}", a, b, c, d)
|
||||
}
|
||||
|
||||
pub fn load_or_create_cap_sids(home: &Path) -> CapSids {
|
||||
let path = cap_sid_file(home);
|
||||
if path.exists() {
|
||||
if let Ok(txt) = fs::read_to_string(&path) {
|
||||
let t = txt.trim();
|
||||
if t.starts_with('{') && t.ends_with('}') {
|
||||
if let Ok(obj) = serde_json::from_str::<CapSids>(t) {
|
||||
return obj;
|
||||
}
|
||||
} else if !t.is_empty() {
|
||||
return CapSids {
|
||||
workspace: t.to_string(),
|
||||
readonly: make_random_cap_sid_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
CapSids {
|
||||
workspace: make_random_cap_sid_string(),
|
||||
readonly: make_random_cap_sid_string(),
|
||||
}
|
||||
}
|
||||
135
qimingclaw/crates/windows-sandbox-helper/src/env.rs
Normal file
135
qimingclaw/crates/windows-sandbox-helper/src/env.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
//! Environment variable manipulation for sandboxed processes.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn normalize_null_device_env(env_map: &mut HashMap<String, String>) {
|
||||
let keys: Vec<String> = env_map.keys().cloned().collect();
|
||||
for k in keys {
|
||||
if let Some(v) = env_map.get(&k).cloned() {
|
||||
let t = v.trim().to_ascii_lowercase();
|
||||
if t == "/dev/null" || t == "\\\\dev\\\\null" {
|
||||
env_map.insert(k, "NUL".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_non_interactive_pager(env_map: &mut HashMap<String, String>) {
|
||||
env_map.entry("GIT_PAGER".into()).or_insert_with(|| "more.com".into());
|
||||
env_map.entry("PAGER".into()).or_insert_with(|| "more.com".into());
|
||||
env_map.entry("LESS".into()).or_insert_with(|| "".into());
|
||||
}
|
||||
|
||||
fn prepend_path(env_map: &mut HashMap<String, String>, prefix: &str) {
|
||||
let existing = env_map
|
||||
.get("PATH")
|
||||
.cloned()
|
||||
.or_else(|| env::var("PATH").ok())
|
||||
.unwrap_or_default();
|
||||
let parts: Vec<String> = existing.split(';').map(|s| s.to_string()).collect();
|
||||
if parts.first().map(|p| p.eq_ignore_ascii_case(prefix)).unwrap_or(false) {
|
||||
return;
|
||||
}
|
||||
let mut new_path = String::new();
|
||||
new_path.push_str(prefix);
|
||||
if !existing.is_empty() {
|
||||
new_path.push(';');
|
||||
new_path.push_str(&existing);
|
||||
}
|
||||
env_map.insert("PATH".into(), new_path);
|
||||
}
|
||||
|
||||
fn reorder_pathext_for_stubs(env_map: &mut HashMap<String, String>) {
|
||||
let default = env_map
|
||||
.get("PATHEXT")
|
||||
.cloned()
|
||||
.or_else(|| env::var("PATHEXT").ok())
|
||||
.unwrap_or(".COM;.EXE;.BAT;.CMD".to_string());
|
||||
let exts: Vec<String> = default.split(';').filter(|e| !e.is_empty()).map(|s| s.to_string()).collect();
|
||||
let exts_norm: Vec<String> = exts.iter().map(|e| e.to_ascii_uppercase()).collect();
|
||||
let want = [".BAT", ".CMD"];
|
||||
let mut front: Vec<String> = Vec::new();
|
||||
for w in want {
|
||||
if let Some(idx) = exts_norm.iter().position(|e| e == w) {
|
||||
front.push(exts[idx].clone());
|
||||
}
|
||||
}
|
||||
let rest: Vec<String> = exts
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| {
|
||||
let up = &exts_norm[*i];
|
||||
up != ".BAT" && up != ".CMD"
|
||||
})
|
||||
.map(|(_, e)| e)
|
||||
.collect();
|
||||
let mut combined = Vec::new();
|
||||
combined.extend(front);
|
||||
combined.extend(rest);
|
||||
env_map.insert("PATHEXT".into(), combined.join(";"));
|
||||
}
|
||||
|
||||
fn ensure_denybin(tools: &[&str], denybin_dir: Option<&Path>) -> Result<PathBuf> {
|
||||
let base = match denybin_dir {
|
||||
Some(p) => p.to_path_buf(),
|
||||
None => {
|
||||
let home = dirs_next::home_dir().ok_or_else(|| anyhow::anyhow!("no home dir"))?;
|
||||
home.join(".sbx-denybin")
|
||||
}
|
||||
};
|
||||
fs::create_dir_all(&base)?;
|
||||
for tool in tools {
|
||||
for ext in [".bat", ".cmd"] {
|
||||
let path = base.join(format!("{}{}", tool, ext));
|
||||
if !path.exists() {
|
||||
let mut f = File::create(&path)?;
|
||||
f.write_all(b"@echo off\r\nexit /b 1\r\n")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(base)
|
||||
}
|
||||
|
||||
pub fn apply_no_network_to_env(env_map: &mut HashMap<String, String>) -> Result<()> {
|
||||
env_map.insert("SBX_NONET_ACTIVE".into(), "1".into());
|
||||
env_map
|
||||
.entry("HTTP_PROXY".into())
|
||||
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||
env_map
|
||||
.entry("HTTPS_PROXY".into())
|
||||
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||
env_map
|
||||
.entry("ALL_PROXY".into())
|
||||
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||
env_map
|
||||
.entry("NO_PROXY".into())
|
||||
.or_insert_with(|| "localhost,127.0.0.1,::1".into());
|
||||
env_map.insert("PIP_NO_INDEX".into(), "1".into());
|
||||
env_map.insert("PIP_DISABLE_PIP_VERSION_CHECK".into(), "1".into());
|
||||
env_map.insert("NPM_CONFIG_OFFLINE".into(), "true".into());
|
||||
env_map.insert("CARGO_NET_OFFLINE".into(), "true".into());
|
||||
env_map.insert("GIT_HTTP_PROXY".into(), "http://127.0.0.1:9".into());
|
||||
env_map.insert("GIT_HTTPS_PROXY".into(), "http://127.0.0.1:9".into());
|
||||
env_map.insert("GIT_SSH_COMMAND".into(), "cmd /c exit 1".into());
|
||||
env_map.insert("GIT_ALLOW_PROTOCOLS".into(), "".into());
|
||||
|
||||
let base = ensure_denybin(&["ssh", "scp"], None)?;
|
||||
for tool in ["curl", "wget"] {
|
||||
for ext in [".bat", ".cmd"] {
|
||||
let p = base.join(format!("{}{}", tool, ext));
|
||||
if p.exists() {
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
}
|
||||
}
|
||||
prepend_path(env_map, &base.to_string_lossy());
|
||||
reorder_pathext_for_stubs(env_map);
|
||||
Ok(())
|
||||
}
|
||||
59
qimingclaw/crates/windows-sandbox-helper/src/logging.rs
Normal file
59
qimingclaw/crates/windows-sandbox-helper/src/logging.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Command logging for audit trail.
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const LOG_COMMAND_PREVIEW_LIMIT: usize = 200;
|
||||
pub const LOG_FILE_NAME: &str = "sandbox_commands.rust.log";
|
||||
|
||||
fn preview(command: &[String]) -> String {
|
||||
let joined = command.join(" ");
|
||||
if joined.len() <= LOG_COMMAND_PREVIEW_LIMIT {
|
||||
joined
|
||||
} else {
|
||||
joined[..LOG_COMMAND_PREVIEW_LIMIT].to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn log_file_path(base_dir: &Path) -> Option<PathBuf> {
|
||||
if base_dir.is_dir() {
|
||||
Some(base_dir.join(LOG_FILE_NAME))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn append_line(line: &str, base_dir: Option<&Path>) {
|
||||
if let Some(dir) = base_dir {
|
||||
if let Some(path) = log_file_path(dir) {
|
||||
if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {
|
||||
let _ = writeln!(f, "{}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_start(command: &[String], base_dir: Option<&Path>) {
|
||||
append_line(&format!("START: {}", preview(command)), base_dir);
|
||||
}
|
||||
|
||||
pub fn log_success(command: &[String], base_dir: Option<&Path>) {
|
||||
append_line(&format!("SUCCESS: {}", preview(command)), base_dir);
|
||||
}
|
||||
|
||||
pub fn log_failure(command: &[String], detail: &str, base_dir: Option<&Path>) {
|
||||
append_line(&format!("FAILURE: {} ({})", preview(command), detail), base_dir);
|
||||
}
|
||||
|
||||
pub fn debug_log(msg: &str, base_dir: Option<&Path>) {
|
||||
if std::env::var("SBX_DEBUG").ok().as_deref() == Some("1") {
|
||||
append_line(&format!("DEBUG: {}", msg), base_dir);
|
||||
eprintln!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_note(msg: &str, base_dir: Option<&Path>) {
|
||||
append_line(msg, base_dir);
|
||||
}
|
||||
799
qimingclaw/crates/windows-sandbox-helper/src/main.rs
Normal file
799
qimingclaw/crates/windows-sandbox-helper/src/main.rs
Normal file
@@ -0,0 +1,799 @@
|
||||
//! Windows Restricted Token sandbox helper binary.
|
||||
//!
|
||||
//! Usage:
|
||||
//! ```bash
|
||||
//! # Capture mode: run command, capture output, print JSON result
|
||||
//! qiming-sandbox-helper run \
|
||||
//! --mode <read-only|workspace-write> \
|
||||
//! --cwd <path> \
|
||||
//! [--home <path>] \
|
||||
//! [--policy-json <json>] \
|
||||
//! -- <command> [args...]
|
||||
//!
|
||||
//! # Proxy mode: run command as persistent stdio proxy (stdin/stdout forwarded)
|
||||
//! qiming-sandbox-helper serve \
|
||||
//! --mode <read-only|workspace-write> \
|
||||
//! --cwd <path> \
|
||||
//! [--home <path>] \
|
||||
//! [--policy-json <json>] \
|
||||
//! -- <command> [args...]
|
||||
//! ```
|
||||
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
mod acl;
|
||||
mod allow;
|
||||
mod audit;
|
||||
mod cap;
|
||||
mod env;
|
||||
mod logging;
|
||||
mod policy;
|
||||
mod token;
|
||||
mod winutil;
|
||||
|
||||
use acl::{add_allow_ace, add_deny_write_ace, allow_null_device, revoke_ace};
|
||||
use allow::compute_allow_paths;
|
||||
use audit::apply_world_writable_scan_and_denies;
|
||||
use cap::{cap_sid_file, load_or_create_cap_sids};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use env::{apply_no_network_to_env, ensure_non_interactive_pager, normalize_null_device_env};
|
||||
use logging::{debug_log, log_failure, log_start, log_success};
|
||||
use policy::SandboxPolicy;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
use std::thread;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use token::{convert_string_sid_to_sid, create_restricted_token};
|
||||
use winutil::{format_last_error, to_wide};
|
||||
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, SetHandleInformation, HANDLE, HANDLE_FLAG_INHERIT};
|
||||
use windows_sys::Win32::System::Pipes::CreatePipe;
|
||||
use windows_sys::Win32::System::Threading::{
|
||||
CreateProcessAsUserW, GetExitCodeProcess, WaitForSingleObject, CREATE_UNICODE_ENVIRONMENT,
|
||||
INFINITE, PROCESS_INFORMATION, STARTF_USESTDHANDLES, STARTUPINFOW,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// CLI
|
||||
// ============================================================================
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug)]
|
||||
enum Mode {
|
||||
ReadOnly,
|
||||
WorkspaceWrite,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "qiming-sandbox-helper")]
|
||||
#[command(author = "Qiming Agent")]
|
||||
#[command(version = "0.1.0")]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
subcommand: Subcommand,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
enum Subcommand {
|
||||
/// Run a command in the sandbox, capture output, print JSON result.
|
||||
Run(RunArgs),
|
||||
/// Run a command in the sandbox as a persistent stdio proxy.
|
||||
/// Stdin/stdout/stderr are forwarded bidirectionally.
|
||||
///
|
||||
/// By default uses a token without WRITE_RESTRICTED for maximum
|
||||
/// compatibility. Pass --write-restricted to enable filesystem
|
||||
/// write protection via restricting SIDs + DACL ACEs (needed for
|
||||
/// strict/compat sandbox modes).
|
||||
Serve(ServeArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct ServeArgs {
|
||||
#[command(flatten)]
|
||||
common: CommonArgs,
|
||||
/// Enable WRITE_RESTRICTED on the sandbox token.
|
||||
/// When set, write access is restricted to paths with ALLOW ACEs
|
||||
/// for the restricting SIDs (capability, logon, everyone).
|
||||
/// Required for strict/compat sandbox modes to enforce filesystem
|
||||
/// write protection.
|
||||
#[arg(long, default_value_t = false)]
|
||||
write_restricted: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct RunArgs {
|
||||
#[command(flatten)]
|
||||
common: CommonArgs,
|
||||
/// Skip WRITE_RESTRICTED on the sandbox token. Allows child processes
|
||||
/// (e.g. Git Bash) to create pipes and modify their own token DACL.
|
||||
/// Filesystem write protection is still enforced by DACL ACEs.
|
||||
#[arg(long, default_value_t = false)]
|
||||
no_write_restricted: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct CommonArgs {
|
||||
#[arg(long)]
|
||||
mode: Mode,
|
||||
#[arg(long)]
|
||||
cwd: PathBuf,
|
||||
#[arg(long, env = "QIMING_SANDBOX_HOME")]
|
||||
home: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
policy_json: Option<String>,
|
||||
#[arg(last = true)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Shared helpers
|
||||
// ============================================================================
|
||||
|
||||
type PipeHandles = ((HANDLE, HANDLE), (HANDLE, HANDLE), (HANDLE, HANDLE));
|
||||
|
||||
fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
|
||||
!policy.has_full_network_access()
|
||||
}
|
||||
|
||||
fn ensure_dir(p: &Path) -> anyhow::Result<()> {
|
||||
if let Some(d) = p.parent() {
|
||||
std::fs::create_dir_all(d)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_sandbox_home_exists(p: &Path) -> anyhow::Result<()> {
|
||||
std::fs::create_dir_all(p)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
|
||||
let mut items: Vec<(String, String)> = env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
items.sort_by(|a, b| a.0.to_uppercase().cmp(&b.0.to_uppercase()).then(a.0.cmp(&b.0)));
|
||||
let mut w: Vec<u16> = Vec::new();
|
||||
for (k, v) in items {
|
||||
let mut s = to_wide(format!("{}={}", k, v));
|
||||
s.pop();
|
||||
w.extend_from_slice(&s);
|
||||
w.push(0);
|
||||
}
|
||||
w.push(0);
|
||||
w
|
||||
}
|
||||
|
||||
fn quote_windows_arg(arg: &str) -> String {
|
||||
let needs_quotes =
|
||||
arg.is_empty() || arg.chars().any(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '"'));
|
||||
if !needs_quotes {
|
||||
return arg.to_string();
|
||||
}
|
||||
let mut quoted = String::with_capacity(arg.len() + 2);
|
||||
quoted.push('"');
|
||||
let mut backslashes = 0usize;
|
||||
for ch in arg.chars() {
|
||||
match ch {
|
||||
'\\' => backslashes += 1,
|
||||
'"' => {
|
||||
quoted.push_str(&"\\".repeat(backslashes * 2 + 1));
|
||||
quoted.push('"');
|
||||
backslashes = 0;
|
||||
}
|
||||
_ => {
|
||||
if backslashes > 0 {
|
||||
quoted.push_str(&"\\".repeat(backslashes));
|
||||
backslashes = 0;
|
||||
}
|
||||
quoted.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if backslashes > 0 {
|
||||
quoted.push_str(&"\\".repeat(backslashes * 2));
|
||||
}
|
||||
quoted.push('"');
|
||||
quoted
|
||||
}
|
||||
|
||||
unsafe fn setup_stdio_pipes() -> io::Result<PipeHandles> {
|
||||
let mut in_r: HANDLE = 0;
|
||||
let mut in_w: HANDLE = 0;
|
||||
let mut out_r: HANDLE = 0;
|
||||
let mut out_w: HANDLE = 0;
|
||||
let mut err_r: HANDLE = 0;
|
||||
let mut err_w: HANDLE = 0;
|
||||
|
||||
if CreatePipe(&mut in_r, &mut in_w, ptr::null_mut(), 0) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
if CreatePipe(&mut out_r, &mut out_w, ptr::null_mut(), 0) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
if CreatePipe(&mut err_r, &mut err_w, ptr::null_mut(), 0) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
if SetHandleInformation(in_r, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
if SetHandleInformation(out_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
if SetHandleInformation(err_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
||||
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||
}
|
||||
Ok(((in_r, in_w), (out_r, out_w), (err_r, err_w)))
|
||||
}
|
||||
|
||||
/// Helper to close an array of HANDLEs, ignoring nulls.
|
||||
unsafe fn close_handles(handles: &[HANDLE]) {
|
||||
for &h in handles {
|
||||
if h != 0 {
|
||||
CloseHandle(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Shared sandbox context (token + ACL + env setup)
|
||||
// ============================================================================
|
||||
|
||||
/// Holds the state needed to spawn a sandboxed child process.
|
||||
struct SandboxContext {
|
||||
/// Modified environment block for the child.
|
||||
env_map: HashMap<String, String>,
|
||||
/// Restricted token for CreateProcessAsUserW.
|
||||
h_token: HANDLE,
|
||||
/// Whether ACEs should persist (workspace-write mode).
|
||||
persist_aces: bool,
|
||||
/// ACE guards to revoke on cleanup (only when !persist_aces).
|
||||
guards: Vec<(PathBuf, *mut c_void)>,
|
||||
/// Logging directory.
|
||||
logs_base_dir: Option<PathBuf>,
|
||||
/// Full command line (for logging).
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
impl SandboxContext {
|
||||
/// Set up sandbox policy, restricted token, ACLs, and environment.
|
||||
fn setup(
|
||||
policy_json_or_preset: &str,
|
||||
sandbox_policy_cwd: &Path,
|
||||
home: &Path,
|
||||
command: Vec<String>,
|
||||
cwd: &Path,
|
||||
args: Vec<String>,
|
||||
write_restricted: bool,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut env_map: HashMap<String, String> = std::env::vars().collect();
|
||||
let mut command = command;
|
||||
command.extend(args);
|
||||
|
||||
let policy = policy::parse_policy(policy_json_or_preset)?;
|
||||
if should_apply_network_block(&policy) {
|
||||
normalize_null_device_env(&mut env_map);
|
||||
ensure_non_interactive_pager(&mut env_map);
|
||||
apply_no_network_to_env(&mut env_map)?;
|
||||
} else {
|
||||
normalize_null_device_env(&mut env_map);
|
||||
ensure_non_interactive_pager(&mut env_map);
|
||||
}
|
||||
ensure_sandbox_home_exists(home)?;
|
||||
|
||||
let current_dir = cwd.to_path_buf();
|
||||
let logs_base_dir = Some(home.to_path_buf());
|
||||
log_start(&command, logs_base_dir.as_deref());
|
||||
|
||||
let cap_sid_path = cap_sid_file(home);
|
||||
let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. });
|
||||
|
||||
let (h_token, psid): (HANDLE, *mut c_void) = unsafe {
|
||||
let caps = load_or_create_cap_sids(home);
|
||||
ensure_dir(&cap_sid_path)?;
|
||||
fs::write(&cap_sid_path, serde_json::to_string(&caps)?)?;
|
||||
|
||||
let cap_sid_str = if is_workspace_write { &caps.workspace } else { &caps.readonly };
|
||||
let psid = convert_string_sid_to_sid(cap_sid_str).unwrap();
|
||||
|
||||
create_restricted_token(psid, write_restricted)?
|
||||
};
|
||||
|
||||
unsafe {
|
||||
if is_workspace_write {
|
||||
if let Ok(base) = token::get_current_token_for_restriction() {
|
||||
if let Ok(bytes) = token::get_logon_sid_bytes(base) {
|
||||
let mut tmp = bytes.clone();
|
||||
let psid2 = tmp.as_mut_ptr() as *mut c_void;
|
||||
allow_null_device(psid2);
|
||||
}
|
||||
CloseHandle(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let persist_aces = is_workspace_write;
|
||||
let allow_deny = compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map);
|
||||
let mut guards: Vec<(PathBuf, *mut c_void)> = Vec::new();
|
||||
|
||||
unsafe {
|
||||
for p in &allow_deny.allow {
|
||||
if let Ok(added) = add_allow_ace(p, psid) {
|
||||
if added && !persist_aces {
|
||||
guards.push((p.clone(), psid));
|
||||
}
|
||||
}
|
||||
}
|
||||
for p in &allow_deny.deny {
|
||||
if let Ok(added) = add_deny_write_ace(p, psid) {
|
||||
if added && !persist_aces {
|
||||
guards.push((p.clone(), psid));
|
||||
}
|
||||
}
|
||||
}
|
||||
allow_null_device(psid);
|
||||
}
|
||||
|
||||
let _ = apply_world_writable_scan_and_denies(
|
||||
home, ¤t_dir, &env_map, &policy, logs_base_dir.as_deref(),
|
||||
);
|
||||
|
||||
// Mark the child as running inside a sandbox so it skips nested sandboxing.
|
||||
env_map.insert("QIMING_IN_SANDBOX".to_string(), "1".to_string());
|
||||
|
||||
Ok(Self {
|
||||
env_map,
|
||||
h_token,
|
||||
persist_aces,
|
||||
guards,
|
||||
logs_base_dir,
|
||||
command,
|
||||
})
|
||||
}
|
||||
|
||||
/// Spawn the child process with restricted token and the given stdio handles.
|
||||
unsafe fn spawn_child(
|
||||
&self,
|
||||
cwd: &Path,
|
||||
child_stdin_r: HANDLE,
|
||||
child_stdout_w: HANDLE,
|
||||
child_stderr_w: HANDLE,
|
||||
all_pipe_handles: &[HANDLE],
|
||||
) -> anyhow::Result<PROCESS_INFORMATION> {
|
||||
let mut si: STARTUPINFOW = std::mem::zeroed();
|
||||
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
|
||||
si.dwFlags |= STARTF_USESTDHANDLES;
|
||||
si.hStdInput = child_stdin_r;
|
||||
si.hStdOutput = child_stdout_w;
|
||||
si.hStdError = child_stderr_w;
|
||||
|
||||
let cmdline_str = self.command
|
||||
.iter()
|
||||
.map(|a| quote_windows_arg(a))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
|
||||
let env_block = make_env_block(&self.env_map);
|
||||
let desktop = to_wide("Winsta0\\Default");
|
||||
si.lpDesktop = desktop.as_ptr() as *mut u16;
|
||||
|
||||
let mut pi: PROCESS_INFORMATION = std::mem::zeroed();
|
||||
|
||||
let spawn_res = CreateProcessAsUserW(
|
||||
self.h_token,
|
||||
ptr::null(),
|
||||
cmdline.as_mut_ptr(),
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
1,
|
||||
CREATE_UNICODE_ENVIRONMENT,
|
||||
env_block.as_ptr() as *mut c_void,
|
||||
to_wide(cwd).as_ptr(),
|
||||
&si,
|
||||
&mut pi,
|
||||
);
|
||||
|
||||
if spawn_res == 0 {
|
||||
let err = GetLastError() as i32;
|
||||
let msg = format!(
|
||||
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={}",
|
||||
err,
|
||||
format_last_error(err),
|
||||
cwd.display(),
|
||||
cmdline_str,
|
||||
env_block.len(),
|
||||
);
|
||||
debug_log(&msg, self.logs_base_dir.as_deref());
|
||||
close_handles(all_pipe_handles);
|
||||
CloseHandle(self.h_token);
|
||||
return Err(anyhow::anyhow!("CreateProcessAsUserW failed: {}", err));
|
||||
}
|
||||
|
||||
Ok(pi)
|
||||
}
|
||||
|
||||
/// Log the result and revoke temporary ACEs.
|
||||
fn cleanup(&mut self, exit_code: i32) {
|
||||
if exit_code == 0 {
|
||||
log_success(&self.command, self.logs_base_dir.as_deref());
|
||||
} else {
|
||||
log_failure(&self.command, &format!("exit code {}", exit_code), self.logs_base_dir.as_deref());
|
||||
}
|
||||
|
||||
if !self.persist_aces {
|
||||
unsafe {
|
||||
for (p, sid) in &self.guards {
|
||||
revoke_ace(p, *sid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Resolve common args
|
||||
// ============================================================================
|
||||
|
||||
fn resolve_home(args: &CommonArgs) -> PathBuf {
|
||||
args.home.clone().unwrap_or_else(|| {
|
||||
dirs_next::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("C:\\Users\\Default"))
|
||||
.join(".qiming-sandbox")
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_policy(args: &CommonArgs) -> String {
|
||||
if let Some(json) = &args.policy_json {
|
||||
json.clone()
|
||||
} else {
|
||||
match args.mode {
|
||||
Mode::ReadOnly => "read-only".to_string(),
|
||||
Mode::WorkspaceWrite => "workspace-write".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split_command(args: &CommonArgs) -> anyhow::Result<(String, Vec<String>)> {
|
||||
if args.command.is_empty() {
|
||||
anyhow::bail!("no command provided; use `-- -- <command> [args...]`");
|
||||
}
|
||||
Ok((args.command[0].clone(), args.command[1..].to_vec()))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Capture mode (run subcommand)
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CaptureResult {
|
||||
pub exit_code: i32,
|
||||
pub stdout: Vec<u8>,
|
||||
pub stderr: Vec<u8>,
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
pub fn run_sandbox_capture(
|
||||
policy_json_or_preset: &str,
|
||||
sandbox_policy_cwd: &Path,
|
||||
home: &Path,
|
||||
command: Vec<String>,
|
||||
cwd: &Path,
|
||||
args: Vec<String>,
|
||||
timeout_ms: Option<u64>,
|
||||
write_restricted: bool,
|
||||
) -> anyhow::Result<CaptureResult> {
|
||||
let mut ctx = SandboxContext::setup(policy_json_or_preset, sandbox_policy_cwd, home, command, cwd, args, write_restricted)?;
|
||||
|
||||
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
|
||||
let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair);
|
||||
|
||||
let all_pipes = [in_r, in_w, out_r, out_w, err_r, err_w];
|
||||
let pi = unsafe { ctx.spawn_child(cwd, in_r, out_w, err_w, &all_pipes)? };
|
||||
|
||||
// Close all child-ends and the stdin write-end (capture mode doesn't forward stdin)
|
||||
unsafe {
|
||||
CloseHandle(in_r); // child's stdin read end
|
||||
CloseHandle(in_w); // parent's stdin write end (not forwarding stdin in capture mode)
|
||||
CloseHandle(out_w); // child's stdout write end
|
||||
CloseHandle(err_w); // child's stderr write end
|
||||
}
|
||||
|
||||
// Read stdout/stderr in background threads
|
||||
let (tx_out, rx_out) = mpsc::channel::<Vec<u8>>();
|
||||
let (tx_err, rx_err) = mpsc::channel::<Vec<u8>>();
|
||||
|
||||
let t_out = thread::spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
let mut tmp = [0u8; 8192];
|
||||
loop {
|
||||
let mut read_bytes: u32 = 0;
|
||||
let ok = unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
||||
out_r,
|
||||
tmp.as_mut_ptr(),
|
||||
tmp.len() as u32,
|
||||
&mut read_bytes,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 || read_bytes == 0 {
|
||||
break;
|
||||
}
|
||||
buf.extend_from_slice(&tmp[..read_bytes as usize]);
|
||||
}
|
||||
let _ = tx_out.send(buf);
|
||||
});
|
||||
|
||||
let t_err = thread::spawn(move || {
|
||||
let mut buf = Vec::new();
|
||||
let mut tmp = [0u8; 8192];
|
||||
loop {
|
||||
let mut read_bytes: u32 = 0;
|
||||
let ok = unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
||||
err_r,
|
||||
tmp.as_mut_ptr(),
|
||||
tmp.len() as u32,
|
||||
&mut read_bytes,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 || read_bytes == 0 {
|
||||
break;
|
||||
}
|
||||
buf.extend_from_slice(&tmp[..read_bytes as usize]);
|
||||
}
|
||||
let _ = tx_err.send(buf);
|
||||
});
|
||||
|
||||
let timeout = timeout_ms.unwrap_or(u64::MAX).min(u64::from(INFINITE)) as u32;
|
||||
let res = unsafe { WaitForSingleObject(pi.hProcess, timeout) };
|
||||
let timed_out = res == 0x0000_0102;
|
||||
|
||||
let mut exit_code_u32: u32 = 1;
|
||||
if !timed_out {
|
||||
unsafe { GetExitCodeProcess(pi.hProcess, &mut exit_code_u32); }
|
||||
} else {
|
||||
unsafe {
|
||||
windows_sys::Win32::System::Threading::TerminateProcess(pi.hProcess, 1);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
close_handles(&[pi.hThread, pi.hProcess, ctx.h_token]);
|
||||
}
|
||||
|
||||
let _ = t_out.join();
|
||||
let _ = t_err.join();
|
||||
let stdout = rx_out.recv().unwrap_or_default();
|
||||
let stderr = rx_err.recv().unwrap_or_default();
|
||||
let exit_code = if timed_out { 128 + 64 } else { exit_code_u32 as i32 };
|
||||
|
||||
ctx.cleanup(exit_code);
|
||||
|
||||
Ok(CaptureResult { exit_code, stdout, stderr, timed_out })
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Proxy mode (serve subcommand)
|
||||
// ============================================================================
|
||||
|
||||
/// Result of a proxy-mode sandbox run.
|
||||
pub struct ProxyResult {
|
||||
pub exit_code: i32,
|
||||
}
|
||||
|
||||
/// Run a sandboxed process with bidirectional stdio forwarding.
|
||||
pub fn run_sandbox_proxy(
|
||||
policy_json_or_preset: &str,
|
||||
sandbox_policy_cwd: &Path,
|
||||
home: &Path,
|
||||
command: Vec<String>,
|
||||
cwd: &Path,
|
||||
args: Vec<String>,
|
||||
write_restricted: bool,
|
||||
) -> anyhow::Result<ProxyResult> {
|
||||
// When write_restricted=true, the restricted token gets WRITE_RESTRICTED
|
||||
// flag + restricting SIDs, so only paths with explicit ALLOW ACEs are
|
||||
// writable. When false, only DACL-level token restrictions apply
|
||||
// (permissive mode — no filesystem write protection).
|
||||
debug_log(
|
||||
&format!(
|
||||
"serve mode: write_restricted={} ({}), policy={}",
|
||||
write_restricted,
|
||||
if write_restricted { "STRICT/COMPAT — filesystem writes restricted to ALLOW ACEs" } else { "PERMISSIVE — no filesystem write protection" },
|
||||
policy_json_or_preset,
|
||||
),
|
||||
None,
|
||||
);
|
||||
let mut ctx = SandboxContext::setup(policy_json_or_preset, sandbox_policy_cwd, home, command, cwd, args, write_restricted)?;
|
||||
|
||||
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
|
||||
let ((child_stdin_r, child_stdin_w), (child_stdout_r, child_stdout_w), (child_stderr_r, child_stderr_w)) =
|
||||
(stdin_pair, stdout_pair, stderr_pair);
|
||||
|
||||
let all_pipes = [child_stdin_r, child_stdin_w, child_stdout_r, child_stdout_w, child_stderr_r, child_stderr_w];
|
||||
let pi = unsafe { ctx.spawn_child(cwd, child_stdin_r, child_stdout_w, child_stderr_w, &all_pipes)? };
|
||||
|
||||
// Close child-side handles that the parent doesn't need
|
||||
unsafe {
|
||||
CloseHandle(child_stdin_r); // child's stdin read end
|
||||
CloseHandle(child_stdout_w); // child's stdout write end
|
||||
CloseHandle(child_stderr_w); // child's stderr write end
|
||||
}
|
||||
|
||||
// --- Bidirectional forwarding threads ---
|
||||
// stdin: parent_stdin → child_stdin_w
|
||||
let stdin_writer_w = child_stdin_w;
|
||||
let (stdin_done_tx, stdin_done_rx) = mpsc::channel::<()>();
|
||||
let stdin_thread = thread::spawn(move || {
|
||||
let mut buf = [0u8; 8192];
|
||||
let mut stdin_handle = io::stdin();
|
||||
loop {
|
||||
match stdin_handle.read(&mut buf) {
|
||||
Ok(0) => break, // EOF
|
||||
Ok(n) => {
|
||||
let mut written = 0usize;
|
||||
while written < n {
|
||||
let mut bytes_written: u32 = 0;
|
||||
let ok = unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::WriteFile(
|
||||
stdin_writer_w,
|
||||
buf[written..].as_ptr(),
|
||||
(n - written) as u32,
|
||||
&mut bytes_written,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 || bytes_written == 0 {
|
||||
break;
|
||||
}
|
||||
written += bytes_written as usize;
|
||||
}
|
||||
if written < n {
|
||||
break; // pipe broken
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
unsafe { CloseHandle(stdin_writer_w); }
|
||||
let _ = stdin_done_tx.send(());
|
||||
});
|
||||
|
||||
// stdout: child_stdout_r → parent_stdout
|
||||
let stdout_reader_r = child_stdout_r;
|
||||
let stdout_thread = thread::spawn(move || {
|
||||
let mut buf = [0u8; 8192];
|
||||
let mut stdout_handle = io::stdout();
|
||||
loop {
|
||||
let mut read_bytes: u32 = 0;
|
||||
let ok = unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
||||
stdout_reader_r,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as u32,
|
||||
&mut read_bytes,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 || read_bytes == 0 {
|
||||
break;
|
||||
}
|
||||
if stdout_handle.write_all(&buf[..read_bytes as usize]).is_err() {
|
||||
break;
|
||||
}
|
||||
let _ = stdout_handle.flush();
|
||||
}
|
||||
unsafe { CloseHandle(stdout_reader_r); }
|
||||
});
|
||||
|
||||
// stderr: child_stderr_r → parent_stderr
|
||||
let stderr_reader_r = child_stderr_r;
|
||||
let stderr_thread = thread::spawn(move || {
|
||||
let mut buf = [0u8; 8192];
|
||||
let mut stderr_handle = io::stderr();
|
||||
loop {
|
||||
let mut read_bytes: u32 = 0;
|
||||
let ok = unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
||||
stderr_reader_r,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as u32,
|
||||
&mut read_bytes,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 || read_bytes == 0 {
|
||||
break;
|
||||
}
|
||||
if stderr_handle.write_all(&buf[..read_bytes as usize]).is_err() {
|
||||
break;
|
||||
}
|
||||
let _ = stderr_handle.flush();
|
||||
}
|
||||
unsafe { CloseHandle(stderr_reader_r); }
|
||||
});
|
||||
|
||||
// Wait for child process to exit
|
||||
unsafe { WaitForSingleObject(pi.hProcess, INFINITE) };
|
||||
|
||||
let mut exit_code_u32: u32 = 1;
|
||||
unsafe { GetExitCodeProcess(pi.hProcess, &mut exit_code_u32); }
|
||||
|
||||
// Close child process handles so stdout/stderr forwarding threads can unblock
|
||||
unsafe {
|
||||
close_handles(&[pi.hThread, pi.hProcess, ctx.h_token]);
|
||||
}
|
||||
|
||||
// Wait for stdout/stderr threads to drain
|
||||
let _ = stdout_thread.join();
|
||||
let _ = stderr_thread.join();
|
||||
|
||||
// stdin thread may block on parent stdin — give it a short window then detach
|
||||
if stdin_done_rx.recv_timeout(Duration::from_secs(2)).is_err() {
|
||||
drop(stdin_thread);
|
||||
}
|
||||
|
||||
let exit_code = exit_code_u32 as i32;
|
||||
ctx.cleanup(exit_code);
|
||||
|
||||
Ok(ProxyResult { exit_code })
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// main
|
||||
// ============================================================================
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
match args.subcommand {
|
||||
Subcommand::Run(run) => {
|
||||
let home = resolve_home(&run.common);
|
||||
let policy = resolve_policy(&run.common);
|
||||
let (cmd, cmd_args) = split_command(&run.common)?;
|
||||
let write_restricted = !run.no_write_restricted;
|
||||
|
||||
let timeout_ms: u64 = 300_000;
|
||||
let result = run_sandbox_capture(
|
||||
&policy,
|
||||
&run.common.cwd,
|
||||
&home,
|
||||
vec![cmd],
|
||||
&run.common.cwd,
|
||||
cmd_args,
|
||||
Some(timeout_ms),
|
||||
write_restricted,
|
||||
)?;
|
||||
|
||||
println!(
|
||||
"{{\"exit_code\":{},\"stdout\":{},\"stderr\":{},\"timed_out\":{}}}",
|
||||
result.exit_code,
|
||||
serde_json::to_string(&String::from_utf8_lossy(&result.stdout))?,
|
||||
serde_json::to_string(&String::from_utf8_lossy(&result.stderr))?,
|
||||
result.timed_out
|
||||
);
|
||||
}
|
||||
Subcommand::Serve(serve) => {
|
||||
let home = resolve_home(&serve.common);
|
||||
let policy = resolve_policy(&serve.common);
|
||||
let (cmd, cmd_args) = split_command(&serve.common)?;
|
||||
|
||||
let result = run_sandbox_proxy(
|
||||
&policy,
|
||||
&serve.common.cwd,
|
||||
&home,
|
||||
vec![cmd],
|
||||
&serve.common.cwd,
|
||||
cmd_args,
|
||||
serve.write_restricted,
|
||||
)?;
|
||||
|
||||
std::process::exit(result.exit_code);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
89
qimingclaw/crates/windows-sandbox-helper/src/policy.rs
Normal file
89
qimingclaw/crates/windows-sandbox-helper/src/policy.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
//! Sandbox policy types and parsing.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Sandbox strictness mode.
|
||||
///
|
||||
/// Using an enum instead of a raw string ensures that invalid values (e.g. typos
|
||||
/// like "strcit") cause a deserialization error rather than silently falling
|
||||
/// through to a less-strict mode (fail-closed).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SandboxMode {
|
||||
Strict,
|
||||
#[default]
|
||||
Compat,
|
||||
Permissive,
|
||||
}
|
||||
|
||||
/// Windows Restricted Token sandbox policy.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum SandboxPolicy {
|
||||
/// Read-only access to the entire file-system.
|
||||
ReadOnly,
|
||||
|
||||
/// Read-only + write access to workspace directories.
|
||||
WorkspaceWrite {
|
||||
/// Additional writable folders beyond cwd and TMPDIR.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
writable_roots: Vec<PathBuf>,
|
||||
|
||||
/// Allow outbound network access. Defaults to false.
|
||||
#[serde(default)]
|
||||
network_access: bool,
|
||||
|
||||
/// Sandbox strictness mode — controls which system paths are writable.
|
||||
/// - "strict": only workspace + TEMP/TMP (no APPDATA)
|
||||
/// - "compat": workspace + TEMP/TMP + APPDATA/LOCALAPPDATA
|
||||
/// - "permissive": all user-writable paths
|
||||
/// Defaults to "compat" when absent.
|
||||
#[serde(default)]
|
||||
sandbox_mode: SandboxMode,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for SandboxPolicy {
|
||||
fn default() -> Self {
|
||||
SandboxPolicy::ReadOnly
|
||||
}
|
||||
}
|
||||
|
||||
impl SandboxPolicy {
|
||||
pub fn new_read_only_policy() -> Self {
|
||||
SandboxPolicy::ReadOnly
|
||||
}
|
||||
|
||||
pub fn new_workspace_write_policy() -> Self {
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: Vec::new(),
|
||||
network_access: false,
|
||||
sandbox_mode: SandboxMode::Compat,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_full_network_access(&self) -> bool {
|
||||
match self {
|
||||
SandboxPolicy::ReadOnly => false,
|
||||
SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the sandbox strictness mode.
|
||||
pub fn sandbox_mode(&self) -> &SandboxMode {
|
||||
match self {
|
||||
SandboxPolicy::ReadOnly => &SandboxMode::Strict,
|
||||
SandboxPolicy::WorkspaceWrite { sandbox_mode, .. } => sandbox_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a policy from a string (preset name or JSON).
|
||||
pub fn parse_policy(value: &str) -> anyhow::Result<SandboxPolicy> {
|
||||
match value {
|
||||
"read-only" => Ok(SandboxPolicy::new_read_only_policy()),
|
||||
"workspace-write" => Ok(SandboxPolicy::new_workspace_write_policy()),
|
||||
other => Ok(serde_json::from_str(other)?),
|
||||
}
|
||||
}
|
||||
225
qimingclaw/crates/windows-sandbox-helper/src/token.rs
Normal file
225
qimingclaw/crates/windows-sandbox-helper/src/token.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
//! Windows Restricted Token creation.
|
||||
|
||||
use crate::winutil::to_wide;
|
||||
use anyhow::Result;
|
||||
use std::ffi::c_void;
|
||||
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, HANDLE, LUID};
|
||||
use windows_sys::Win32::Security::{
|
||||
AdjustTokenPrivileges, CopySid, CreateRestrictedToken, CreateWellKnownSid,
|
||||
GetLengthSid, GetTokenInformation, LookupPrivilegeValueW, TokenGroups, SID_AND_ATTRIBUTES,
|
||||
TOKEN_ADJUST_DEFAULT, TOKEN_ADJUST_PRIVILEGES, TOKEN_ADJUST_SESSIONID, TOKEN_ASSIGN_PRIMARY,
|
||||
TOKEN_DUPLICATE, TOKEN_PRIVILEGES, TOKEN_QUERY,
|
||||
};
|
||||
use windows_sys::Win32::System::Threading::GetCurrentProcess;
|
||||
|
||||
const DISABLE_MAX_PRIVILEGE: u32 = 0x01;
|
||||
const LUA_TOKEN: u32 = 0x04;
|
||||
const WRITE_RESTRICTED: u32 = 0x08;
|
||||
const WIN_WORLD_SID: i32 = 1;
|
||||
const SE_GROUP_LOGON_ID: u32 = 0xC0000000;
|
||||
|
||||
pub unsafe fn world_sid() -> Result<Vec<u8>> {
|
||||
let mut size: u32 = 0;
|
||||
CreateWellKnownSid(WIN_WORLD_SID, std::ptr::null_mut(), std::ptr::null_mut(), &mut size);
|
||||
let mut buf: Vec<u8> = vec![0u8; size as usize];
|
||||
let ok = CreateWellKnownSid(WIN_WORLD_SID, std::ptr::null_mut(), buf.as_mut_ptr() as *mut c_void, &mut size);
|
||||
if ok == 0 {
|
||||
return Err(anyhow::anyhow!("CreateWellKnownSid failed: {}", GetLastError()));
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub unsafe fn convert_string_sid_to_sid(s: &str) -> Option<*mut c_void> {
|
||||
#[link(name = "advapi32")]
|
||||
extern "system" {
|
||||
fn ConvertStringSidToSidW(StringSid: *const u16, Sid: *mut *mut c_void) -> i32;
|
||||
}
|
||||
let mut psid: *mut c_void = std::ptr::null_mut();
|
||||
let ok = unsafe { ConvertStringSidToSidW(to_wide(s).as_ptr(), &mut psid) };
|
||||
if ok != 0 {
|
||||
Some(psid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn get_current_token_for_restriction() -> Result<HANDLE> {
|
||||
let desired = TOKEN_DUPLICATE
|
||||
| TOKEN_QUERY
|
||||
| TOKEN_ASSIGN_PRIMARY
|
||||
| TOKEN_ADJUST_DEFAULT
|
||||
| TOKEN_ADJUST_SESSIONID
|
||||
| TOKEN_ADJUST_PRIVILEGES;
|
||||
let mut h: HANDLE = 0;
|
||||
#[link(name = "advapi32")]
|
||||
extern "system" {
|
||||
fn OpenProcessToken(ProcessHandle: HANDLE, DesiredAccess: u32, TokenHandle: *mut HANDLE) -> i32;
|
||||
}
|
||||
let ok = unsafe { OpenProcessToken(GetCurrentProcess(), desired, &mut h) };
|
||||
if ok == 0 {
|
||||
return Err(anyhow::anyhow!("OpenProcessToken failed: {}", GetLastError()));
|
||||
}
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
pub unsafe fn get_logon_sid_bytes(h_token: HANDLE) -> Result<Vec<u8>> {
|
||||
unsafe fn scan_token_groups_for_logon(h: HANDLE) -> Option<Vec<u8>> {
|
||||
let mut needed: u32 = 0;
|
||||
GetTokenInformation(h, TokenGroups, std::ptr::null_mut(), 0, &mut needed);
|
||||
if needed == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut buf: Vec<u8> = vec![0u8; needed as usize];
|
||||
let ok = GetTokenInformation(
|
||||
h, TokenGroups, buf.as_mut_ptr() as *mut c_void, needed, &mut needed,
|
||||
);
|
||||
if ok == 0 || (needed as usize) < std::mem::size_of::<u32>() {
|
||||
return None;
|
||||
}
|
||||
let group_count = std::ptr::read_unaligned(buf.as_ptr() as *const u32) as usize;
|
||||
let after_count =
|
||||
unsafe { buf.as_ptr().add(std::mem::size_of::<u32>()) } as usize;
|
||||
let align = std::mem::align_of::<SID_AND_ATTRIBUTES>();
|
||||
let aligned = (after_count + (align - 1)) & !(align - 1);
|
||||
let groups_ptr = aligned as *const SID_AND_ATTRIBUTES;
|
||||
for i in 0..group_count {
|
||||
let entry: SID_AND_ATTRIBUTES = unsafe { std::ptr::read_unaligned(groups_ptr.add(i)) };
|
||||
if (entry.Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID {
|
||||
let sid = entry.Sid;
|
||||
let sid_len = unsafe { GetLengthSid(sid) };
|
||||
if sid_len == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut out = vec![0u8; sid_len as usize];
|
||||
if unsafe { CopySid(sid_len, out.as_mut_ptr() as *mut c_void, sid) } == 0 {
|
||||
return None;
|
||||
}
|
||||
return Some(out);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
if let Some(v) = unsafe { scan_token_groups_for_logon(h_token) } {
|
||||
return Ok(v);
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct TOKEN_LINKED_TOKEN {
|
||||
linked_token: HANDLE,
|
||||
}
|
||||
const TOKEN_LINKED_TOKEN_CLASS: i32 = 19;
|
||||
let mut ln_needed: u32 = 0;
|
||||
unsafe {
|
||||
GetTokenInformation(
|
||||
h_token,
|
||||
TOKEN_LINKED_TOKEN_CLASS,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
&mut ln_needed,
|
||||
)
|
||||
};
|
||||
if ln_needed >= std::mem::size_of::<TOKEN_LINKED_TOKEN>() as u32 {
|
||||
let mut ln_buf: Vec<u8> = vec![0u8; ln_needed as usize];
|
||||
let ok = unsafe {
|
||||
GetTokenInformation(
|
||||
h_token,
|
||||
TOKEN_LINKED_TOKEN_CLASS,
|
||||
ln_buf.as_mut_ptr() as *mut c_void,
|
||||
ln_needed,
|
||||
&mut ln_needed,
|
||||
)
|
||||
};
|
||||
if ok != 0 {
|
||||
let lt: TOKEN_LINKED_TOKEN = unsafe { std::ptr::read_unaligned(ln_buf.as_ptr() as *const TOKEN_LINKED_TOKEN) };
|
||||
if lt.linked_token != 0 {
|
||||
let res = unsafe { scan_token_groups_for_logon(lt.linked_token) };
|
||||
unsafe { CloseHandle(lt.linked_token) };
|
||||
if let Some(v) = res {
|
||||
return Ok(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("Logon SID not present on token"))
|
||||
}
|
||||
|
||||
unsafe fn enable_single_privilege(h_token: HANDLE, name: &str) -> Result<()> {
|
||||
let mut luid = LUID { LowPart: 0, HighPart: 0 };
|
||||
let ok = unsafe { LookupPrivilegeValueW(std::ptr::null(), to_wide(name).as_ptr(), &mut luid) };
|
||||
if ok == 0 {
|
||||
return Err(anyhow::anyhow!("LookupPrivilegeValueW failed: {}", GetLastError()));
|
||||
}
|
||||
let mut tp: TOKEN_PRIVILEGES = unsafe { std::mem::zeroed() };
|
||||
tp.PrivilegeCount = 1;
|
||||
tp.Privileges[0].Luid = luid;
|
||||
tp.Privileges[0].Attributes = 0x00000002;
|
||||
let ok2 = unsafe {
|
||||
AdjustTokenPrivileges(h_token, 0, &tp, 0, std::ptr::null_mut(), std::ptr::null_mut())
|
||||
};
|
||||
if ok2 == 0 {
|
||||
return Err(anyhow::anyhow!("AdjustTokenPrivileges failed: {}", GetLastError()));
|
||||
}
|
||||
let err = unsafe { GetLastError() };
|
||||
if err != 0 {
|
||||
return Err(anyhow::anyhow!("AdjustTokenPrivileges error {}", err));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a restricted token for the sandbox.
|
||||
///
|
||||
/// When `write_restricted` is true, the token gets WRITE_RESTRICTED flag and
|
||||
/// restricting SIDs (logon SID, everyone SID, capability SID), which prevents
|
||||
/// write access except to paths explicitly allowed via DACL ACEs.
|
||||
///
|
||||
/// When `write_restricted` is false (serve mode), the token only gets
|
||||
/// DISABLE_MAX_PRIVILEGE | LUA_TOKEN, allowing child processes to spawn
|
||||
/// grandchildren. File-system write protection is handled by DACL ACEs alone.
|
||||
pub unsafe fn create_restricted_token(
|
||||
psid_capability: *mut c_void,
|
||||
write_restricted: bool,
|
||||
) -> Result<(HANDLE, *mut c_void)> {
|
||||
let base = get_current_token_for_restriction()?;
|
||||
|
||||
let (flags, restricting_count, restricting_sids) = if write_restricted {
|
||||
let mut logon_sid_bytes = get_logon_sid_bytes(base)
|
||||
.map_err(|e| { CloseHandle(base); e })?;
|
||||
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
|
||||
let mut everyone = world_sid().map_err(|e| { CloseHandle(base); e })?;
|
||||
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
|
||||
|
||||
let mut entries: [SID_AND_ATTRIBUTES; 3] = std::mem::zeroed();
|
||||
entries[0].Sid = psid_capability;
|
||||
entries[0].Attributes = 0;
|
||||
entries[1].Sid = psid_logon;
|
||||
entries[1].Attributes = 0;
|
||||
entries[2].Sid = psid_everyone;
|
||||
entries[2].Attributes = 0;
|
||||
|
||||
(DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED, 3, entries)
|
||||
} else {
|
||||
(DISABLE_MAX_PRIVILEGE | LUA_TOKEN, 0, [std::mem::zeroed(); 3])
|
||||
};
|
||||
|
||||
let mut new_token: HANDLE = 0;
|
||||
let ok = CreateRestrictedToken(
|
||||
base,
|
||||
flags,
|
||||
0,
|
||||
std::ptr::null(),
|
||||
0,
|
||||
std::ptr::null(),
|
||||
restricting_count,
|
||||
if restricting_count > 0 { restricting_sids.as_ptr() as *mut SID_AND_ATTRIBUTES } else { std::ptr::null() },
|
||||
&mut new_token,
|
||||
);
|
||||
CloseHandle(base);
|
||||
if ok == 0 {
|
||||
return Err(anyhow::anyhow!("CreateRestrictedToken failed: {}", GetLastError()));
|
||||
}
|
||||
enable_single_privilege(new_token, "SeChangeNotifyPrivilege")
|
||||
.map_err(|e| { CloseHandle(new_token); e })?;
|
||||
Ok((new_token, psid_capability))
|
||||
}
|
||||
46
qimingclaw/crates/windows-sandbox-helper/src/winutil.rs
Normal file
46
qimingclaw/crates/windows-sandbox-helper/src/winutil.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! Win32 utility functions.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::Foundation::{LocalFree, HLOCAL};
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::{
|
||||
FormatMessageW, FORMAT_MESSAGE_ALLOCATE_BUFFER, FORMAT_MESSAGE_FROM_SYSTEM,
|
||||
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
};
|
||||
|
||||
/// Convert an OsStr to a null-terminated wide string (LPWSTR).
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn to_wide<S: AsRef<OsStr>>(s: S) -> Vec<u16> {
|
||||
let mut v: Vec<u16> = s.as_ref().encode_wide().collect();
|
||||
v.push(0);
|
||||
v
|
||||
}
|
||||
|
||||
/// Produce a readable description for a Win32 error code.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn format_last_error(err: i32) -> String {
|
||||
unsafe {
|
||||
let mut buf_ptr: *mut u16 = std::ptr::null_mut();
|
||||
let flags = FORMAT_MESSAGE_ALLOCATE_BUFFER
|
||||
| FORMAT_MESSAGE_FROM_SYSTEM
|
||||
| FORMAT_MESSAGE_IGNORE_INSERTS;
|
||||
let len = FormatMessageW(
|
||||
flags,
|
||||
std::ptr::null(),
|
||||
err as u32,
|
||||
0,
|
||||
(&mut buf_ptr as *mut *mut u16) as *mut u16,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if len == 0 || buf_ptr.is_null() {
|
||||
return format!("Win32 error {}", err);
|
||||
}
|
||||
let slice = std::slice::from_raw_parts(buf_ptr, len as usize);
|
||||
let mut s = String::from_utf16_lossy(slice);
|
||||
s = s.trim().to_string();
|
||||
let _ = LocalFree(buf_ptr as HLOCAL);
|
||||
s
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user