├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE.txt ├── LICENSE-MIT.txt ├── README.md └── src ├── config.rs ├── detector.rs └── main.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | format: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: dtolnay/rust-toolchain@stable 22 | - name: Check formatting 23 | run: cargo fmt --verbose --check 24 | 25 | check: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: dtolnay/rust-toolchain@stable 30 | - uses: taiki-e/install-action@v2 31 | with: 32 | tool: cargo-all-features 33 | - name: Check all feature combinations 34 | run: cargo check-all-features 35 | 36 | clippy: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: dtolnay/rust-toolchain@stable 41 | - name: Clippy 42 | run: cargo clippy -- -D warnings 43 | 44 | test: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: dtolnay/rust-toolchain@stable 49 | - uses: taiki-e/install-action@v2 50 | with: 51 | tool: cargo-all-features 52 | - name: Test all feature combinations 53 | run: cargo test-all-features 54 | 55 | build: 56 | strategy: 57 | matrix: 58 | toolchain: [stable, beta] 59 | os: [ubuntu-latest, macos-latest, windows-latest] 60 | runs-on: ${{ matrix.os }} 61 | steps: 62 | - uses: actions/checkout@v4 63 | - uses: dtolnay/rust-toolchain@master 64 | with: 65 | toolchain: ${{ matrix.toolchain }} 66 | - uses: taiki-e/install-action@v2 67 | with: 68 | tool: cargo-all-features 69 | - name: Build all feature combinations 70 | run: cargo build-all-features 71 | 72 | verify_msrv: 73 | runs-on: ubuntu-latest 74 | steps: 75 | - uses: actions/checkout@v4 76 | - uses: dtolnay/rust-toolchain@stable 77 | - uses: taiki-e/install-action@v2 78 | with: 79 | tool: cargo-msrv 80 | - name: Verify MSRV 81 | run: cargo msrv verify --all-features 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 49 | dependencies = [ 50 | "anstyle", 51 | "once_cell", 52 | "windows-sys", 53 | ] 54 | 55 | [[package]] 56 | name = "bitflags" 57 | version = "2.9.1" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 60 | 61 | [[package]] 62 | name = "clap" 63 | version = "4.5.39" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 66 | dependencies = [ 67 | "clap_builder", 68 | "clap_derive", 69 | ] 70 | 71 | [[package]] 72 | name = "clap_builder" 73 | version = "4.5.39" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 76 | dependencies = [ 77 | "anstream", 78 | "anstyle", 79 | "clap_lex", 80 | "strsim", 81 | ] 82 | 83 | [[package]] 84 | name = "clap_derive" 85 | version = "4.5.32" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 88 | dependencies = [ 89 | "heck", 90 | "proc-macro2", 91 | "quote", 92 | "syn", 93 | ] 94 | 95 | [[package]] 96 | name = "clap_lex" 97 | version = "0.7.4" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 100 | 101 | [[package]] 102 | name = "colorchoice" 103 | version = "1.0.3" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 106 | 107 | [[package]] 108 | name = "cosmic_ray_detection" 109 | version = "5.1.0" 110 | dependencies = [ 111 | "clap", 112 | "humantime", 113 | "jiff", 114 | "rayon", 115 | "sysinfo", 116 | ] 117 | 118 | [[package]] 119 | name = "crossbeam-deque" 120 | version = "0.8.6" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 123 | dependencies = [ 124 | "crossbeam-epoch", 125 | "crossbeam-utils", 126 | ] 127 | 128 | [[package]] 129 | name = "crossbeam-epoch" 130 | version = "0.9.18" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 133 | dependencies = [ 134 | "crossbeam-utils", 135 | ] 136 | 137 | [[package]] 138 | name = "crossbeam-utils" 139 | version = "0.8.21" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 142 | 143 | [[package]] 144 | name = "either" 145 | version = "1.15.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 148 | 149 | [[package]] 150 | name = "heck" 151 | version = "0.5.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 154 | 155 | [[package]] 156 | name = "humantime" 157 | version = "2.2.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" 160 | 161 | [[package]] 162 | name = "is_terminal_polyfill" 163 | version = "1.70.1" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 166 | 167 | [[package]] 168 | name = "jiff" 169 | version = "0.2.14" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" 172 | dependencies = [ 173 | "jiff-static", 174 | "jiff-tzdb-platform", 175 | "log", 176 | "portable-atomic", 177 | "portable-atomic-util", 178 | "serde", 179 | "windows-sys", 180 | ] 181 | 182 | [[package]] 183 | name = "jiff-static" 184 | version = "0.2.14" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" 187 | dependencies = [ 188 | "proc-macro2", 189 | "quote", 190 | "syn", 191 | ] 192 | 193 | [[package]] 194 | name = "jiff-tzdb" 195 | version = "0.1.4" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" 198 | 199 | [[package]] 200 | name = "jiff-tzdb-platform" 201 | version = "0.1.3" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" 204 | dependencies = [ 205 | "jiff-tzdb", 206 | ] 207 | 208 | [[package]] 209 | name = "libc" 210 | version = "0.2.172" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 213 | 214 | [[package]] 215 | name = "log" 216 | version = "0.4.27" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 219 | 220 | [[package]] 221 | name = "memchr" 222 | version = "2.7.4" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 225 | 226 | [[package]] 227 | name = "ntapi" 228 | version = "0.4.1" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" 231 | dependencies = [ 232 | "winapi", 233 | ] 234 | 235 | [[package]] 236 | name = "objc2-core-foundation" 237 | version = "0.3.1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" 240 | dependencies = [ 241 | "bitflags", 242 | ] 243 | 244 | [[package]] 245 | name = "objc2-io-kit" 246 | version = "0.3.1" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" 249 | dependencies = [ 250 | "libc", 251 | "objc2-core-foundation", 252 | ] 253 | 254 | [[package]] 255 | name = "once_cell" 256 | version = "1.21.3" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 259 | 260 | [[package]] 261 | name = "portable-atomic" 262 | version = "1.11.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 265 | 266 | [[package]] 267 | name = "portable-atomic-util" 268 | version = "0.2.4" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 271 | dependencies = [ 272 | "portable-atomic", 273 | ] 274 | 275 | [[package]] 276 | name = "proc-macro2" 277 | version = "1.0.95" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 280 | dependencies = [ 281 | "unicode-ident", 282 | ] 283 | 284 | [[package]] 285 | name = "quote" 286 | version = "1.0.40" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 289 | dependencies = [ 290 | "proc-macro2", 291 | ] 292 | 293 | [[package]] 294 | name = "rayon" 295 | version = "1.10.0" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 298 | dependencies = [ 299 | "either", 300 | "rayon-core", 301 | ] 302 | 303 | [[package]] 304 | name = "rayon-core" 305 | version = "1.12.1" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 308 | dependencies = [ 309 | "crossbeam-deque", 310 | "crossbeam-utils", 311 | ] 312 | 313 | [[package]] 314 | name = "serde" 315 | version = "1.0.219" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 318 | dependencies = [ 319 | "serde_derive", 320 | ] 321 | 322 | [[package]] 323 | name = "serde_derive" 324 | version = "1.0.219" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 327 | dependencies = [ 328 | "proc-macro2", 329 | "quote", 330 | "syn", 331 | ] 332 | 333 | [[package]] 334 | name = "strsim" 335 | version = "0.11.1" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 338 | 339 | [[package]] 340 | name = "syn" 341 | version = "2.0.101" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 344 | dependencies = [ 345 | "proc-macro2", 346 | "quote", 347 | "unicode-ident", 348 | ] 349 | 350 | [[package]] 351 | name = "sysinfo" 352 | version = "0.35.1" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "79251336d17c72d9762b8b54be4befe38d2db56fbbc0241396d70f173c39d47a" 355 | dependencies = [ 356 | "libc", 357 | "memchr", 358 | "ntapi", 359 | "objc2-core-foundation", 360 | "objc2-io-kit", 361 | "windows", 362 | ] 363 | 364 | [[package]] 365 | name = "unicode-ident" 366 | version = "1.0.18" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 369 | 370 | [[package]] 371 | name = "utf8parse" 372 | version = "0.2.2" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 375 | 376 | [[package]] 377 | name = "winapi" 378 | version = "0.3.9" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 381 | dependencies = [ 382 | "winapi-i686-pc-windows-gnu", 383 | "winapi-x86_64-pc-windows-gnu", 384 | ] 385 | 386 | [[package]] 387 | name = "winapi-i686-pc-windows-gnu" 388 | version = "0.4.0" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 391 | 392 | [[package]] 393 | name = "winapi-x86_64-pc-windows-gnu" 394 | version = "0.4.0" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 397 | 398 | [[package]] 399 | name = "windows" 400 | version = "0.61.1" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" 403 | dependencies = [ 404 | "windows-collections", 405 | "windows-core", 406 | "windows-future", 407 | "windows-link", 408 | "windows-numerics", 409 | ] 410 | 411 | [[package]] 412 | name = "windows-collections" 413 | version = "0.2.0" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" 416 | dependencies = [ 417 | "windows-core", 418 | ] 419 | 420 | [[package]] 421 | name = "windows-core" 422 | version = "0.61.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" 425 | dependencies = [ 426 | "windows-implement", 427 | "windows-interface", 428 | "windows-link", 429 | "windows-result", 430 | "windows-strings", 431 | ] 432 | 433 | [[package]] 434 | name = "windows-future" 435 | version = "0.2.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" 438 | dependencies = [ 439 | "windows-core", 440 | "windows-link", 441 | ] 442 | 443 | [[package]] 444 | name = "windows-implement" 445 | version = "0.60.0" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 448 | dependencies = [ 449 | "proc-macro2", 450 | "quote", 451 | "syn", 452 | ] 453 | 454 | [[package]] 455 | name = "windows-interface" 456 | version = "0.59.1" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 459 | dependencies = [ 460 | "proc-macro2", 461 | "quote", 462 | "syn", 463 | ] 464 | 465 | [[package]] 466 | name = "windows-link" 467 | version = "0.1.1" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 470 | 471 | [[package]] 472 | name = "windows-numerics" 473 | version = "0.2.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" 476 | dependencies = [ 477 | "windows-core", 478 | "windows-link", 479 | ] 480 | 481 | [[package]] 482 | name = "windows-result" 483 | version = "0.3.2" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 486 | dependencies = [ 487 | "windows-link", 488 | ] 489 | 490 | [[package]] 491 | name = "windows-strings" 492 | version = "0.4.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" 495 | dependencies = [ 496 | "windows-link", 497 | ] 498 | 499 | [[package]] 500 | name = "windows-sys" 501 | version = "0.59.0" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 504 | dependencies = [ 505 | "windows-targets", 506 | ] 507 | 508 | [[package]] 509 | name = "windows-targets" 510 | version = "0.52.6" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 513 | dependencies = [ 514 | "windows_aarch64_gnullvm", 515 | "windows_aarch64_msvc", 516 | "windows_i686_gnu", 517 | "windows_i686_gnullvm", 518 | "windows_i686_msvc", 519 | "windows_x86_64_gnu", 520 | "windows_x86_64_gnullvm", 521 | "windows_x86_64_msvc", 522 | ] 523 | 524 | [[package]] 525 | name = "windows_aarch64_gnullvm" 526 | version = "0.52.6" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 529 | 530 | [[package]] 531 | name = "windows_aarch64_msvc" 532 | version = "0.52.6" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 535 | 536 | [[package]] 537 | name = "windows_i686_gnu" 538 | version = "0.52.6" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 541 | 542 | [[package]] 543 | name = "windows_i686_gnullvm" 544 | version = "0.52.6" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 547 | 548 | [[package]] 549 | name = "windows_i686_msvc" 550 | version = "0.52.6" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 553 | 554 | [[package]] 555 | name = "windows_x86_64_gnu" 556 | version = "0.52.6" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 559 | 560 | [[package]] 561 | name = "windows_x86_64_gnullvm" 562 | version = "0.52.6" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 565 | 566 | [[package]] 567 | name = "windows_x86_64_msvc" 568 | version = "0.52.6" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 571 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cosmic_ray_detection" 3 | version = "5.1.0" 4 | edition = "2021" 5 | authors = ["Johanna Sörngård "] 6 | license = "MIT OR Apache-2.0" 7 | rust-version = "1.75.0" 8 | 9 | [dependencies] 10 | clap = { version = "4.5", features = ["derive"] } 11 | humantime = "2.2.0" 12 | jiff = "0.2" 13 | rayon = { version = "1.10", optional = true } 14 | sysinfo = { version = "0.35", default-features = false, features = ["system"] } 15 | 16 | [profile.release-lto] 17 | inherits = "release" 18 | lto = "fat" 19 | strip = "symbols" 20 | codegen-units = 1 21 | 22 | [profile.dev.package."*"] 23 | # Enable optimization of dependencies also in debug mode 24 | opt-level = 3 25 | 26 | [features] 27 | # Enable this feature to run all memory reads and writes in parallel. 28 | rayon = ["dep:rayon", "clap/cargo"] 29 | -------------------------------------------------------------------------------- /LICENSE-APACHE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2025 Johanna Sörngård 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Johanna Sörngård 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Your computer can double up as a cosmic ray detector. Yes, really!** 2 | 3 | [Cosmic rays](https://en.wikipedia.org/wiki/Cosmic_ray) hit your computer all the time. If they hit the RAM, this can [sometimes cause disturbances](https://en.wikipedia.org/wiki/Soft_error#Cosmic_rays_creating_energetic_neutrons_and_protons), like flipping a random bit in memory. 4 | To use your computer as a cosmic ray detector, simply run this program! 5 | The detection works by allocating a vector of zeroed bytes and then checking regularly to see if they are all still zero. Ta-da! 6 | 7 | * Do not run this on a computer with [ECC memory](https://en.wikipedia.org/wiki/ECC_memory), as that will prevent the data corruption we are trying to detect! 8 | * The chance of detection increases with the physical size of your DRAM modules and the percentage of them you allocate to this program. 9 | * Beware of operating systems being clever, and e.g. compressing unused memory pages or swapping them to disk. A vector of nothing but zeros that hasn't been used in a while is an excellent target for this. This will shrink your detector! 10 | * Expect detections to be *very* rare. 11 | 12 | It may also not work on DDR5 memory modules and later as those contain onboard ECC. 13 | 14 | **Special thanks to** 15 | * /u/csdt0 and /u/HeroicKatora on reddit for ideas about how to improve the correctness of the program and avoid the pitfalls of virtual memory. 16 | 17 |
18 | 19 | ### License 20 | 21 | 22 | Licensed under either of Apache License, Version 23 | 2.0 or MIT license at your option. 24 | 25 | 26 |
27 | 28 | 29 | Unless you explicitly state otherwise, any contribution intentionally submitted 30 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 31 | dual licensed as above, without any additional terms or conditions. 32 | 33 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Johanna Sörngård 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | #[cfg(feature = "rayon")] 5 | use clap::crate_version; 6 | #[cfg(all(not(target_os = "windows"), not(target_os = "freebsd")))] 7 | use clap::ValueEnum; 8 | use clap::{ArgGroup, Parser}; 9 | use core::{num::NonZeroUsize, time::Duration}; 10 | 11 | const DEFAULT_DELAY: &str = "30s"; 12 | 13 | #[cfg(all(not(target_os = "windows"), not(target_os = "freebsd")))] 14 | #[derive(Debug, Clone, Copy, ValueEnum)] 15 | pub enum AllocationMode { 16 | Available, 17 | Free, 18 | } 19 | 20 | #[cfg(feature = "rayon")] 21 | const LONG_VERSION: &str = concat!(crate_version!(), "\nparallelization enabled"); 22 | #[cfg(not(feature = "rayon"))] 23 | const LONG_VERSION: Option<&str> = None; 24 | 25 | /// Monitors memory for bit-flips. 26 | /// Won't work on ECC memory, and may not work on DDR5 memory modules and later since they contain onboard ECC. 27 | /// The chance of detection scales with the physical size of your DRAM modules 28 | /// and the percentage of them you allocate to this program. 29 | #[derive(Parser, Debug)] 30 | #[clap(author, version, about, long_about = None, long_version = LONG_VERSION)] 31 | #[clap(group( 32 | ArgGroup::new("detector memory size") 33 | .required(true) 34 | .args(&["memory_to_monitor", "use_all"]) 35 | ))] 36 | pub struct Cli { 37 | #[arg(short, long, value_parser(parse_memory_string))] 38 | /// The size of the memory to monitor for bit flips, understands e.g. 200, 5kB, 2GB and 3Mb. 39 | /// If no suffix is given the program will assume that the given number is the number of bytes to monitor. 40 | pub memory_to_monitor: Option, 41 | 42 | // There is a difference between free and available memory, 43 | // and on most operating systems we can detect this difference. 44 | // This option lets the user specify which alternative they mean. 45 | #[cfg(all(not(target_os = "windows"), not(target_os = "freebsd")))] 46 | #[arg(short = 'a', long, value_enum, value_name = "ALLOCATION_MODE")] 47 | /// Allocate as much memory as possible to the detector. 48 | /// If "free" is specified the program will allocate all currently unused memory, 49 | /// while if "available" is specified the program will also try to eject things that sit in memory 50 | /// but haven't been used in a while. 51 | pub use_all: Option, 52 | 53 | // On Windows and FreeBSD sysinfo has no way to differentiate free and available memory, 54 | // so we just allocate as much as the OS gives us. 55 | #[cfg(any(target_os = "windows", target_os = "freebsd"))] 56 | #[arg(short = 'a', long)] 57 | /// Allocate as much memory as possible to the detector. 58 | pub use_all: bool, 59 | 60 | #[arg(short, value_parser = parse_delay_string, default_value = DEFAULT_DELAY)] 61 | /// The delay in between each integrity check. 62 | /// If a bitflip occurs it will not be detected until the next integrity check. 63 | pub delay: Duration, 64 | 65 | #[arg(short, long)] 66 | /// Print extra information. 67 | pub verbose: bool, 68 | 69 | #[arg(short, long)] 70 | /// Don't print any carriage returns in the output. 71 | /// This results in a better format for logging to a file. 72 | pub log_format: bool, 73 | 74 | #[cfg(feature = "rayon")] 75 | #[arg(short, long)] 76 | /// The number of parallel jobs to run when writing and reading the detector memory. 77 | /// If this is not set the number of jobs will be set to the number of logical cores. 78 | pub jobs: Option, 79 | } 80 | 81 | /// Parses a string describing a number of bytes into an integer. 82 | /// The string can use common SI prefixes as well, like '4GB' or '30kB'. 83 | fn parse_memory_string(size_string: &str) -> Result { 84 | if let Ok(t) = size_string.parse() { 85 | // The input was a number, interpret it as the number of bytes if nonzero. 86 | NonZeroUsize::new(t).ok_or_else(|| "zero is not a valid value".to_owned()) 87 | } else { 88 | // The input was more than just an integer 89 | 90 | // We begin by splitting the string into the number and the suffix. 91 | let (number, suffix) = match size_string 92 | .chars() 93 | .position(|c| !c.is_ascii_digit() && c != '.') 94 | { 95 | Some(index) => Ok(size_string.split_at(index)), 96 | None => Err("you need to specify a suffix to use non-integer numbers".to_owned()), 97 | }?; 98 | 99 | // Parse the number part 100 | let mut num_bytes: f64 = number 101 | .parse() 102 | .map_err(|_| format!("could not interpret '{number}' as a number"))?; 103 | 104 | if suffix.len() > 2 { 105 | return Err("the suffix can be at most two letters long".to_owned()); 106 | } 107 | 108 | let mut chars = suffix.chars().rev(); 109 | 110 | if let Some(ending) = chars.next() { 111 | match ending { 112 | 'B' => { 113 | if let Some(si_prefix) = chars.next() { 114 | num_bytes *= parse_si_prefix(si_prefix)?; 115 | } 116 | } 117 | 'b' => { 118 | if let Some(si_prefix) = chars.next() { 119 | num_bytes *= parse_si_prefix(si_prefix)?; 120 | } 121 | num_bytes /= 8.0; 122 | } 123 | _ => { 124 | return Err(format!( 125 | "the suffix must end with either 'B' or 'b', not '{ending}'" 126 | )); 127 | } 128 | } 129 | } 130 | 131 | if num_bytes.fract() != 0.0 { 132 | return Err("the size must be an integer number of bytes".to_owned()); 133 | } 134 | 135 | NonZeroUsize::new(num_bytes as usize) 136 | .ok_or_else(|| "the size must be at least one byte".to_owned()) 137 | } 138 | } 139 | 140 | fn parse_si_prefix(c: char) -> Result { 141 | match c { 142 | 'k' => Ok(1e3), 143 | 'M' => Ok(1e6), 144 | 'G' => Ok(1e9), 145 | 'T' => Ok(1e12), 146 | // Values higher than this one should not be needed, but are included for completeness. 147 | 'P' => Ok(1e15), 148 | 'E' => Ok(1e18), 149 | 'Z' => Ok(1e21), 150 | 'Y' => Ok(1e24), 151 | _ => Err(format!("'{c}' is not a supported SI prefix")), 152 | } 153 | } 154 | 155 | fn parse_delay_string(s: &str) -> Result { 156 | match s.parse::() { 157 | Ok(d) => Ok(d.into()), 158 | Err(e) => Err(e.to_string()), 159 | } 160 | } 161 | 162 | #[cfg(test)] 163 | mod test { 164 | use super::*; 165 | 166 | #[test] 167 | fn verify_cli() { 168 | use clap::CommandFactory; 169 | Cli::command().debug_assert() 170 | } 171 | 172 | #[test] 173 | fn check_memory_parsing() { 174 | for s in (0..10).map(|i| 2_usize.pow(i)) { 175 | assert_eq!(parse_memory_string(&format!("{s}")).unwrap().get(), s); 176 | assert_eq!( 177 | parse_memory_string(&format!("{s}kB")).unwrap().get(), 178 | s * 1000 179 | ); 180 | assert_eq!( 181 | parse_memory_string(&format!("{s}MB")).unwrap().get(), 182 | s * 1000000 183 | ); 184 | assert_eq!( 185 | parse_memory_string(&format!("{s}GB")).unwrap().get(), 186 | s * 1000000000 187 | ); 188 | assert_eq!( 189 | parse_memory_string(&format!("{s}TB")).unwrap().get(), 190 | s * 1000000000000 191 | ); 192 | assert_eq!( 193 | parse_memory_string(&format!("{s}PB")).unwrap().get(), 194 | s * 1000000000000000 195 | ); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/detector.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Johanna Sörngård 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::ptr::{read_volatile, write_volatile}; 5 | 6 | #[cfg(all(not(target_os = "windows"), not(target_os = "freebsd")))] 7 | use crate::config::AllocationMode; 8 | 9 | #[cfg(feature = "rayon")] 10 | use rayon::prelude::{ 11 | IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator, 12 | }; 13 | 14 | use sysinfo::{MemoryRefreshKind, RefreshKind, System}; 15 | 16 | /// In order to prevent the optimizer from removing the reads of the memory that make up the detector 17 | /// this struct will only use volatile reads and writes to its memory. 18 | pub struct Detector { 19 | default: u8, 20 | detector_mass: Box<[u8]>, 21 | } 22 | 23 | impl Detector { 24 | pub fn new(default: u8, capacity_bytes: usize) -> Self { 25 | Detector { 26 | default, 27 | detector_mass: (0..capacity_bytes).map(|_| default).collect(), 28 | } 29 | } 30 | 31 | #[cfg(any(target_os = "windows", target_os = "freebsd"))] 32 | /// Creates a new detector that fills up as much memory as possible. 33 | pub fn new_with_maximum_size(default: u8) -> Self { 34 | // Know this is supported on windows. 35 | let s = System::new_with_specifics( 36 | RefreshKind::nothing().with_memory(MemoryRefreshKind::nothing().with_ram()), 37 | ); 38 | let capacity_bytes = usize::try_from(s.available_memory()).unwrap_or(usize::MAX); 39 | 40 | Detector { 41 | default, 42 | detector_mass: (0..capacity_bytes).map(|_| default).collect(), 43 | } 44 | } 45 | 46 | #[cfg(all(not(target_os = "windows"), not(target_os = "freebsd")))] 47 | /// Creates a new detector that fills up as much memory as possible in the specified way. 48 | /// # Panic 49 | /// Panics if this function is called on an operating system that is not supported by [sysinfo](https://crates.io/crates/sysinfo). 50 | pub fn new_with_maximum_size_in_mode(default: u8, mode: AllocationMode) -> Self { 51 | if !sysinfo::IS_SUPPORTED_SYSTEM { 52 | panic!("{} is not supported by the mechanism this program uses to determine available memory, please specify it manually", std::env::consts::OS); 53 | } 54 | 55 | let s = System::new_with_specifics( 56 | RefreshKind::nothing().with_memory(MemoryRefreshKind::nothing().with_ram()), 57 | ); 58 | let capacity_bytes = usize::try_from(match mode { 59 | AllocationMode::Available => s.available_memory(), 60 | AllocationMode::Free => s.free_memory(), 61 | }) 62 | .unwrap_or(usize::MAX); 63 | 64 | Detector { 65 | default, 66 | detector_mass: (0..capacity_bytes).map(|_| default).collect(), 67 | } 68 | } 69 | 70 | /// Returns the allocated memory size of the detector in bytes. 71 | pub fn len(&self) -> usize { 72 | self.detector_mass.len() 73 | } 74 | 75 | /// Checks if every element of the detector memory is equal to the default value. 76 | pub fn is_intact(&self) -> bool { 77 | self.position_and_value_of_changed_element().is_none() 78 | } 79 | 80 | /// Returns the default value of the detector. This is what gets written to 81 | /// every byte when [`reset`](Detector::reset) is called. 82 | pub const fn default(&self) -> u8 { 83 | self.default 84 | } 85 | 86 | /// Writes the given value to every element of the detector memory. 87 | pub fn write(&mut self, value: u8) { 88 | #[cfg(feature = "rayon")] 89 | self.detector_mass 90 | .par_iter_mut() 91 | .for_each(|n| unsafe { write_volatile(n, value) }); 92 | 93 | #[cfg(not(feature = "rayon"))] 94 | self.detector_mass 95 | .iter_mut() 96 | .for_each(|n| unsafe { write_volatile(n, value) }); 97 | } 98 | 99 | /// If an element in the detector does not match its default value, return its index. 100 | pub fn position_of_changed_element(&self) -> Option { 101 | #[cfg(feature = "rayon")] 102 | return self 103 | .detector_mass 104 | .par_iter() 105 | .position_any(|r| unsafe { read_volatile(r) != self.default }); 106 | 107 | #[cfg(not(feature = "rayon"))] 108 | self.detector_mass 109 | .iter() 110 | .position(|r| unsafe { read_volatile(r) != self.default }) 111 | } 112 | 113 | /// If an element in the detector does not match its default value, return its index and value. 114 | pub fn position_and_value_of_changed_element(&self) -> Option<(usize, u8)> { 115 | match self.position_of_changed_element() { 116 | Some(i) => self.get(i).map(|v| (i, v)), 117 | None => None, 118 | } 119 | } 120 | 121 | /// Resets the detector to its default value. 122 | pub fn reset(&mut self) { 123 | if self.default == 0 { 124 | // Just writing zero to memory pages might not prompt the OS to actually allocate them. 125 | // This is relevant the first time the detector is reset, and if the OS has moved 126 | // some pages to swap. 127 | self.write(42); 128 | } 129 | self.write(self.default); 130 | } 131 | 132 | /// Returns the value of the element at the given index, if it exists. 133 | pub fn get(&self, index: usize) -> Option { 134 | self.detector_mass 135 | .get(index) 136 | .map(|reference| unsafe { read_volatile(reference) }) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Johanna Sörngård 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use std::{ 5 | error::Error, 6 | io::{stdout, Write}, 7 | thread::sleep, 8 | time::Instant, 9 | }; 10 | 11 | use clap::Parser; 12 | use humantime::format_duration; 13 | use jiff::Zoned; 14 | 15 | mod config; 16 | mod detector; 17 | 18 | #[cfg(all(not(target_os = "windows"), not(target_os = "freebsd")))] 19 | use crate::config::AllocationMode; 20 | 21 | use crate::{config::Cli, detector::Detector}; 22 | 23 | fn main() -> Result<(), Box> { 24 | let conf = Cli::parse(); 25 | 26 | let verbose: bool = conf.verbose; 27 | let sleep_duration = conf.delay; 28 | let cr = !conf.log_format; 29 | 30 | if verbose { 31 | println!("\n------------ Runtime settings ------------"); 32 | println!( 33 | "Using {} as detector", 34 | match conf.memory_to_monitor { 35 | Some(s) => format!("{} bytes", s.get()), 36 | #[cfg(all(not(target_os = "windows"), not(target_os = "freebsd")))] 37 | None => match conf.use_all.expect("this only happens if -m wasn't specified, and either -m or --use-all must be specified at the CLI level") { 38 | AllocationMode::Available => "as much memory as possible", 39 | AllocationMode::Free => "all unused memory", 40 | } 41 | .to_owned(), 42 | #[cfg(any(target_os = "windows", target_os = "freebsd"))] 43 | None => "as much memory as possible".to_owned(), 44 | } 45 | ); 46 | 47 | println!( 48 | "Waiting {} between integrity checks", 49 | format_duration(sleep_duration) 50 | ); 51 | 52 | println!("------------------------------------------\n"); 53 | 54 | print!("Allocating detector memory..."); 55 | stdout().flush()?; 56 | } 57 | 58 | #[cfg(feature = "rayon")] 59 | if let Some(jobs) = conf.jobs { 60 | rayon::ThreadPoolBuilder::new() 61 | .num_threads(jobs.into()) 62 | .build_global() 63 | .map_err(|e| Box::new(e))?; 64 | } 65 | 66 | // Instead of building a detector out of scintillators and photo-multiplier tubes, 67 | // we just allocate some memory on this here computer. 68 | let mut detector = match conf.memory_to_monitor { 69 | Some(s) => Detector::new(0, s.get()), 70 | #[cfg(any(target_os = "windows", target_os = "freebsd"))] 71 | None => Detector::new_with_maximum_size(0), 72 | #[cfg(all(not(target_os = "windows"), not(target_os = "freebsd")))] 73 | None => Detector::new_with_maximum_size_in_mode(0, conf.use_all.expect("this only happens if -m wasn't specified, and either -m or --use-all must be specified at the CLI level")), 74 | }; 75 | // Less exciting, much less accurate and sensitive, but much cheaper 76 | 77 | if verbose { 78 | print!(" done"); 79 | if conf.memory_to_monitor.is_none() { 80 | print!(" with allocation of {} bytes", detector.len()); 81 | } 82 | println!("\nBeginning detection loop"); 83 | } 84 | 85 | let mut checks: u64 = 1; 86 | let mut memory_is_intact: bool; 87 | let start: Instant = Instant::now(); 88 | loop { 89 | // Reset detector! 90 | if verbose { 91 | print!("Zeroing detector memory... "); 92 | stdout().flush()?; 93 | } 94 | detector.reset(); 95 | memory_is_intact = true; 96 | 97 | // Some feedback for the user that the program is still running 98 | if verbose { 99 | println!("done"); 100 | print!("Waiting for first check"); 101 | if conf.log_format { 102 | println!(); 103 | } else { 104 | stdout().flush()?; 105 | } 106 | } 107 | 108 | while memory_is_intact { 109 | // We're not gonna miss any events by being too slow 110 | sleep(sleep_duration); 111 | // Check if all the bytes are still zero 112 | memory_is_intact = detector.is_intact(); 113 | if memory_is_intact { 114 | if cr { 115 | print!("\r") 116 | } 117 | print!("Passed integrity check number {checks} at {}", Zoned::now()); 118 | if !cr { 119 | println!(); 120 | } 121 | stdout().flush()?; 122 | } 123 | checks += 1; 124 | } 125 | 126 | println!( 127 | "\nDetected a bitflip after {} on integrity check number {checks} at {}", 128 | humantime::Duration::from(start.elapsed()), 129 | Zoned::now(), 130 | ); 131 | 132 | match detector.position_and_value_of_changed_element() { 133 | Some((index, value)) => println!( 134 | "The byte at index {index} flipped from {} to {value}", 135 | detector.default(), 136 | ), 137 | None => println!( 138 | "The same bit flipped back before we could find which one it was! Incredible!" 139 | ), 140 | } 141 | } 142 | } 143 | --------------------------------------------------------------------------------