From 439a58f8a263024e923c91f93c63fca54d208286 Mon Sep 17 00:00:00 2001 From: Joe Ranweiler Date: Wed, 11 Nov 2020 04:20:38 -0800 Subject: [PATCH] Add uninstrumented block coverage recording (#280) --- src/agent/Cargo.lock | 255 ++++++--- src/agent/Cargo.toml | 1 + src/agent/coverage/Cargo.toml | 13 +- src/agent/coverage/examples/block_coverage.rs | 68 +++ src/agent/coverage/src/block.rs | 59 +++ src/agent/coverage/src/block/linux.rs | 486 ++++++++++++++++++ src/agent/coverage/src/block/windows.rs | 176 +++++++ src/agent/coverage/src/lib.rs | 12 +- src/agent/debugger/src/debugger.rs | 2 +- 9 files changed, 995 insertions(+), 77 deletions(-) create mode 100644 src/agent/coverage/examples/block_coverage.rs create mode 100644 src/agent/coverage/src/block.rs create mode 100644 src/agent/coverage/src/block/linux.rs create mode 100644 src/agent/coverage/src/block/windows.rs diff --git a/src/agent/Cargo.lock b/src/agent/Cargo.lock index a801f8c10..a335c111e 100644 --- a/src/agent/Cargo.lock +++ b/src/agent/Cargo.lock @@ -16,10 +16,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" [[package]] -name = "aho-corasick" -version = "0.7.14" +name = "adler32" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b476ce7103678b0c6d3d395dbbae31d48ff910bd28be979ba5d48c6351131d0d" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" dependencies = [ "memchr", ] @@ -35,9 +41,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c" +checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7" [[package]] name = "appinsights" @@ -98,9 +104,9 @@ dependencies = [ [[package]] name = "async-io" -version = "1.1.10" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54bc4c1c7292475efb2253227dbcfad8fe1ca4c02bc62c510cc2f3da5c4704e" +checksum = "40a0b2bb8ae20fede194e779150fe283f65a4a08461b496de546ec366b174ad9" dependencies = [ "concurrent-queue", "fastrand", @@ -127,15 +133,15 @@ dependencies = [ [[package]] name = "async-std" -version = "1.6.5" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9fa76751505e8df1c7a77762f60486f60c71bbd9b8557f4da6ad47d083732ed" +checksum = "a7e82538bc65a25dbdff70e4c5439d52f068048ab97cdea0acd73f131594caa1" dependencies = [ "async-global-executor", "async-io", "async-mutex", "blocking", - "crossbeam-utils 0.7.2", + "crossbeam-utils 0.8.0", "futures-channel", "futures-core", "futures-io", @@ -299,9 +305,9 @@ checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" [[package]] name = "cc" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d" +checksum = "f1770ced377336a88a67c473594ccc14eca6f4559217c34f64aac8f83d641b40" [[package]] name = "cfg-if" @@ -354,15 +360,15 @@ dependencies = [ [[package]] name = "const_fn" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce90df4c658c62f12d78f7508cf92f9173e5184a539c10bfe54a3107b3ffd0f2" +checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab" [[package]] name = "core-foundation" -version = "0.7.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" dependencies = [ "core-foundation-sys", "libc", @@ -370,9 +376,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" [[package]] name = "coverage" @@ -380,12 +386,17 @@ version = "0.1.0" dependencies = [ "anyhow", "bincode", + "debugger", + "env_logger 0.8.1", "fixedbitset", "goblin", "iced-x86", "log", "memmap", + "object", "pdb", + "pete", + "procfs", "serde", "uuid", "winapi 0.3.9", @@ -407,6 +418,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "crossbeam-channel" version = "0.4.4" @@ -555,7 +575,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "atty", - "humantime", + "humantime 1.3.0", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54532e3223c5af90a6a757c90b5c5521564b07e5e7a958681bcd2afad421cdcd" +dependencies = [ + "atty", + "humantime 2.0.1", "log", "regex", "termcolor", @@ -617,11 +650,11 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed85775dcc68644b5c950ac06a2b23768d3bc9390464151aaf27136998dcf9e" +checksum = "0c122a393ea57648015bf06fbd3d372378992e86b9ff5a7a497b076a28c79efe" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libc", "redox_syscall", "winapi 0.3.9", @@ -633,6 +666,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e08c8bc7575d7e091fe0706963bd22e2a4be6a64da995f03b2a5a57d66ad015" +[[package]] +name = "flate2" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -681,6 +726,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7684cf33bb7f28497939e8c7cf17e3e4e3b8d9a0080ffa4f8ae2f515442ee855" +[[package]] +name = "form_urlencoded" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "fsevent" version = "0.4.0" @@ -718,9 +773,9 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] name = "futures" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95314d38584ffbfda215621d723e0a3906f032e03ae5551e650058dac83d4797" +checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0" dependencies = [ "futures-channel", "futures-core", @@ -733,9 +788,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0448174b01148032eed37ac4aed28963aaaa8cfa93569a08e5b479bbc6c2c151" +checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" dependencies = [ "futures-core", "futures-sink", @@ -743,15 +798,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18eaa56102984bed2c88ea39026cff3ce3b4c7f508ca970cedf2450ea10d4e46" +checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" [[package]] name = "futures-executor" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f8e0c9258abaea85e78ebdda17ef9666d390e987f006be6080dfe354b708cb" +checksum = "4caa2b2b68b880003057c1dd49f1ed937e38f22fcf6c212188a121f08cf40a65" dependencies = [ "futures-core", "futures-task", @@ -760,9 +815,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1798854a4727ff944a7b12aa999f58ce7aa81db80d2dfaaf2ba06f065ddd2b" +checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb" [[package]] name = "futures-lite" @@ -781,9 +836,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36fccf3fc58563b4a14d265027c627c3b665d7fed489427e88e7cc929559efe" +checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" dependencies = [ "proc-macro-hack", "proc-macro2 1.0.24", @@ -793,24 +848,24 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e3ca3f17d6e8804ae5d3df7a7d35b2b3a6fe89dac84b31872720fc3060a0b11" +checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d" [[package]] name = "futures-task" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d502af37186c4fef99453df03e374683f8a1eec9dcc1e66b3b82dc8278ce3c" +checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d" dependencies = [ "once_cell", ] [[package]] name = "futures-util" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abcb44342f62e6f3e8ac427b8aa815f724fd705dfad060b18ac7866c15bb8e34" +checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2" dependencies = [ "futures-channel", "futures-core", @@ -987,10 +1042,16 @@ dependencies = [ ] [[package]] -name = "hyper" -version = "0.13.8" +name = "humantime" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3afcfae8af5ad0576a31e768415edb627824129e8e5a29b8bfccb2f234e835" +checksum = "3c1ad908cc71012b7bea4d0c53ba96a8cba9962f048fa68d143376143d863b7a" + +[[package]] +name = "hyper" +version = "0.13.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ad767baac13b44d4529fcf58ba2cd0995e36e7b435bc5b039de6f47e880dbf" dependencies = [ "bytes", "futures-channel", @@ -1002,7 +1063,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project 0.4.27", + "pin-project 1.0.1", "socket2", "tokio", "tower-service", @@ -1068,9 +1129,9 @@ dependencies = [ [[package]] name = "inotify-sys" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0" +checksum = "c4563555856585ab3180a5bf0b2f9f8d301a728462afffc8195b3f5394229c55" dependencies = [ "libc", ] @@ -1169,6 +1230,24 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +[[package]] +name = "libflate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389de7875e06476365974da3e7ff85d55f1972188ccd9f6020dd7c8156e17914" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", + "rle-decode-fast", +] + +[[package]] +name = "libflate_lz77" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b" + [[package]] name = "libproc" version = "0.3.2" @@ -1344,9 +1423,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d" +checksum = "1a1cda389c26d6b88f3d2dc38aa1b750fe87d298cc5d795ec9e975f402f00372" dependencies = [ "lazy_static", "libc", @@ -1467,6 +1546,10 @@ name = "object" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" +dependencies = [ + "flate2", + "wasmparser", +] [[package]] name = "once_cell" @@ -1526,7 +1609,7 @@ dependencies = [ "appinsights", "async-trait", "clap", - "env_logger", + "env_logger 0.7.1", "futures", "hex", "lazy_static", @@ -1554,7 +1637,7 @@ dependencies = [ "async-trait", "clap", "downcast-rs", - "env_logger", + "env_logger 0.7.1", "futures", "log", "onefuzz", @@ -1750,9 +1833,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "proc-macro-error" @@ -1832,6 +1915,21 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "procfs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a336c8310f4955f343935b9c11a30254d1ad8fad98ec257a4407a061a6fd49" +dependencies = [ + "bitflags", + "byteorder", + "chrono", + "hex", + "lazy_static", + "libc", + "libflate", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -2018,10 +2116,16 @@ dependencies = [ ] [[package]] -name = "rstack" -version = "0.3.1" +name = "rle-decode-fast" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85dc9d336d64dd0210b24a981da1a972801f1b262d894c4eb2feeed72fd3d774" +checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" + +[[package]] +name = "rstack" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db35178d78a6c680852a6b48b089100d038c82fde25283a77ee923c3f6f90bd" dependencies = [ "cfg-if 0.1.10", "libc", @@ -2097,9 +2201,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "0.4.4" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535" +checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" dependencies = [ "bitflags", "core-foundation", @@ -2110,9 +2214,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "0.4.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405" +checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" dependencies = [ "core-foundation-sys", "libc", @@ -2190,12 +2294,12 @@ dependencies = [ [[package]] name = "sha2" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2933378ddfeda7ea26f48c555bdad8bb446bf8a3d17832dc83e380d444cfb8c1" +checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8" dependencies = [ "block-buffer", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "cpuid-bool", "digest", "opaque-debug", @@ -2408,18 +2512,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "318234ffa22e0920fe9a40d7b8369b5f649d490980cf7aadcf1eb91594869b42" +checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cae2447b6282786c3493999f40a9be2a6ad20cb8bd268b0a0dbf5a065535c0ab" +checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", @@ -2620,9 +2724,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "unwind" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89784c377152a6b8a4ecdf1e2a0fc15345773a552c1752ffd2365149aa8d79ca" +checksum = "b737c38ba258c25916dd4002b12e631b180b6ea63528147a94d6364f68d886df" dependencies = [ "foreign-types 0.5.0", "libc", @@ -2631,9 +2735,9 @@ dependencies = [ [[package]] name = "unwind-sys" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd1c4a6d1cfe0072924d1b1d4ca6faa211c95056666979d7ef1bab4cd206057f" +checksum = "e72a873f034625fa57b8c2c16e14f2a44dc93c08fe330e81405b72d0aa42b2ff" dependencies = [ "libc", "pkg-config", @@ -2641,10 +2745,11 @@ dependencies = [ [[package]] name = "url" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" +checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" dependencies = [ + "form_urlencoded", "idna", "matches", "percent-encoding", @@ -2805,6 +2910,12 @@ version = "0.2.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" +[[package]] +name = "wasmparser" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fddd575d477c6e9702484139cf9f23dcd554b06d185ed0f56c857dd3a47aa6" + [[package]] name = "web-sys" version = "0.3.45" diff --git a/src/agent/Cargo.toml b/src/agent/Cargo.toml index fb2b8af7e..b2e876cd0 100644 --- a/src/agent/Cargo.toml +++ b/src/agent/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "atexit", + "coverage", "debugger", "input-tester", "onefuzz", diff --git a/src/agent/coverage/Cargo.toml b/src/agent/coverage/Cargo.toml index bf1d6f7e8..6a89da265 100644 --- a/src/agent/coverage/Cargo.toml +++ b/src/agent/coverage/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" [dependencies] anyhow = "1.0" bincode = "1.3" +debugger = { path = "../debugger" } fixedbitset = "0.3" goblin = "0.2" iced-x86 = { version = "1.1", features = ["decoder", "op_code_info", "instr_info", "masm"] } @@ -16,6 +17,14 @@ memmap = "0.7" serde = { version = "1.0", features = ["derive"] } uuid = { version = "0.8", features = ["guid"] } -[target.'cfg(windows)'.dependencies] +[target.'cfg(target_os = "windows")'.dependencies] pdb = "0.6" -winapi = "0.3" \ No newline at end of file +winapi = "0.3" + +[target.'cfg(target_os = "linux")'.dependencies] +object = "0.22" +pete = "0.3" +procfs = "0.8" + +[dev-dependencies] +env_logger = "0.8" \ No newline at end of file diff --git a/src/agent/coverage/examples/block_coverage.rs b/src/agent/coverage/examples/block_coverage.rs new file mode 100644 index 000000000..93797a79a --- /dev/null +++ b/src/agent/coverage/examples/block_coverage.rs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::env; + +use anyhow::Result; + +#[cfg(target_os = "windows")] +fn main() -> Result<()> { + use std::process::Command; + + env_logger::init(); + + let mut args = env::args().skip(1); + let exe = args.next().unwrap(); + let args: Vec<_> = args.collect(); + + let mut cmd = Command::new(exe); + cmd.args(&args); + + let coverage = coverage::block::windows::record(cmd)?; + let hit = coverage.count_blocks_hit(); + let found = coverage.count_blocks(); + let percent = 100.0 * (hit as f64) / (found as f64); + + log::info!("block coverage = {}/{} ({:.2}%)", hit, found, percent); + + Ok(()) +} + +#[cfg(target_os = "linux")] +fn main() -> Result<()> { + use coverage::block::linux::record; + use pete::Command; + + env_logger::init(); + + let argv = env::args().skip(1).collect(); + let cmd = Command::new(argv)?; + + let coverage = record(cmd)?; + + for m in coverage.modules.values() { + let mut hit = 0; + let mut found = 0; + + let name = m.module.file_name().unwrap().to_string_lossy(); + + log::info!("{}", m.module.display()); + + for b in m.blocks.values() { + found += 1; + + if b.count > 0 { + hit += 1; + }; + + let marker = if b.count == 0 { " " } else { "x" }; + + log::debug!(" [{}] {}+{:x}", marker, name, b.offset); + } + + let percent = 100.0 * (hit as f64) / (found as f64); + log::info!("block coverage = {}/{} ({:.2}%)", hit, found, percent); + } + + Ok(()) +} diff --git a/src/agent/coverage/src/block.rs b/src/agent/coverage/src/block.rs new file mode 100644 index 000000000..9d17162f4 --- /dev/null +++ b/src/agent/coverage/src/block.rs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use serde::{Deserialize, Serialize}; + +#[cfg(target_os = "linux")] +pub mod linux; + +#[cfg(target_os = "windows")] +pub mod windows; + +use std::collections::BTreeMap; +use std::path::PathBuf; + +/// Block coverage for a module. +/// +/// May describe coverage relative to a single input or corpus of inputs. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ModuleCov { + /// Absolute path to the module on the filesystem where coverage was recorded. + pub module: PathBuf, + + /// Mapping from basic block module-relative offsets to block coverage info. + pub blocks: BTreeMap, +} + +impl ModuleCov { + pub fn new(module: impl Into, blocks: impl IntoIterator) -> Self { + let module = module.into(); + let blocks = blocks.into_iter().map(|o| (o, BlockCov::new(o))).collect(); + + Self { module, blocks } + } +} + +/// Coverage info for a specific block, identified by its offset. +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct BlockCov { + /// Offset of the block, relative to the module base load address. + pub offset: u64, + + /// Number of times a block was seen to be executed, relative to some input + /// or corpus. + /// + /// Right now, we only set one-shot breakpoints, so the max `count` for a + /// single input is 1. In this usage, if we measure corpus block coverage + /// with `sum()` as the aggregation function, then `count` / `corpus.len()` + /// tells us the proportion of corpus inputs that cover a block. + /// + /// If we reset breakpoints and recorded multiple block hits per input, then + /// the corpus semantics would depend on the aggregation function. + pub count: u32, +} + +impl BlockCov { + pub fn new(offset: u64) -> Self { + Self { offset, count: 0 } + } +} diff --git a/src/agent/coverage/src/block/linux.rs b/src/agent/coverage/src/block/linux.rs new file mode 100644 index 000000000..2dc42f449 --- /dev/null +++ b/src/agent/coverage/src/block/linux.rs @@ -0,0 +1,486 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::{BTreeMap, BTreeSet}; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +use anyhow::{format_err, Result}; +use iced_x86::{Decoder, DecoderOptions, Instruction}; +use object::endian::LittleEndian as LE; +use object::{read::elf, Object, ObjectSection, ObjectSegment, ObjectSymbol}; +use pete::{Command, Ptracer, Restart, Signal, Stop, Tracee}; +use procfs::process::{MMapPath, MemoryMap, Process}; +use serde::{Deserialize, Serialize}; + +use crate::block::ModuleCov; + +pub fn record(cmd: Command) -> Result { + let mut recorder = Recorder::default(); + recorder.record(cmd)?; + Ok(recorder.coverage) +} + +#[derive(Default, Debug)] +pub struct Recorder { + breakpoints: Breakpoints, + coverage: CommandBlockCov, + images: Option, +} + +impl Recorder { + pub fn record(&mut self, cmd: Command) -> Result<()> { + use pete::ptracer::Options; + + let mut ptracer = Ptracer::new(); + + // Attach stop. + let mut tracee = ptracer.spawn(cmd)?; + + ptracer.restart(tracee, Restart::Continue)?; + + // Continue until `exec()`. + while let Some(t) = ptracer.wait()? { + if let Stop::Exec(..) = t.stop { + tracee = t; + break; + } + + ptracer.restart(tracee, Restart::Continue)?; + } + + // Do not follow forks. + // + // After this, we assume that any new tracee is a thread in the same + // group as the root tracee. + let mut options = Options::all(); + options.remove(Options::PTRACE_O_TRACEFORK); + options.remove(Options::PTRACE_O_TRACEVFORK); + options.remove(Options::PTRACE_O_TRACEEXEC); + tracee.set_options(options)?; + + self.images = Some(Images::new(tracee.pid.as_raw())); + self.update_images(&mut tracee)?; + + ptracer.restart(tracee, Restart::Syscall)?; + + while let Some(mut tracee) = ptracer.wait()? { + match tracee.stop { + Stop::SyscallEnterStop(..) => log::trace!("syscall-enter: {:?}", tracee.stop), + Stop::SyscallExitStop(..) => { + self.update_images(&mut tracee)?; + } + Stop::SignalDeliveryStop(_pid, Signal::SIGTRAP) => { + self.on_breakpoint(&mut tracee)?; + } + Stop::Clone(pid, tid) => { + // Only seen when the `VM_CLONE` flag is set, as of Linux 4.15. + log::info!("new thread: {} -> {}", pid, tid); + } + _ => { + log::debug!("stop: {:?}", tracee.stop); + } + } + + if let Err(err) = ptracer.restart(tracee, Restart::Syscall) { + log::error!("unable to restart tracee: {}", err); + } + } + + Ok(()) + } + + fn update_images(&mut self, tracee: &mut Tracee) -> Result<()> { + let images = self + .images + .as_mut() + .ok_or_else(|| format_err!("internal error: recorder images not initialized"))?; + let events = images.update()?; + + for (_base, image) in &events.loaded { + self.on_module_load(tracee, image)?; + } + + Ok(()) + } + + fn on_breakpoint(&mut self, tracee: &mut Tracee) -> Result<()> { + let mut regs = tracee.registers()?; + + log::trace!("hit breakpoint: {:x} (~{})", regs.rip - 1, tracee.pid); + + // Adjust for synthetic `int3`. + let pc = regs.rip - 1; + + if self.breakpoints.clear(tracee, pc)? { + let images = self + .images + .as_ref() + .ok_or_else(|| format_err!("internal error: recorder images not initialized"))?; + let image = images + .find_va_image(pc) + .ok_or_else(|| format_err!("unable to find image for va = {:x}", pc))?; + + self.coverage.increment(&image, pc)?; + + // Execute clobbered instruction on restart. + regs.rip = pc; + tracee.set_registers(regs)?; + } else { + // Assume the tracee concurrently executed an `int3` that we restored + // in another handler. + // + // We could improve on this by not removing breakpoints metadata when + // clearing, but making their value a state. + log::debug!("no breakpoint at {:x}, assuming race", pc); + regs.rip = pc; + tracee.set_registers(regs)?; + } + + Ok(()) + } + + fn on_module_load(&mut self, tracee: &mut Tracee, image: &ModuleImage) -> Result<()> { + log::info!("module load: {}", image.path().display()); + + let blocks = find_module_blocks(image.path())?; + + log::debug!("found {} blocks", blocks.len()); + + if blocks.is_empty() { + // This almost certainly means the binary was stripped of symbols. + log::warn!("no blocks for module, not setting breakpoints"); + return Ok(()); + } + + let new = self.coverage.add_module(image, &blocks)?; + + if new { + for b in blocks { + let va = image.offset_to_va(b.offset); + log::trace!("set breakpoint: {:x} (~{})", va, tracee.pid); + self.breakpoints.set(tracee, va)?; + } + } + + Ok(()) + } +} + +/// Block coverage for a command invocation. +/// +/// Organized by module. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct CommandBlockCov { + pub modules: BTreeMap, +} + +impl CommandBlockCov { + pub fn add_module(&mut self, image: &ModuleImage, blocks: &[Block]) -> Result { + if self.modules.contains_key(image.path()) { + return Ok(false); + } + + let blocks = blocks.iter().map(|b| b.offset); + let cov = ModuleCov::new(image.path(), blocks); + + self.modules.insert(image.path().to_owned(), cov); + + Ok(true) + } + + pub fn increment(&mut self, image: &ModuleImage, va: u64) -> Result<()> { + if let Some(cov) = self.modules.get_mut(image.path()) { + let offset = image.va_to_offset(va); + if let Some(block) = cov.blocks.get_mut(&offset) { + block.count += 1; + } + } + + Ok(()) + } +} + +/// Executable memory-mapped files for a process. +#[derive(Clone, Debug, PartialEq)] +pub struct Images { + mapped: BTreeMap, + pid: i32, +} + +impl Images { + pub fn new(pid: i32) -> Self { + let mapped = BTreeMap::default(); + + Self { mapped, pid } + } + + pub fn mapped(&self) -> impl Iterator { + self.mapped.iter().map(|(va, i)| (*va, i)) + } + + pub fn update(&mut self) -> Result { + let proc = Process::new(self.pid)?; + + let mut new = BTreeMap::default(); + + for map in proc.maps()? { + if let Ok(image) = ModuleImage::new(map) { + new.insert(image.base(), image); + } + } + + let events = LoadEvents::new(&self.mapped, &new); + + self.mapped = new; + + Ok(events) + } + + pub fn find_va_image(&self, va: u64) -> Option<&ModuleImage> { + for (base, image) in self.mapped() { + if va < base { + continue; + } + + if image.region().contains(&va) { + return Some(&image); + } + } + + None + } +} + +/// A `MemoryMap` that is known to be file-backed and executable. +#[derive(Clone, Debug, PartialEq)] +pub struct ModuleImage { + map: MemoryMap, +} + +impl ModuleImage { + pub fn new(map: MemoryMap) -> Result { + if let MMapPath::Path(..) = &map.pathname { + if map.perms.contains("x") { + return Ok(ModuleImage { map }); + } else { + anyhow::bail!("memory mapping is not executable"); + } + } else { + anyhow::bail!("memory mapping is not file-backed"); + } + } + + pub fn name(&self) -> &OsStr { + // File name existence guaranteed by how we acquired the `Path`. + self.path().file_name().unwrap() + } + + pub fn path(&self) -> &Path { + if let MMapPath::Path(path) = &self.map.pathname { + return &path; + } + + // Enforced by ctor. + unreachable!() + } + + pub fn map(&self) -> &MemoryMap { + &self.map + } + + pub fn base(&self) -> u64 { + self.map.address.0 - self.map.offset + } + + pub fn size(&self) -> u64 { + self.map.address.1 - self.map.address.0 + } + + pub fn region(&self) -> std::ops::Range { + (self.map.address.0)..(self.map.address.1) + } + + pub fn va_to_offset(&self, va: u64) -> u64 { + va - self.base() + } + + pub fn offset_to_va(&self, offset: u64) -> u64 { + self.base() + offset + } +} + +pub struct LoadEvents { + pub loaded: Vec<(u64, ModuleImage)>, + pub unloaded: Vec<(u64, ModuleImage)>, +} + +impl LoadEvents { + pub fn new(old: &BTreeMap, new: &BTreeMap) -> Self { + // New not in old. + let loaded: Vec<_> = new + .iter() + .filter(|(nva, n)| { + old.iter() + .find(|(iva, i)| nva == iva && n.path() == i.path()) + .is_none() + }) + .map(|(va, i)| (*va, i.clone())) + .collect(); + + // Old not in new. + let unloaded: Vec<_> = old + .iter() + .filter(|(iva, i)| { + new.iter() + .find(|(nva, n)| nva == iva && n.path() == i.path()) + .is_none() + }) + .map(|(va, i)| (*va, i.clone())) + .collect(); + + Self { loaded, unloaded } + } +} + +#[derive(Clone, Debug, Default)] +pub struct Breakpoints { + saved: BTreeMap, +} + +impl Breakpoints { + pub fn set(&mut self, tracee: &mut Tracee, va: u64) -> Result<()> { + // Return if the breakpoint exists. We don't want to conclude that the + // saved instruction byte was `0xcc`. + if self.saved.contains_key(&va) { + return Ok(()); + } + + let mut data = [0u8]; + tracee.read_memory_mut(va, &mut data)?; + self.saved.insert(va, data[0]); + tracee.write_memory(va, &[0xcc])?; + + Ok(()) + } + + pub fn clear(&mut self, tracee: &mut Tracee, va: u64) -> Result { + let data = self.saved.remove(&va); + + let cleared = if let Some(data) = data { + tracee.write_memory(va, &[data])?; + true + } else { + false + }; + + Ok(cleared) + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct Block { + pub offset: u64, +} + +type ElfFile<'a> = elf::ElfFile64<'a, LE>; +type ElfSymbol<'a> = elf::ElfSymbol64<'a, 'a, LE>; +type ElfSection<'a> = elf::ElfSection64<'a, 'a, LE>; + +pub fn find_module_blocks(module: &Path) -> Result> { + let data = std::fs::read(module)?; + let elf = ElfFile::parse(&data)?; + + let load_va = + elf.segments().map(|s| s.address()).min().ok_or_else(|| { + format_err!("no loadable segments for ELF object ({})", module.display()) + })?; + + let mut blocks = vec![]; + + for sym in elf.symbols() { + if sym.kind() == object::SymbolKind::Text && sym.size() > 0 { + if let Some(idx) = sym.section().index() { + let section = elf.section_by_index(idx)?; + let sym_blocks = find_symbol_blocks(section, sym)?; + blocks.extend(sym_blocks); + } + } + } + + // The blocks we've collected have VAs as `offset` values, assuming the preferred base + // load address. Make them true offsets, relative to that image base address. + for block in &mut blocks { + block.offset -= load_va; + } + + Ok(blocks) +} + +pub fn find_symbol_blocks(section: ElfSection, sym: ElfSymbol) -> Result> { + // Slice symbol's data within section. + let lo = (sym.address() - section.address()) as usize; + let hi = lo + (sym.size() as usize); + let data = section.data()?; + let data = &data[lo..hi]; + + let mut decoder = Decoder::new(64, data, DecoderOptions::NONE); + decoder.set_ip(sym.address()); + + // Contains leaders with VAs, assuming section load address. + let mut leaders = BTreeSet::new(); + + // Function entry is a leader. + leaders.insert(sym.address()); + + let mut inst = Instruction::default(); + while decoder.can_decode() { + decoder.decode_out(&mut inst); + + if let Some((target, conditional)) = branch_target(&inst) { + // The branch target is a leader. + leaders.insert(target); + + // Only mark the next instruction as a leader if the branch is conditional. + // This will give an invalid basic block decomposition if the leaders we emit + // are used as delimiters. In particular, blocks that end with a `jmp` will be + // too large, and have an unconditional branch mid-block. + // + // However, we only care about the leaders as block entry points, so we can + // set software breakpoints. These maybe-unreachable leaders are a liability + // wrt mutating the running process' code, so we discard them for now. + if conditional { + // The next instruction is a leader, if it exists. + if decoder.can_decode() { + // We decoded the current instruction, so the decoder offset is + // set to the next instruction. + let next = decoder.ip() as u64; + leaders.insert(next); + } + } + } + } + + let blocks: Vec<_> = leaders.iter().map(|va| Block { offset: *va }).collect(); + + Ok(blocks) +} + +// Returns the virtual address of a branch target, if present, with a flag that +// is true when the branch is conditional. +fn branch_target(inst: &iced_x86::Instruction) -> Option<(u64, bool)> { + use iced_x86::FlowControl; + + match inst.flow_control() { + FlowControl::ConditionalBranch => Some((inst.near_branch_target(), true)), + FlowControl::UnconditionalBranch => Some((inst.near_branch_target(), false)), + FlowControl::Call + | FlowControl::Exception + | FlowControl::IndirectBranch + | FlowControl::IndirectCall + | FlowControl::Interrupt + | FlowControl::Next + | FlowControl::Return + | FlowControl::XbeginXabortXend => None, + } +} diff --git a/src/agent/coverage/src/block/windows.rs b/src/agent/coverage/src/block/windows.rs new file mode 100644 index 000000000..d9372385d --- /dev/null +++ b/src/agent/coverage/src/block/windows.rs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::BTreeMap; +use std::process::Command; +use std::time::{Duration, Instant}; + +use anyhow::Result; +use debugger::{ + dbghelp::SymInfo, + debugger::{BreakpointId, BreakpointType, DebugEventHandler, Debugger}, + target::Module, +}; + +use crate::{AppCoverageBlocks, ModuleCoverageBlocks}; + +pub fn record(cmd: Command) -> Result { + let mut handler = BlockCoverageHandler::new(); + + let (mut dbg, _child) = Debugger::init(cmd, &mut handler)?; + dbg.run(&mut handler)?; + + Ok(handler.coverage) +} + +pub struct BlockCoverageHandler { + bp_to_block: BTreeMap, + coverage: AppCoverageBlocks, + started: Instant, + max_duration: Duration, + timed_out: bool, +} + +impl BlockCoverageHandler { + pub fn new() -> Self { + let coverage = AppCoverageBlocks::new(); + let bp_to_block = BTreeMap::default(); + let started = Instant::now(); + let max_duration = Duration::from_secs(5); + let timed_out = false; + + Self { + bp_to_block, + coverage, + max_duration, + started, + timed_out, + } + } + + pub fn pc(&self, dbg: &mut Debugger) -> Result<(u64, Option)> { + use iced_x86::Register::RIP; + + let pc = dbg.read_register_u64(RIP)?; + let sym = dbg.get_symbol(pc).ok(); + + Ok((pc, sym)) + } + + fn add_module(&mut self, dbg: &mut Debugger, module: &Module) { + let bitset = crate::pe::process_image(module.path(), false); + let bitset = match bitset { + Ok(bitset) => bitset, + Err(err) => { + // If we can't add the module, continue debugging. + // We don't expect to have symbols for every module. + log::warn!( + "cannot record coverage for module = {}, err = {}", + module.path().display(), + err, + ); + return; + } + }; + + let module_coverage = ModuleCoverageBlocks::new(module.path(), module.name(), bitset); + + let m = self.coverage.add_module(module_coverage); + let module_coverage = &self.coverage.modules()[m]; + for (b, block) in module_coverage.blocks().iter().enumerate() { + let bp = + dbg.register_breakpoint(module.name(), block.rva() as u64, BreakpointType::OneTime); + self.bp_to_block.insert(bp, (m, b)); + } + + log::debug!( + "inserted {} breakpoints for module {}", + module_coverage.blocks().len(), + module.path().display(), + ); + } + + fn stop(&self, dbg: &mut Debugger) { + dbg.quit_debugging(); + } + + fn try_on_create_process(&mut self, dbg: &mut Debugger, module: &Module) -> Result<()> { + dbg.target().sym_initialize()?; + + log::info!( + "exe loaded: {}, {} bytes", + module.path().display(), + module.image_size(), + ); + + self.add_module(dbg, module); + + Ok(()) + } + + fn try_on_load_dll(&mut self, dbg: &mut Debugger, module: &Module) -> Result<()> { + log::info!( + "dll loaded: {}, {} bytes", + module.path().display(), + module.image_size(), + ); + + self.add_module(dbg, module); + + Ok(()) + } + + fn try_on_breakpoint(&mut self, dbg: &mut Debugger, bp: BreakpointId) -> Result<()> { + let (pc, _sym) = self.pc(dbg)?; + + if let Some(&(m, b)) = self.bp_to_block.get(&bp) { + if log::max_level() == log::Level::Trace { + let module = &self.coverage.modules()[m]; + let block = &module.blocks()[b]; + let name = module.name().display(); + log::trace!("{:>16x}: {}+{:x}", pc, name, block.rva()); + } + + self.coverage.report_block_hit(m, b); + } else { + log::error!("no block for breakpoint"); + } + + Ok(()) + } + + fn try_on_poll(&mut self, dbg: &mut Debugger) -> Result<()> { + if !self.timed_out && self.started.elapsed() > self.max_duration { + self.timed_out = true; + dbg.quit_debugging(); + } + + Ok(()) + } +} + +impl DebugEventHandler for BlockCoverageHandler { + fn on_create_process(&mut self, dbg: &mut Debugger, module: &Module) { + if self.try_on_create_process(dbg, module).is_err() { + self.stop(dbg); + } + } + + fn on_load_dll(&mut self, dbg: &mut Debugger, module: &Module) { + if self.try_on_load_dll(dbg, module).is_err() { + self.stop(dbg); + } + } + + fn on_breakpoint(&mut self, dbg: &mut Debugger, bp: BreakpointId) { + if self.try_on_breakpoint(dbg, bp).is_err() { + self.stop(dbg); + } + } + + fn on_poll(&mut self, dbg: &mut Debugger) { + if self.try_on_poll(dbg).is_err() { + self.stop(dbg); + } + } +} diff --git a/src/agent/coverage/src/lib.rs b/src/agent/coverage/src/lib.rs index d01501e0a..ea44a1772 100644 --- a/src/agent/coverage/src/lib.rs +++ b/src/agent/coverage/src/lib.rs @@ -1,13 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - -#![cfg(windows)] #![allow(clippy::as_conversions)] #![allow(clippy::new_without_default)] +#[cfg(target_os = "windows")] mod intel; + +#[cfg(target_os = "windows")] pub mod pe; +pub mod block; + use std::{ ffi::OsString, fs::File, @@ -121,6 +124,10 @@ impl AppCoverageBlocks { Ok(()) } + pub fn count_blocks(&self) -> usize { + self.modules().iter().map(|m| m.blocks().len()).sum() + } + pub fn count_blocks_hit(&self) -> usize { self.modules().iter().map(|m| m.count_blocks_hit()).sum() } @@ -128,6 +135,7 @@ impl AppCoverageBlocks { /// Statically analyze the specified images to discover the basic block /// entry points and write out the results in a file in `output_dir`. +#[cfg(target_os = "windows")] pub fn run_init(output_dir: PathBuf, modules: Vec, function: bool) -> Result<()> { let mut result = AppCoverageBlocks::new(); for module in modules { diff --git a/src/agent/debugger/src/debugger.rs b/src/agent/debugger/src/debugger.rs index a78c85799..bdf5b6dc7 100644 --- a/src/agent/debugger/src/debugger.rs +++ b/src/agent/debugger/src/debugger.rs @@ -45,7 +45,7 @@ use crate::{ const STATUS_WX86_BREAKPOINT: u32 = ::winapi::shared::ntstatus::STATUS_WX86_BREAKPOINT as u32; /// Uniquely identify a breakpoint. -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct BreakpointId(pub u32); #[derive(Copy, Clone)]