chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
target/
.cargo/

View 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"

View 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"

View 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) };
}

View 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 }
}

View 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(())
}

View 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(),
}
}

View 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(())
}

View 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);
}

View 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, &current_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, &current_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(())
}

View 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)?),
}
}

View 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))
}

View 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
}
}