diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..6b80765e --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" + diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c9c84b4..3472cd68 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,24 +12,8 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - mps-build: - runs-on: ubuntu-latest - steps: - - name: Checkout repository and submodules - uses: actions/checkout@v4 - - name: Build MPS - uses: addnab/docker-run-action@v3 - with: - image: galoisinc/hardens:latest - options: -v ${{ github.workspace }}:/HARDENS - run: | - cd components/mission_protection_system/src - SENSORS=NotSimulated SELF_TEST=Enabled make rts - make clean - SENSORS=NotSimulated SELF_TEST=Enabled make rts_bottom - - mps-test: - runs-on: ubuntu-latest + mps-build: + runs-on: ubuntu-22.04 steps: - name: Checkout repository and submodules uses: actions/checkout@v4 @@ -40,12 +24,54 @@ jobs: options: -v ${{ github.workspace }}:/HARDENS run: | cd components/mission_protection_system/src + SENSORS=NotSimulated SELF_TEST=Enabled make rts_bottom make clean SENSORS=NotSimulated SELF_TEST=Disabled make rts mv rts rts.no_self_test make clean SENSORS=NotSimulated SELF_TEST=Enabled make rts mv rts rts.self_test - cd ../tests - pip3 install -r requirements.txt - RTS_DEBUG=1 QUICK=1 python3 ./run_all.py + - name: Upload MPS binaries + uses: actions/upload-artifact@v4 + with: + name: mps-binaries + path: components/mission_protection_system/src/rts.* + + mps-test: + runs-on: ubuntu-22.04 + needs: mps-build + steps: + - name: Checkout repository and submodules + uses: actions/checkout@v4 + - name: Download MPS binaries + uses: actions/download-artifact@v4 + with: + name: mps-binaries + - name: Display structure of downloaded files + run: | + chmod +x rts.* + mv rts.* components/mission_protection_system/src/. + - name: Test MPS + uses: addnab/docker-run-action@v3 + with: + image: galoisinc/hardens:latest + options: -v ${{ github.workspace }}:/HARDENS + run: | + cd components/mission_protection_system/tests + pip3 install -r requirements.txt + RTS_DEBUG=1 QUICK=1 python3 ./run_all.py + + vmrunner: + runs-on: ubuntu-22.04 + steps: + - name: Install aarch64 toolchain + run: sudo apt-get install -y gcc-aarch64-linux-gnu + - uses: hecrj/setup-rust-action@v2 + with: + rust-version: 1.74 + targets: aarch64-unknown-linux-gnu + - uses: actions/checkout@master + - name: Build VM runner + run: | + cd src/vm_runner + cargo build --release --target aarch64-unknown-linux-gnu diff --git a/.github/workflows/proofs.yml b/.github/workflows/proofs.yml index 3eb721fa..f4306b68 100644 --- a/.github/workflows/proofs.yml +++ b/.github/workflows/proofs.yml @@ -14,7 +14,7 @@ on: jobs: mps-verify-cn: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout repository and submodules uses: actions/checkout@v4 @@ -32,7 +32,7 @@ jobs: make -f cn.mk proofs mps-verify-frama-c: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout repository and submodules uses: actions/checkout@v4 diff --git a/components/mission_protection_system/build_aarch64.sh b/components/mission_protection_system/build_aarch64.sh new file mode 100755 index 00000000..49c0325c --- /dev/null +++ b/components/mission_protection_system/build_aarch64.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")/src" +make clean +make CC=aarch64-linux-gnu-g++ CXX=aarch64-linux-gnu-g++ SENSORS=NotSimulated SELF_TEST=Disabled +cp -v rts rts.no_self_test.aarch64 +make clean +make CC=aarch64-linux-gnu-g++ CXX=aarch64-linux-gnu-g++ SENSORS=NotSimulated SELF_TEST=Enabled +cp -v rts rts.self_test.aarch64 diff --git a/components/mission_protection_system/src/posix_main.c b/components/mission_protection_system/src/posix_main.c index 2b47e712..e50e4d43 100644 --- a/components/mission_protection_system/src/posix_main.c +++ b/components/mission_protection_system/src/posix_main.c @@ -83,6 +83,10 @@ void update_display() { } } +// A copy of the `argv` that was passed to main. This is used to implement the +// reset (`R`) command inside `read_rts_command`. +static char** main_argv = NULL; + int read_rts_command(struct rts_command *cmd) { int ok = 0; uint8_t device, on, div, ch, mode, sensor; @@ -166,8 +170,13 @@ int read_rts_command(struct rts_command *cmd) { sensor,ch,val)); #endif } else if (line[0] == 'Q') { - // printf(" read_rts_command QUIT\n"); + printf(" read_rts_command QUIT\n"); exit(0); + } else if (line[0] == 'R') { + printf(" read_rts_command RESET\n"); + // Re-exec the RTS binary with the same arguments and environment. This + // has the effect of resetting the entire RTS to its initial state. + execv("/proc/self/exe", main_argv); } else if (line[0] == 'D') { DEBUG_PRINTF((" read_rts_command UPDATE DISPLAY\n")); update_display(); @@ -363,6 +372,7 @@ uint32_t time_in_s() } int main(int argc, char **argv) { + main_argv = argv; struct rts_command *cmd = (struct rts_command *)malloc(sizeof(*cmd)); core_init(&core); diff --git a/components/mission_protection_system/src/self_test_data/test.csv b/components/mission_protection_system/src/self_test_data/test.csv index 60d3d069..9fc447ef 100644 --- a/components/mission_protection_system/src/self_test_data/test.csv +++ b/components/mission_protection_system/src/self_test_data/test.csv @@ -1,10 +1,190 @@ -0x1 [[0xb56b, 0x9b67], [0x116d, 0x305e], [0x1e6a, 0x6d2b], [0x4278, 0x3cac]] [[0xbea4, 0x8cff, 0x8276], [0x73c1, 0x3306, 0x4b66], [0x1306, 0x8017, 0x061b], [0x0681, 0xe654, 0x563f]] 0x1 0x2 -0x1 [[0xb4bf, 0xf2a5], [0xb729, 0x0592], [0xc445, 0x3267], [0xdb63, 0x98d5]] [[0x0af4, 0xb1ce, 0xa1c2], [0x3042, 0x23f3, 0x50b6], [0x578e, 0xc16a, 0xad24], [0xebd5, 0xc4b4, 0x6ea9]] 0x2 0x3 -0x1 [[0x9e64, 0x646e], [0xec38, 0xa171], [0x8b17, 0x26fa], [0x6e11, 0x13e9]] [[0x4301, 0xeed8, 0x407c], [0xbdd9, 0x5e64, 0x978f], [0x337e, 0xb6b8, 0xc261], [0xf272, 0xbec3, 0xe020]] 0x3 0x3 -0x1 [[0xc707, 0x909b], [0x5950, 0x5023], [0x2174, 0x7fbc], [0x9c8e, 0xb1c5]] [[0x450a, 0x5387, 0x4251], [0x54f7, 0x7331, 0x610c], [0x2246, 0xc23c, 0xe353], [0xd742, 0x11f0, 0x5c9b]] 0x2 0x2 -0x1 [[0x64a7, 0xfc58], [0x73c5, 0x0dca], [0x76be, 0x322f], [0xf275, 0x856e]] [[0x71fb, 0xe1fc, 0xc226], [0xdca3, 0x47de, 0x2a2c], [0x365b, 0xe8d8, 0x5e63], [0x7d75, 0xf509, 0x0d46]] 0x0 0x0 -0x1 [[0x3379, 0x097d], [0x8d0a, 0xf187], [0xb18d, 0x6bde], [0x31b3, 0x1656]] [[0x2a5f, 0xa347, 0xfd69], [0x99a0, 0xb82f, 0x9554], [0x2106, 0x7623, 0x08dd], [0xabf1, 0xc54c, 0x1237]] 0x0 0x2 -0x1 [[0xe71e, 0x506e], [0x84a3, 0xf7d7], [0x3c66, 0xfdb7], [0x354c, 0xa3bb]] [[0x79e7, 0xec8c, 0x73c2], [0x9d4c, 0x395b, 0x4a52], [0x783c, 0xf1cc, 0xd0ee], [0x8171, 0xc116, 0x08bd]] 0x0 0x0 -0x1 [[0x33fe, 0x41d3], [0x1842, 0xce9e], [0xa731, 0x6312], [0xbabb, 0xaa2e]] [[0xd683, 0x8077, 0x2d0d], [0x0c50, 0xa354, 0xb23e], [0xc806, 0xa680, 0x25d1], [0x965f, 0xba1f, 0x7f91]] 0x1 0x3 -0x3 [[0x8f55, 0xa308], [0x8910, 0xc8f3], [0xed53, 0xa96e], [0x6b72, 0xb094]] [[0x8024, 0x2af2, 0x8a77], [0xd392, 0x6b95, 0xc5e4], [0xd167, 0x78eb, 0xae62], [0xd786, 0x2183, 0xeda3]] 0x3 0x3 -0x1 [[0x03a4, 0x7579], [0xfef5, 0x193e], [0x8381, 0xbdd3], [0x649d, 0xae79]] [[0xc42c, 0xd33a, 0x2cc9], [0xa687, 0x657b, 0xbf3b], [0x48d2, 0xc9b1, 0x2b48], [0xb123, 0x8814, 0x497c]] 0x2 0x3 +0x1 [[0x753c, +0x8567], +[0x9941, +0x9047], +[0x010f, +0x619e], +[0x6c16, +0xa5e2]] [[0xbe86, +0x2ac0, +0x508e], +[0xf929, +0x7f97, +0x2e07], +[0x92f3, +0xdec1, +0xe896], +[0xca3a, +0x4532, +0x67ac]] 0x1 0x3 +0x1 [[0xe2bc, +0x48ad], +[0x20ab, +0x6b44], +[0x5074, +0x8063], +[0x74b5, +0xfadb]] [[0xe5ce, +0x151f, +0x69fc], +[0x933a, +0xe0e5, +0x372d], +[0x52d0, +0x5320, +0x6273], +[0x6d2a, +0xc90a, +0x6c50]] 0x3 0x1 +0x3 [[0xaf29, +0xfb7f], +[0xd293, +0xcaa6], +[0x101d, +0xb852], +[0x10da, +0xfa5c]] [[0x1f9b, +0xf7d2, +0x64c2], +[0xfa38, +0x35e3, +0xd88e], +[0x0660, +0xe4b8, +0xb256], +[0x5071, +0x5b45, +0xa947]] 0x3 0x1 +0x3 [[0xfd39, +0x21d4], +[0xdb77, +0xff43], +[0x899b, +0xd07b], +[0x576c, +0xe806]] [[0xf591, +0x3092, +0xdeab], +[0x53b7, +0xcdd8, +0x3125], +[0x71c2, +0x375c, +0x4d54], +[0xf62f, +0xe4f1, +0x7d1a]] 0x0 0x3 +0x1 [[0x2111, +0x72a4], +[0x5ef8, +0xc6af], +[0x702d, +0x1f3f], +[0x7030, +0x1828]] [[0x70bd, +0xa499, +0xb6a4], +[0x3749, +0x0bc8, +0x0170], +[0x90ef, +0x1291, +0xf908], +[0x66b2, +0xc0a7, +0x4c86]] 0x0 0x2 +0x1 [[0x205c, +0x813b], +[0xdfcc, +0xb70d], +[0x877f, +0x930a], +[0x4300, +0x3a90]] [[0x1966, +0xf30a, +0x1237], +[0xcbb6, +0xdf45, +0xb3a7], +[0xb5d1, +0x9b11, +0x1e04], +[0x4ca2, +0xa2c5, +0xe2b2]] 0x2 0x1 +0x1 [[0x64f6, +0x2140], +[0x839a, +0x55a7], +[0x6255, +0x27ed], +[0x9baa, +0xb966]] [[0xc51d, +0x1552, +0xc126], +[0x86c1, +0x66ae, +0x8294], +[0xb374, +0x4385, +0x5ffb], +[0x3ff4, +0x9a3c, +0xb897]] 0x2 0x0 +0x1 [[0x77b9, +0x9969], +[0x648b, +0x1459], +[0x1c41, +0x5f5e], +[0x30a9, +0x9941]] [[0xa6c2, +0xc89c, +0x854d], +[0xbff7, +0xd30a, +0xae30], +[0x733a, +0xacbc, +0x5680], +[0xde27, +0x0ba9, +0xcc1c]] 0x1 0x2 +0x3 [[0x95a8, +0xecfd], +[0x617d, +0xeb9b], +[0x74c8, +0xfb58], +[0x73e2, +0x378c]] [[0x1674, +0x53e5, +0xabbd], +[0x6ee4, +0xe0c2, +0xdd77], +[0x26ab, +0x9e64, +0x6e96], +[0x0c21, +0x13a3, +0x9d27]] 0x3 0x1 +0x3 [[0xd477, +0x4235], +[0xa5b1, +0xf5e3], +[0x30ec, +0xf265], +[0x014e, +0x8fd2]] [[0x31b9, +0x7d86, +0x15e4], +[0xea39, +0xf888, +0x9fbe], +[0x8247, +0x7fbd, +0x5b29], +[0xdb42, +0x301d, +0x7ed8]] 0x2 0x0 diff --git a/components/mission_protection_system/src/self_test_data/test_to_c.py b/components/mission_protection_system/src/self_test_data/test_to_c.py index 6e88ff76..474639fa 100755 --- a/components/mission_protection_system/src/self_test_data/test_to_c.py +++ b/components/mission_protection_system/src/self_test_data/test_to_c.py @@ -27,8 +27,16 @@ def render(test): return f"{test}" with open(sys.argv[1]) as csv_file: - reader = csv.reader(csv_file,delimiter='\t') - foos = [render(tc) for testcase in reader for tc in expand(testcase)] + line_buf = '' + foos = [] + for line in csv_file: + line = line.rstrip() + line_buf += line + if not line_buf.endswith(','): + testcase = line_buf.split('\t') + foos.extend(render(tc) for tc in expand(testcase)) + line_buf = '' + tests = ",\n".join(foos) print(f"// Tests generated from {sys.argv[1]}") print(tests) diff --git a/components/mission_protection_system/src/self_test_data/tests.inc.c b/components/mission_protection_system/src/self_test_data/tests.inc.c index 72900933..b8a9cfe6 100644 --- a/components/mission_protection_system/src/self_test_data/tests.inc.c +++ b/components/mission_protection_system/src/self_test_data/tests.inc.c @@ -1,41 +1,41 @@ // Tests generated from test.csv -{{{46443, 39783}, {4461, 12382}, {7786, 27947}, {17016, 15532}}, {{48804, 36095, 33398}, {29633, 13062, 19302}, {4870, 32791, 1563}, {1665, 58964, 22079}}, {1, 0}, 0, 0, 0}, -{{{46443, 39783}, {4461, 12382}, {7786, 27947}, {17016, 15532}}, {{48804, 36095, 33398}, {29633, 13062, 19302}, {4870, 32791, 1563}, {1665, 58964, 22079}}, {1, 0}, 1, 0, 0}, -{{{46443, 39783}, {4461, 12382}, {7786, 27947}, {17016, 15532}}, {{48804, 36095, 33398}, {29633, 13062, 19302}, {4870, 32791, 1563}, {1665, 58964, 22079}}, {1, 0}, 0, 1, 1}, -{{{46443, 39783}, {4461, 12382}, {7786, 27947}, {17016, 15532}}, {{48804, 36095, 33398}, {29633, 13062, 19302}, {4870, 32791, 1563}, {1665, 58964, 22079}}, {1, 0}, 1, 1, 1}, -{{{46271, 62117}, {46889, 1426}, {50245, 12903}, {56163, 39125}}, {{2804, 45518, 41410}, {12354, 9203, 20662}, {22414, 49514, 44324}, {60373, 50356, 28329}}, {2, 3}, 0, 0, 0}, -{{{46271, 62117}, {46889, 1426}, {50245, 12903}, {56163, 39125}}, {{2804, 45518, 41410}, {12354, 9203, 20662}, {22414, 49514, 44324}, {60373, 50356, 28329}}, {2, 3}, 1, 0, 0}, -{{{46271, 62117}, {46889, 1426}, {50245, 12903}, {56163, 39125}}, {{2804, 45518, 41410}, {12354, 9203, 20662}, {22414, 49514, 44324}, {60373, 50356, 28329}}, {2, 3}, 0, 1, 1}, -{{{46271, 62117}, {46889, 1426}, {50245, 12903}, {56163, 39125}}, {{2804, 45518, 41410}, {12354, 9203, 20662}, {22414, 49514, 44324}, {60373, 50356, 28329}}, {2, 3}, 1, 1, 1}, -{{{40548, 25710}, {60472, 41329}, {35607, 9978}, {28177, 5097}}, {{17153, 61144, 16508}, {48601, 24164, 38799}, {13182, 46776, 49761}, {62066, 48835, 57376}}, {3, 0}, 0, 0, 0}, -{{{40548, 25710}, {60472, 41329}, {35607, 9978}, {28177, 5097}}, {{17153, 61144, 16508}, {48601, 24164, 38799}, {13182, 46776, 49761}, {62066, 48835, 57376}}, {3, 0}, 1, 0, 0}, -{{{40548, 25710}, {60472, 41329}, {35607, 9978}, {28177, 5097}}, {{17153, 61144, 16508}, {48601, 24164, 38799}, {13182, 46776, 49761}, {62066, 48835, 57376}}, {3, 0}, 0, 1, 1}, -{{{40548, 25710}, {60472, 41329}, {35607, 9978}, {28177, 5097}}, {{17153, 61144, 16508}, {48601, 24164, 38799}, {13182, 46776, 49761}, {62066, 48835, 57376}}, {3, 0}, 1, 1, 1}, -{{{50951, 37019}, {22864, 20515}, {8564, 32700}, {40078, 45509}}, {{17674, 21383, 16977}, {21751, 29489, 24844}, {8774, 49724, 58195}, {55106, 4592, 23707}}, {2, 1}, 0, 0, 0}, -{{{50951, 37019}, {22864, 20515}, {8564, 32700}, {40078, 45509}}, {{17674, 21383, 16977}, {21751, 29489, 24844}, {8774, 49724, 58195}, {55106, 4592, 23707}}, {2, 1}, 1, 0, 0}, -{{{50951, 37019}, {22864, 20515}, {8564, 32700}, {40078, 45509}}, {{17674, 21383, 16977}, {21751, 29489, 24844}, {8774, 49724, 58195}, {55106, 4592, 23707}}, {2, 1}, 0, 1, 1}, -{{{50951, 37019}, {22864, 20515}, {8564, 32700}, {40078, 45509}}, {{17674, 21383, 16977}, {21751, 29489, 24844}, {8774, 49724, 58195}, {55106, 4592, 23707}}, {2, 1}, 1, 1, 1}, -{{{25767, 64600}, {29637, 3530}, {30398, 12847}, {62069, 34158}}, {{29179, 57852, 49702}, {56483, 18398, 10796}, {13915, 59608, 24163}, {32117, 62729, 3398}}, {0, 1}, 0, 0, 0}, -{{{25767, 64600}, {29637, 3530}, {30398, 12847}, {62069, 34158}}, {{29179, 57852, 49702}, {56483, 18398, 10796}, {13915, 59608, 24163}, {32117, 62729, 3398}}, {0, 1}, 1, 0, 0}, -{{{25767, 64600}, {29637, 3530}, {30398, 12847}, {62069, 34158}}, {{29179, 57852, 49702}, {56483, 18398, 10796}, {13915, 59608, 24163}, {32117, 62729, 3398}}, {0, 1}, 0, 1, 1}, -{{{25767, 64600}, {29637, 3530}, {30398, 12847}, {62069, 34158}}, {{29179, 57852, 49702}, {56483, 18398, 10796}, {13915, 59608, 24163}, {32117, 62729, 3398}}, {0, 1}, 1, 1, 1}, -{{{13177, 2429}, {36106, 61831}, {45453, 27614}, {12723, 5718}}, {{10847, 41799, 64873}, {39328, 47151, 38228}, {8454, 30243, 2269}, {44017, 50508, 4663}}, {0, 3}, 0, 0, 0}, -{{{13177, 2429}, {36106, 61831}, {45453, 27614}, {12723, 5718}}, {{10847, 41799, 64873}, {39328, 47151, 38228}, {8454, 30243, 2269}, {44017, 50508, 4663}}, {0, 3}, 1, 0, 0}, -{{{13177, 2429}, {36106, 61831}, {45453, 27614}, {12723, 5718}}, {{10847, 41799, 64873}, {39328, 47151, 38228}, {8454, 30243, 2269}, {44017, 50508, 4663}}, {0, 3}, 0, 1, 1}, -{{{13177, 2429}, {36106, 61831}, {45453, 27614}, {12723, 5718}}, {{10847, 41799, 64873}, {39328, 47151, 38228}, {8454, 30243, 2269}, {44017, 50508, 4663}}, {0, 3}, 1, 1, 1}, -{{{59166, 20590}, {33955, 63447}, {15462, 64951}, {13644, 41915}}, {{31207, 60556, 29634}, {40268, 14683, 19026}, {30780, 61900, 53486}, {33137, 49430, 2237}}, {0, 1}, 0, 0, 0}, -{{{59166, 20590}, {33955, 63447}, {15462, 64951}, {13644, 41915}}, {{31207, 60556, 29634}, {40268, 14683, 19026}, {30780, 61900, 53486}, {33137, 49430, 2237}}, {0, 1}, 1, 0, 0}, -{{{59166, 20590}, {33955, 63447}, {15462, 64951}, {13644, 41915}}, {{31207, 60556, 29634}, {40268, 14683, 19026}, {30780, 61900, 53486}, {33137, 49430, 2237}}, {0, 1}, 0, 1, 1}, -{{{59166, 20590}, {33955, 63447}, {15462, 64951}, {13644, 41915}}, {{31207, 60556, 29634}, {40268, 14683, 19026}, {30780, 61900, 53486}, {33137, 49430, 2237}}, {0, 1}, 1, 1, 1}, -{{{13310, 16851}, {6210, 52894}, {42801, 25362}, {47803, 43566}}, {{54915, 32887, 11533}, {3152, 41812, 45630}, {51206, 42624, 9681}, {38495, 47647, 32657}}, {1, 2}, 0, 0, 0}, -{{{13310, 16851}, {6210, 52894}, {42801, 25362}, {47803, 43566}}, {{54915, 32887, 11533}, {3152, 41812, 45630}, {51206, 42624, 9681}, {38495, 47647, 32657}}, {1, 2}, 1, 0, 0}, -{{{13310, 16851}, {6210, 52894}, {42801, 25362}, {47803, 43566}}, {{54915, 32887, 11533}, {3152, 41812, 45630}, {51206, 42624, 9681}, {38495, 47647, 32657}}, {1, 2}, 0, 1, 1}, -{{{13310, 16851}, {6210, 52894}, {42801, 25362}, {47803, 43566}}, {{54915, 32887, 11533}, {3152, 41812, 45630}, {51206, 42624, 9681}, {38495, 47647, 32657}}, {1, 2}, 1, 1, 1}, -{{{36693, 41736}, {35088, 51443}, {60755, 43374}, {27506, 45204}}, {{32804, 10994, 35447}, {54162, 27541, 50660}, {53607, 30955, 44642}, {55174, 8579, 60835}}, {3, 0}, 0, 0, 1}, -{{{36693, 41736}, {35088, 51443}, {60755, 43374}, {27506, 45204}}, {{32804, 10994, 35447}, {54162, 27541, 50660}, {53607, 30955, 44642}, {55174, 8579, 60835}}, {3, 0}, 1, 0, 1}, -{{{36693, 41736}, {35088, 51443}, {60755, 43374}, {27506, 45204}}, {{32804, 10994, 35447}, {54162, 27541, 50660}, {53607, 30955, 44642}, {55174, 8579, 60835}}, {3, 0}, 0, 1, 1}, -{{{36693, 41736}, {35088, 51443}, {60755, 43374}, {27506, 45204}}, {{32804, 10994, 35447}, {54162, 27541, 50660}, {53607, 30955, 44642}, {55174, 8579, 60835}}, {3, 0}, 1, 1, 1}, -{{{932, 30073}, {65269, 6462}, {33665, 48595}, {25757, 44665}}, {{50220, 54074, 11465}, {42631, 25979, 48955}, {18642, 51633, 11080}, {45347, 34836, 18812}}, {2, 3}, 0, 0, 0}, -{{{932, 30073}, {65269, 6462}, {33665, 48595}, {25757, 44665}}, {{50220, 54074, 11465}, {42631, 25979, 48955}, {18642, 51633, 11080}, {45347, 34836, 18812}}, {2, 3}, 1, 0, 0}, -{{{932, 30073}, {65269, 6462}, {33665, 48595}, {25757, 44665}}, {{50220, 54074, 11465}, {42631, 25979, 48955}, {18642, 51633, 11080}, {45347, 34836, 18812}}, {2, 3}, 0, 1, 1}, -{{{932, 30073}, {65269, 6462}, {33665, 48595}, {25757, 44665}}, {{50220, 54074, 11465}, {42631, 25979, 48955}, {18642, 51633, 11080}, {45347, 34836, 18812}}, {2, 3}, 1, 1, 1} +{{{30012, 34151}, {39233, 36935}, {271, 24990}, {27670, 42466}}, {{48774, 10944, 20622}, {63785, 32663, 11783}, {37619, 57025, 59542}, {51770, 17714, 26540}}, {1, 2}, 0, 0, 0}, +{{{30012, 34151}, {39233, 36935}, {271, 24990}, {27670, 42466}}, {{48774, 10944, 20622}, {63785, 32663, 11783}, {37619, 57025, 59542}, {51770, 17714, 26540}}, {1, 2}, 1, 0, 0}, +{{{30012, 34151}, {39233, 36935}, {271, 24990}, {27670, 42466}}, {{48774, 10944, 20622}, {63785, 32663, 11783}, {37619, 57025, 59542}, {51770, 17714, 26540}}, {1, 2}, 0, 1, 1}, +{{{30012, 34151}, {39233, 36935}, {271, 24990}, {27670, 42466}}, {{48774, 10944, 20622}, {63785, 32663, 11783}, {37619, 57025, 59542}, {51770, 17714, 26540}}, {1, 2}, 1, 1, 1}, +{{{58044, 18605}, {8363, 27460}, {20596, 32867}, {29877, 64219}}, {{58830, 5407, 27132}, {37690, 57573, 14125}, {21200, 21280, 25203}, {27946, 51466, 27728}}, {3, 1}, 0, 0, 0}, +{{{58044, 18605}, {8363, 27460}, {20596, 32867}, {29877, 64219}}, {{58830, 5407, 27132}, {37690, 57573, 14125}, {21200, 21280, 25203}, {27946, 51466, 27728}}, {3, 1}, 1, 0, 0}, +{{{58044, 18605}, {8363, 27460}, {20596, 32867}, {29877, 64219}}, {{58830, 5407, 27132}, {37690, 57573, 14125}, {21200, 21280, 25203}, {27946, 51466, 27728}}, {3, 1}, 0, 1, 1}, +{{{58044, 18605}, {8363, 27460}, {20596, 32867}, {29877, 64219}}, {{58830, 5407, 27132}, {37690, 57573, 14125}, {21200, 21280, 25203}, {27946, 51466, 27728}}, {3, 1}, 1, 1, 1}, +{{{44841, 64383}, {53907, 51878}, {4125, 47186}, {4314, 64092}}, {{8091, 63442, 25794}, {64056, 13795, 55438}, {1632, 58552, 45654}, {20593, 23365, 43335}}, {3, 1}, 0, 0, 1}, +{{{44841, 64383}, {53907, 51878}, {4125, 47186}, {4314, 64092}}, {{8091, 63442, 25794}, {64056, 13795, 55438}, {1632, 58552, 45654}, {20593, 23365, 43335}}, {3, 1}, 1, 0, 1}, +{{{44841, 64383}, {53907, 51878}, {4125, 47186}, {4314, 64092}}, {{8091, 63442, 25794}, {64056, 13795, 55438}, {1632, 58552, 45654}, {20593, 23365, 43335}}, {3, 1}, 0, 1, 1}, +{{{44841, 64383}, {53907, 51878}, {4125, 47186}, {4314, 64092}}, {{8091, 63442, 25794}, {64056, 13795, 55438}, {1632, 58552, 45654}, {20593, 23365, 43335}}, {3, 1}, 1, 1, 1}, +{{{64825, 8660}, {56183, 65347}, {35227, 53371}, {22380, 59398}}, {{62865, 12434, 57003}, {21431, 52696, 12581}, {29122, 14172, 19796}, {63023, 58609, 32026}}, {0, 1}, 0, 0, 1}, +{{{64825, 8660}, {56183, 65347}, {35227, 53371}, {22380, 59398}}, {{62865, 12434, 57003}, {21431, 52696, 12581}, {29122, 14172, 19796}, {63023, 58609, 32026}}, {0, 1}, 1, 0, 1}, +{{{64825, 8660}, {56183, 65347}, {35227, 53371}, {22380, 59398}}, {{62865, 12434, 57003}, {21431, 52696, 12581}, {29122, 14172, 19796}, {63023, 58609, 32026}}, {0, 1}, 0, 1, 1}, +{{{64825, 8660}, {56183, 65347}, {35227, 53371}, {22380, 59398}}, {{62865, 12434, 57003}, {21431, 52696, 12581}, {29122, 14172, 19796}, {63023, 58609, 32026}}, {0, 1}, 1, 1, 1}, +{{{8465, 29348}, {24312, 50863}, {28717, 7999}, {28720, 6184}}, {{28861, 42137, 46756}, {14153, 3016, 368}, {37103, 4753, 63752}, {26290, 49319, 19590}}, {0, 3}, 0, 0, 0}, +{{{8465, 29348}, {24312, 50863}, {28717, 7999}, {28720, 6184}}, {{28861, 42137, 46756}, {14153, 3016, 368}, {37103, 4753, 63752}, {26290, 49319, 19590}}, {0, 3}, 1, 0, 0}, +{{{8465, 29348}, {24312, 50863}, {28717, 7999}, {28720, 6184}}, {{28861, 42137, 46756}, {14153, 3016, 368}, {37103, 4753, 63752}, {26290, 49319, 19590}}, {0, 3}, 0, 1, 1}, +{{{8465, 29348}, {24312, 50863}, {28717, 7999}, {28720, 6184}}, {{28861, 42137, 46756}, {14153, 3016, 368}, {37103, 4753, 63752}, {26290, 49319, 19590}}, {0, 3}, 1, 1, 1}, +{{{8284, 33083}, {57292, 46861}, {34687, 37642}, {17152, 14992}}, {{6502, 62218, 4663}, {52150, 57157, 45991}, {46545, 39697, 7684}, {19618, 41669, 58034}}, {2, 0}, 0, 0, 0}, +{{{8284, 33083}, {57292, 46861}, {34687, 37642}, {17152, 14992}}, {{6502, 62218, 4663}, {52150, 57157, 45991}, {46545, 39697, 7684}, {19618, 41669, 58034}}, {2, 0}, 1, 0, 0}, +{{{8284, 33083}, {57292, 46861}, {34687, 37642}, {17152, 14992}}, {{6502, 62218, 4663}, {52150, 57157, 45991}, {46545, 39697, 7684}, {19618, 41669, 58034}}, {2, 0}, 0, 1, 1}, +{{{8284, 33083}, {57292, 46861}, {34687, 37642}, {17152, 14992}}, {{6502, 62218, 4663}, {52150, 57157, 45991}, {46545, 39697, 7684}, {19618, 41669, 58034}}, {2, 0}, 1, 1, 1}, +{{{25846, 8512}, {33690, 21927}, {25173, 10221}, {39850, 47462}}, {{50461, 5458, 49446}, {34497, 26286, 33428}, {45940, 17285, 24571}, {16372, 39484, 47255}}, {2, 3}, 0, 0, 0}, +{{{25846, 8512}, {33690, 21927}, {25173, 10221}, {39850, 47462}}, {{50461, 5458, 49446}, {34497, 26286, 33428}, {45940, 17285, 24571}, {16372, 39484, 47255}}, {2, 3}, 1, 0, 0}, +{{{25846, 8512}, {33690, 21927}, {25173, 10221}, {39850, 47462}}, {{50461, 5458, 49446}, {34497, 26286, 33428}, {45940, 17285, 24571}, {16372, 39484, 47255}}, {2, 3}, 0, 1, 1}, +{{{25846, 8512}, {33690, 21927}, {25173, 10221}, {39850, 47462}}, {{50461, 5458, 49446}, {34497, 26286, 33428}, {45940, 17285, 24571}, {16372, 39484, 47255}}, {2, 3}, 1, 1, 1}, +{{{30649, 39273}, {25739, 5209}, {7233, 24414}, {12457, 39233}}, {{42690, 51356, 34125}, {49143, 54026, 44592}, {29498, 44220, 22144}, {56871, 2985, 52252}}, {1, 0}, 0, 0, 0}, +{{{30649, 39273}, {25739, 5209}, {7233, 24414}, {12457, 39233}}, {{42690, 51356, 34125}, {49143, 54026, 44592}, {29498, 44220, 22144}, {56871, 2985, 52252}}, {1, 0}, 1, 0, 0}, +{{{30649, 39273}, {25739, 5209}, {7233, 24414}, {12457, 39233}}, {{42690, 51356, 34125}, {49143, 54026, 44592}, {29498, 44220, 22144}, {56871, 2985, 52252}}, {1, 0}, 0, 1, 1}, +{{{30649, 39273}, {25739, 5209}, {7233, 24414}, {12457, 39233}}, {{42690, 51356, 34125}, {49143, 54026, 44592}, {29498, 44220, 22144}, {56871, 2985, 52252}}, {1, 0}, 1, 1, 1}, +{{{38312, 60669}, {24957, 60315}, {29896, 64344}, {29666, 14220}}, {{5748, 21477, 43965}, {28388, 57538, 56695}, {9899, 40548, 28310}, {3105, 5027, 40231}}, {3, 1}, 0, 0, 1}, +{{{38312, 60669}, {24957, 60315}, {29896, 64344}, {29666, 14220}}, {{5748, 21477, 43965}, {28388, 57538, 56695}, {9899, 40548, 28310}, {3105, 5027, 40231}}, {3, 1}, 1, 0, 1}, +{{{38312, 60669}, {24957, 60315}, {29896, 64344}, {29666, 14220}}, {{5748, 21477, 43965}, {28388, 57538, 56695}, {9899, 40548, 28310}, {3105, 5027, 40231}}, {3, 1}, 0, 1, 1}, +{{{38312, 60669}, {24957, 60315}, {29896, 64344}, {29666, 14220}}, {{5748, 21477, 43965}, {28388, 57538, 56695}, {9899, 40548, 28310}, {3105, 5027, 40231}}, {3, 1}, 1, 1, 1}, +{{{54391, 16949}, {42417, 62947}, {12524, 62053}, {334, 36818}}, {{12729, 32134, 5604}, {59961, 63624, 40894}, {33351, 32701, 23337}, {56130, 12317, 32472}}, {2, 3}, 0, 0, 1}, +{{{54391, 16949}, {42417, 62947}, {12524, 62053}, {334, 36818}}, {{12729, 32134, 5604}, {59961, 63624, 40894}, {33351, 32701, 23337}, {56130, 12317, 32472}}, {2, 3}, 1, 0, 1}, +{{{54391, 16949}, {42417, 62947}, {12524, 62053}, {334, 36818}}, {{12729, 32134, 5604}, {59961, 63624, 40894}, {33351, 32701, 23337}, {56130, 12317, 32472}}, {2, 3}, 0, 1, 1}, +{{{54391, 16949}, {42417, 62947}, {12524, 62053}, {334, 36818}}, {{12729, 32134, 5604}, {59961, 63624, 40894}, {33351, 32701, 23337}, {56130, 12317, 32472}}, {2, 3}, 1, 1, 1} diff --git a/components/mission_protection_system/tests/README.md b/components/mission_protection_system/tests/README.md index 0bd491d1..dc79ebd9 100644 --- a/components/mission_protection_system/tests/README.md +++ b/components/mission_protection_system/tests/README.md @@ -72,7 +72,78 @@ test, that is not quite equivalent, would be look for a UI state in which at least two sensor values differ: clearly this is quite a complicated regular expression. -## License + +# Running Tests under `vm_runner` + +This section describes how to run the test suite against an instance of MPS +that is running under a guest VM managed by the OpenSUT `vm_runner`. + +First, cross-compile MPS for aarch64 in the appropriate configuration: + +```sh +# In the mission_protection_system/ directory: +./build_aarch64.sh +``` + +This will produce an aarch64 `rts.no_self_test.aarch64` binary. On Debian, the +necessary aarch64 toolchain can be installed from the `g++-aarch64-linux-gnu` +package. + +Then, in the `vm_runner` directory, prepare host and guest application images +for running MPS: + +```sh +# In the vm_runner/ directory: +bash tests/mps/build_image.sh +``` + +This copies the aarch64 `rts` binary produced above into the images. + +Start the VMs and MPS by running: + +```sh +# In the vm_runner/ directory: +cargo run -- tests/mps/base_nested.toml +``` + +This will start a host VM, a guest VM, and MPS inside the guest VM. It will +print various Linux boot messages, followed by a line like this: + +``` +[ 65.044425] opensut_boot[302]: [ 39.737218] opensut_boot[305]: Starting mps +``` + +At this point, MPS is running inside the VM. + +For manual testing, you can connect to MPS via `socat`: + +```sh +# In the vm_runner/ directory: +socat - unix:./serial.socket +``` + +This should display the usual MPS status screen and accept commands as normal. +Press ^C to disconnect (MPS will continue running). To restart MPS and reset +it to its initial state, enter the `R` (reset) command. + +In this setup, `serial.socket` on the base system is connected to an emulated +UART in the host VM, which is forwarded to an emulated UART in the guest VM, +and MPS communicates over that emulated UART. See `vm_runner/tests/mps*.toml` +for details. + +The MPS test suite can now be run against `serial.socket`, which allows testing +MPS as it runs inside the VM. In the MPS `tests/` subdirectory, run the suite +with the appropriate environment variables set: + +```sh +# In the mission_protection_system/tests/ directory: +RTS_SOCKET=/path/to/vm_runner/serial.socket python3 run_all.py +``` + +All tests should pass as normal in this configuration. + + +# License Copyright 2021, 2022, 2023 Galois, Inc. diff --git a/components/mission_protection_system/tests/run_all.py b/components/mission_protection_system/tests/run_all.py index 69619a40..5a5efa6c 100755 --- a/components/mission_protection_system/tests/run_all.py +++ b/components/mission_protection_system/tests/run_all.py @@ -18,6 +18,7 @@ import subprocess import glob import os +import sys # Turn off screen clearing ANSI os.environ["RTS_NOCLEAR"] = "1" @@ -34,18 +35,46 @@ "scenarios/exceptional_4e", ] +pass_count = 0 +fail_count = 0 +skip_count = 0 for test in sorted(glob.glob("scenarios/*")): fn, ext = os.path.splitext(test) if ext == ".cases": continue - bin = "../src/rts.no_self_test" - if fn in NEEDS_SELF_TEST: - bin = "../src/rts.self_test" - os.environ["RTS_BIN"] = bin - print(f"{fn} ({bin})") - - if os.path.exists(fn + ".cases"): - subprocess.run(["./test.py", fn, fn + ".cases"],check=True) + if not os.environ.get("RTS_SOCKET"): + bin = "../src/rts.no_self_test" + if fn in NEEDS_SELF_TEST: + bin = "../src/rts.self_test" + os.environ["RTS_BIN"] = bin + os.environ.pop("RTS_SOCKET", None) + print(f"{fn} ({bin})") else: - subprocess.run(["./test.py", fn],check=True) + if fn in NEEDS_SELF_TEST: + # Most tests require an RTS binary built with SELF_TEST=Disabled, + # but a few need SELF_TEST=Enabled instead. Since we can't switch + # binaries when testing through a socket, we run only the + # SELF_TEST=Disabled part of the test suite. + print('skipping test %r: requires SELF_TEST=Enabled' % fn) + skip_count += 1 + continue + # Remove RTS_BIN from the environment, if it's present. + os.environ.pop("RTS_BIN", None) + print(f"{fn} ({os.environ['RTS_SOCKET']})") + + + try: + if os.path.exists(fn + ".cases"): + subprocess.run(["./test.py", fn, fn + ".cases"],check=True) + else: + subprocess.run(["./test.py", fn],check=True) + pass_count += 1 + except subprocess.CalledProcessError: + import traceback + traceback.print_exc() + fail_count += 1 + +print('\n%d tests passed, %d failed, %d skipped' % (pass_count, fail_count, skip_count)) +if fail_count > 0: + sys.exit(1) diff --git a/components/mission_protection_system/tests/runner.py b/components/mission_protection_system/tests/runner.py index 87a540ac..d7021127 100755 --- a/components/mission_protection_system/tests/runner.py +++ b/components/mission_protection_system/tests/runner.py @@ -17,15 +17,18 @@ # Run a test on a single input import pexpect +import pexpect.fdpexpect import sys import os +import socket import time RTS_BIN = os.environ.get("RTS_BIN") +RTS_SOCKET = os.environ.get("RTS_SOCKET") RTS_DEBUG = os.environ.get("RTS_DEBUG") is not None -def try_expect(p,expected,timeout=1,retries=60): +def try_expect(p,expected,timeout=10,retries=10): expected = expected.strip() if RTS_DEBUG: print(f"CHECKING: {expected}") @@ -83,7 +86,15 @@ def run_script(p, cmds): return True def run(script, args): - p = pexpect.spawn(RTS_BIN) + if not RTS_SOCKET: + p = pexpect.spawn(RTS_BIN) + else: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(RTS_SOCKET) + p = pexpect.fdpexpect.fdspawn(sock.fileno()) + # Reset the RTS to its initial state. + p.sendline('R') + try_expect(p, 'RESET') time.sleep(0.1) with open(script) as f: cmds = f.readlines() diff --git a/src/pkvm_setup/create_disk_images.sh b/src/pkvm_setup/create_disk_images.sh index 5f882aeb..155c521e 100644 --- a/src/pkvm_setup/create_disk_images.sh +++ b/src/pkvm_setup/create_disk_images.sh @@ -5,27 +5,46 @@ mkdir -p vms disk_base=vms/disk_base.img disk_host=vms/disk_host.img +disk_host_dev=vms/disk_host_dev.img disk_guest=vms/disk_guest.img +disk_guest_dev=vms/disk_guest_dev.img -# Building these disk images is rather expensive, so don't overwrite existing -# ones. -for disk in "$disk_base" "$disk_host" "$disk_guest"; do - if [[ -e "$disk" ]]; then - echo "error: refusing to overwrite existing $disk" 1>&2 - #exit 1 - fi -done +if [[ -e "$disk_base" ]]; then + echo "keeping existing $disk_base" 1>&2 +else + bash debian_image/create_base_vm.sh "$disk_base" + echo "created base image $disk_base" +fi -bash debian_image/create_base_vm.sh "$disk_base" +if [[ -e "$disk_host" ]]; then + echo "keeping existing $disk_host" 1>&2 +else + bash debian_image/clone_vm.sh "$disk_base" "$disk_host" + bash run_vm_script.sh "$disk_host" debian_image/setup_host_vm.sh + echo "created host image $disk_host" +fi -bash debian_image/clone_vm.sh "$disk_base" "$disk_host" -bash run_vm_script.sh "$disk_host" debian_image/setup_host_vm.sh -# This part is optional, but convenient for interactive use: -bash run_vm_script.sh "$disk_host" debian_image/setup_host_vm_interactive.sh +if [[ -e "$disk_host_dev" ]]; then + echo "keeping existing $disk_host_dev" 1>&2 +else + bash debian_image/clone_vm.sh "$disk_base" "$disk_host_dev" + bash run_vm_script.sh "$disk_host_dev" debian_image/setup_host_vm.sh + bash run_vm_script.sh "$disk_host_dev" debian_image/setup_host_vm_interactive.sh + echo "created host dev image $disk_host_dev" +fi -bash debian_image/clone_vm.sh "$disk_base" "$disk_guest" -bash run_vm_script.sh "$disk_guest" debian_image/setup_guest_vm.sh +if [[ -e "$disk_guest" ]]; then + echo "keeping existing $disk_guest" 1>&2 +else + bash debian_image/clone_vm.sh "$disk_base" "$disk_guest" + bash run_vm_script.sh "$disk_guest" debian_image/setup_guest_vm.sh + echo "created guest image $disk_guest" +fi -echo -echo "created host image $disk_host" -echo "created guest image $disk_guest" +if [[ -e "$disk_guest_dev" ]]; then + echo "keeping existing $disk_guest_dev" 1>&2 +else + bash debian_image/clone_vm.sh "$disk_base" "$disk_guest_dev" + bash run_vm_script.sh "$disk_guest_dev" debian_image/setup_guest_vm.sh + echo "created guest dev image $disk_guest_dev" +fi diff --git a/src/pkvm_setup/debian_image/setup_guest_vm.sh b/src/pkvm_setup/debian_image/setup_guest_vm.sh index 44577f83..157820d9 100644 --- a/src/pkvm_setup/debian_image/setup_guest_vm.sh +++ b/src/pkvm_setup/debian_image/setup_guest_vm.sh @@ -25,3 +25,9 @@ edo sed -i -e "s/verse-opensut-vm/$hostname/g" /etc/hosts # Generate new SSH host keys edo rm -f /etc/ssh/ssh_host_*_key /etc/ssh/ssh_host_*_key.pub edo ssh-keygen -A + + +# Enable passwordless sudo for `user` +edo tee -a /etc/sudoers <"] +edition = "2018" + +default-run = "opensut_vm_runner" + +[dependencies] +# Latest `nix` version with MSRV <= 1.63. Rust 1.63 is what's currently +# provided in Debian Stable. +nix = "0.26.4" +shlex = "1.3.0" + +serde = { version = "1", features = ["derive"] } +toml = "0.7.3" + +log = "0.4" +env_logger = "0.10.2" + +indexmap = { version = "2.2.6", features = ["serde"] } diff --git a/src/vm_runner/README.md b/src/vm_runner/README.md new file mode 100644 index 00000000..e3b32c27 --- /dev/null +++ b/src/vm_runner/README.md @@ -0,0 +1,112 @@ +# OpenSUT VM Runner + +This is a tool for running QEMU VMs or other processes according to a config +file. The project has two binaries, corresponding to its two modes of +operation: + +* `opensut_vm_runner`: Takes a config file as an argument and runs the + processes described in that config. This is useful for running on the base + system to start up the host VM. +* `opensut_boot`: Reads a device path from the kernel command line, mounts that + device, and runs `opensut_vm_runner` on a config file found there. This is + designed to be run in the VM upon boot as a system service or `systemd.run` + command. In the future, this can be extended to check for a specific + signature before mounting the device. + +A typical boot process works like this: + +* The user runs `opensut_vm_runner` on the base system to start up the host VM. +* The host VM boots and runs `opensut_boot`. +* In the host, `opensut_boot` mounts the host application partition, reads the + config file, and starts up the guest VM. +* The guest VM boots and runs `opensut_boot`. +* In the guest, `opensut_boot` mounts the guest application partition, reads + the config file, and starts up some application process such as MPS. + + +## Setup + +First, run the setup steps in `../pkvm_setup/` to build the base disk images +for the host and guest VMs. + +There are two options for building the `vm_runner` binaries: + +1. Cross-compile for aarch64 from the local machine: + + ```sh + cargo build --release --target aarch64-unknown-linux-gnu + ``` + + This may require installing additional Rust target libraries. + +2. Compile inside a VM: + + ```sh + cargo run -- build_config.toml + ``` + + This will start up the development version of the host VM, install a Rust + toolchain (if needed), and compile `vm_runner`. This will usually be + slower than the cross-compiling option. + +Finally, install the new `vm_runner` binaries into the host and guest VMs: + +```sh +cargo run -- install_config.toml +cargo run -- install_config_guest.toml +``` + + +## Usage + +The `tests/` directory contains some example configurations. + +To run a trivial Hello World script: + +```sh +cargo run -- tests/hello_guest.toml +``` + +This should print "Hello, World!" + +The remaining tests require building Hello World application images first: + +```sh +bash tests/build_hello_image.sh +``` + +To run the Hello World script inside a VM: + +```sh +cargo run -- tests/hello_base_single.toml +``` + +This will start up a VM with an application image containing `hello_guest.toml` +and `hello.sh`. This will produce a large amount of output as Linux boots +within the VM, consisting of kernel messages, a group of "starting service" +messages, and a group of "stopped service" messages. Between the start group +and the stop group, output like the following should appear: + +``` + Starting kernel-command-li…ommand from Kernel Command Line... +[ 19.241859] squashfs: version 4.0 (2009/01/31) Phillip Lougher +[ 19.326691] opensut_boot[303]: Hello, World! +[ OK ] Finished kernel-command-li… Command from Kernel Command Line. +``` + +To run the Hello World script inside two nested VMs: + +```sh +cargo run -- tests/hello_base_nested.toml +``` + +This is similar to the previous example, but there will be two sets of Linux +startup and shutdown messages, one for the host and one for the guest. In the +middle should be output like this: + +``` +[ 68.312436] opensut_boot[301]: Starting kernel-command-li…ommand from Kernel Command Line... +[ 68.498854] opensut_boot[301]: [ 45.040450] squashfs: version 4.0 (2009/01/31) Phillip Lougher +[ 68.757693] opensut_boot[301]: [ 42.171542] opensut_boot[307]: Hello, World! +[ 68.814990] opensut_boot[301]: [ OK ] Finished kernel-command-li… Command from Kernel Command Line. +``` diff --git a/src/vm_runner/build_application_image.py b/src/vm_runner/build_application_image.py new file mode 100644 index 00000000..b4e5047b --- /dev/null +++ b/src/vm_runner/build_application_image.py @@ -0,0 +1,57 @@ +''' +Script for building an application image for use with `opensut_boot`. This is +a wrapper around `mksquashfs` that makes it easier to gather files from +multiple places into a single image. +''' + +import argparse +import os +import shutil +import subprocess +import tempfile + +def parse_args(): + parser = argparse.ArgumentParser( + description = 'build an opensut_boot application image') + parser.add_argument('--file', '-f', action='append', metavar='SRC[=DEST]', default=[], + help = 'include file SRC at path DEST (default: basename(SRC)) ' + 'in the generated image') + parser.add_argument('--dir', '-d', action='append', metavar='SRC[=DEST]', default=[], + help = 'include directory SRC at path DEST (default: basename(SRC)) ' + 'in the generated image') + parser.add_argument('--output', '-o', metavar='OUT.IMG', default='out.img', + help = 'where to write the generated image') + + return parser.parse_args() + +def main(): + args = parse_args() + + temp_dir = tempfile.TemporaryDirectory() + + for file_spec in args.file: + src, delim, dest = file_spec.partition('=') + if delim == '': + assert dest == '' + dest = os.path.basename(src) + assert not os.path.isabs(dest), 'destination path must be relative (got %r)' % dest + dest_full = os.path.join(temp_dir.name, dest) + os.makedirs(os.path.dirname(dest_full), exist_ok=True) + shutil.copyfile(src, dest_full) + shutil.copystat(src, dest_full) + + for dir_spec in args.dir: + src, delim, dest = dir_spec.partition('=') + if delim == '': + assert dest == '' + dest = os.path.basename(src) + assert not os.path.isabs(dest), 'destination path must be relative (got %r)' % dest + dest_full = os.path.join(temp_dir.name, dest) + os.makedirs(os.path.dirname(dest_full), exist_ok=True) + shutil.copytree(src, dest_full) + + subprocess.run(('mksquashfs', temp_dir.name, args.output, '-noappend'), + check = True) + +if __name__ == '__main__': + main() diff --git a/src/vm_runner/build_config.toml b/src/vm_runner/build_config.toml new file mode 100644 index 00000000..c6b740fc --- /dev/null +++ b/src/vm_runner/build_config.toml @@ -0,0 +1,19 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = false +kernel = "../pkvm_setup/vms/debian-boot/vmlinuz" +initrd = "../pkvm_setup/vms/debian-boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run="/bin/bash /dev/vdb"' + +[process.disk.vda] +format = "qcow2" +path = "../pkvm_setup/vms/disk_host_dev.img" + +[process.disk.vdb] +format = "raw" +path = "build_helper.sh" + +[process.9p.vm_runner] +path = "." diff --git a/src/vm_runner/build_helper.sh b/src/vm_runner/build_helper.sh new file mode 100644 index 00000000..19b840e5 --- /dev/null +++ b/src/vm_runner/build_helper.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -euo pipefail + +# Script for building the `vm_runner` project inside a VM. + +if [[ "$(id -u)" -eq "0" ]]; then + # Drop privileges for the rest of the script. + cp "$0" /tmp/vm-script + chown user:user /tmp/vm-script + exec sudo -u user /bin/bash /tmp/vm-script "$@" +fi + +cd ~ + +sudo apt install -y rustc cargo + +mkdir -p vm_runner +sudo mount -t 9p -o trans=virtio,version=9p2000.L,rw vm_runner vm_runner + +cd vm_runner +ls -l + +# Passing `--target` to cargo causes it to use a target-specific build +# directory. This prevents confusion between artifacts built outside and +# inside the VM when the two are sharing the same directory. +rust_target="$(rustc -vV | sed -n 's/host: //p')" +cargo build --release --target "$rust_target" + +echo "Build succeeded" diff --git a/src/vm_runner/install_config.toml b/src/vm_runner/install_config.toml new file mode 100644 index 00000000..bad2c450 --- /dev/null +++ b/src/vm_runner/install_config.toml @@ -0,0 +1,19 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = false +kernel = "../pkvm_setup/vms/debian-boot/vmlinuz" +initrd = "../pkvm_setup/vms/debian-boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run="/bin/bash /dev/vdb"' + +[process.disk.vda] +format = "qcow2" +path = "../pkvm_setup/vms/disk_host.img" + +[process.disk.vdb] +format = "raw" +path = "install_helper.sh" + +[process.9p.vm_runner] +path = "." diff --git a/src/vm_runner/install_config_guest.toml b/src/vm_runner/install_config_guest.toml new file mode 100644 index 00000000..bccca6a1 --- /dev/null +++ b/src/vm_runner/install_config_guest.toml @@ -0,0 +1,19 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = false +kernel = "../pkvm_setup/vms/debian-boot/vmlinuz" +initrd = "../pkvm_setup/vms/debian-boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run="/bin/bash /dev/vdb"' + +[process.disk.vda] +format = "qcow2" +path = "../pkvm_setup/vms/disk_guest.img" + +[process.disk.vdb] +format = "raw" +path = "install_helper.sh" + +[process.9p.vm_runner] +path = "." diff --git a/src/vm_runner/install_helper.sh b/src/vm_runner/install_helper.sh new file mode 100644 index 00000000..eb27c70c --- /dev/null +++ b/src/vm_runner/install_helper.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -euo pipefail + +# Script for installing `vm_runner` binaries into the current VM image. + +if [[ "$(id -u)" -eq "0" ]]; then + # Drop privileges for the rest of the script. + cp "$0" /tmp/vm-script + chown user:user /tmp/vm-script + exec sudo -u user /bin/bash /tmp/vm-script "$@" +fi + +cd ~ + +mkdir -p vm_runner +sudo mount -t 9p -o trans=virtio,version=9p2000.L,rw vm_runner vm_runner + +sudo mkdir -p /opt/opensut/bin +target=aarch64-unknown-linux-gnu +sudo cp -v \ + vm_runner/target/"$target"/release/opensut_vm_runner \ + vm_runner/target/"$target"/release/opensut_boot \ + /opt/opensut/bin diff --git a/src/vm_runner/src/bin/opensut_boot.rs b/src/vm_runner/src/bin/opensut_boot.rs new file mode 100644 index 00000000..1616ccb6 --- /dev/null +++ b/src/vm_runner/src/bin/opensut_boot.rs @@ -0,0 +1,7 @@ +use env_logger; +use opensut_vm_runner; + +fn main() { + env_logger::init(); + opensut_vm_runner::boot_main(); +} diff --git a/src/vm_runner/src/bin/opensut_vm_runner.rs b/src/vm_runner/src/bin/opensut_vm_runner.rs new file mode 100644 index 00000000..056a6f35 --- /dev/null +++ b/src/vm_runner/src/bin/opensut_vm_runner.rs @@ -0,0 +1,18 @@ +use std::env; +use std::process; + +use env_logger; +use opensut_vm_runner; + +fn main() { + env_logger::init(); + + let args = env::args_os().collect::>(); + if args.len() != 2 { + let cmd_name = env::args().nth(0).unwrap_or_else(|| "vm_runner".to_string()); + eprintln!("usage: {} config.toml", cmd_name); + process::exit(1); + } + + opensut_vm_runner::runner_main(&args[1]); +} diff --git a/src/vm_runner/src/config.rs b/src/vm_runner/src/config.rs new file mode 100644 index 00000000..6a152176 --- /dev/null +++ b/src/vm_runner/src/config.rs @@ -0,0 +1,285 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use indexmap::IndexMap; +use log::trace; +use serde::{Serialize, Deserialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + pub mode: Mode, + #[serde(default)] + pub process: Vec, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Mode { + /// Manage multiple processes running concurrently. Any number of processes is permitted. All + /// processes will be started, and the runner will wait for all of them to exit. If a process + /// exits unsuccessfully, all other processes will be terminated. + Manage, + /// `exec` a single command. There must be exactly one process in the config file. + Exec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Process { + Shell(ShellProcess), + Vm(VmProcess), +} + +/// Run a shell command. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ShellProcess { + pub command: String, + /// Directory to use as the current directory when running `command`. By default, this is the + /// directory containing the config file, so any paths in `command` are interpreted relative to + /// the config directory. + #[serde(default)] + pub cwd: PathBuf, +} + +/// Spawn a VM. +/// +/// This could instead be done using a `ShellProcess` that invokes QEMU, but using `type = "vm"` +/// handles the most common device options automatically. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct VmProcess { + pub kernel: PathBuf, + pub initrd: Option, + #[serde(default)] + pub append: String, + + #[serde(default = "const_u32::<1024>")] + pub ram_mb: u32, + + /// If set, use KVM. Otherwise, run emulation with no hardware support. + /// + /// Using KVM requires access to the `/dev/kvm` device. + #[serde(default = "const_bool::")] + pub kvm: bool, + + /// Disk definitions. Devices must be named sequentially as `vda`, `vdb`, and so on. They + /// will be presented to the guest in name order, so `vda` will appear as `/dev/vda`, `vdb` as + /// `/dev/vdb`, and so on. + /// + /// The disks we present to the guest will be sequentially named starting with `/dev/vda`. + /// Requiring the user to also name their config file entries sequentially means lets us ensure + /// that there's a correspondence between the names in the config file and the device names + /// within the guest. + #[serde(default)] + pub disk: HashMap, + #[serde(default)] + pub net: VmNet, + /// 9p filesystem definitions. The key will be used as the "mount tag", which must be passed + /// to `mount` in the guest to mount the filesystem. + #[serde(default, rename = "9p")] + pub fs_9p: HashMap, + /// Serial port / UART definitions. Devices must be named sequentially as `hvc0`, + /// `hvc1`, and so on. They will be presented to the guest in name order, so `hvc0` will + /// appear as `/dev/hvc0`, `hvc1` as `/dev/hvc1`, and so on. + /// + /// In addition, the default console can be configured by providing an entry named `ttyAMA0`. + /// Without such an entry, `ttyAMA0` will be automatically connected to stdio. + #[serde(default)] + pub serial: IndexMap, + /// GPIO device definitions. Devices are added in order; the first one listed in the config + /// file will be `/dev/gpiochip1`, the second will be `/dev/gpiochip2`, and so on. (Note that + /// the guest will also have a `gpiochip0` provided automatically by QEMU.) + #[serde(default)] + pub gpio: IndexMap, +} + +fn const_bool() -> bool { + B +} + +fn const_u32() -> u32 { + N +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct VmDisk { + pub format: String, + pub path: PathBuf, + #[serde(default = "const_bool::")] + pub read_only: bool, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct VmNet { + #[serde(default)] + pub port_forward: HashMap, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PortForward { + pub outer_port: u16, + pub inner_port: u16, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Vm9P { + pub path: PathBuf, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "mode", rename_all = "snake_case")] +pub enum VmSerial { + /// Connect the serial port in the guest to stdin/stdout on the host. + Stdio, + /// Pass through one of the host's serial ports to the guest. + Passthrough(PassthroughSerial), + /// Listen for a Unix socket connection on the host, and connect it to the serial port in the + /// guest. + Unix(UnixSerial), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PassthroughSerial { + pub device: PathBuf, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct UnixSerial { + pub path: PathBuf, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "mode", rename_all = "snake_case")] +pub enum VmGpio { + External, + Passthrough(PassthroughGpio), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PassthroughGpio { + pub device: PathBuf, +} + + +fn resolve_relative_path(path: &mut PathBuf, base: &Path) { + let new = base.join(&*path); + trace!("resolve_relative_path: {:?} -> {:?}", path, new); + *path = new; + //*path = base.join(&*path); +} + +impl Config { + /// Resolve any relative paths within `self` relative to `base`. + pub fn resolve_relative_paths(&mut self, base: &Path) { + let Config { mode: _, ref mut process } = *self; + for p in process { + p.resolve_relative_paths(base); + } + } +} + +impl Process { + pub fn resolve_relative_paths(&mut self, base: &Path) { + match *self { + Process::Shell(ref mut sp) => sp.resolve_relative_paths(base), + Process::Vm(ref mut vp) => vp.resolve_relative_paths(base), + } + } +} + +impl ShellProcess { + pub fn resolve_relative_paths(&mut self, base: &Path) { + let ShellProcess { command: _, ref mut cwd } = *self; + resolve_relative_path(cwd, base); + } +} + +impl VmProcess { + pub fn resolve_relative_paths(&mut self, base: &Path) { + let VmProcess { + ref mut kernel, ref mut initrd, append: _, + ram_mb: _, kvm: _, + ref mut disk, net: _, ref mut fs_9p, ref mut serial, ref mut gpio, + } = *self; + + resolve_relative_path(kernel, base); + if let Some(ref mut initrd) = *initrd { + resolve_relative_path(initrd, base); + } + + for x in disk.values_mut() { + x.resolve_relative_paths(base); + } + for x in fs_9p.values_mut() { + x.resolve_relative_paths(base); + } + for x in serial.values_mut() { + x.resolve_relative_paths(base); + } + for x in gpio.values_mut() { + x.resolve_relative_paths(base); + } + } +} + +impl VmDisk { + pub fn resolve_relative_paths(&mut self, base: &Path) { + let VmDisk { format: _, ref mut path, read_only: _ } = *self; + resolve_relative_path(path, base); + } +} + +impl Vm9P { + pub fn resolve_relative_paths(&mut self, base: &Path) { + let Vm9P { ref mut path } = *self; + resolve_relative_path(path, base); + } +} + +impl VmSerial { + pub fn resolve_relative_paths(&mut self, base: &Path) { + match *self { + VmSerial::Stdio => {}, + VmSerial::Passthrough(ref mut ps) => ps.resolve_relative_paths(base), + VmSerial::Unix(ref mut us) => us.resolve_relative_paths(base), + } + } +} + +impl PassthroughSerial { + pub fn resolve_relative_paths(&mut self, base: &Path) { + let PassthroughSerial { ref mut device } = *self; + resolve_relative_path(device, base); + } +} + +impl UnixSerial { + pub fn resolve_relative_paths(&mut self, base: &Path) { + let UnixSerial { ref mut path } = *self; + resolve_relative_path(path, base); + } +} + +impl VmGpio { + pub fn resolve_relative_paths(&mut self, base: &Path) { + match *self { + VmGpio::External => {}, + VmGpio::Passthrough(ref mut ps) => ps.resolve_relative_paths(base), + } + } +} + +impl PassthroughGpio { + pub fn resolve_relative_paths(&mut self, base: &Path) { + let PassthroughGpio { ref mut device } = *self; + resolve_relative_path(device, base); + } +} diff --git a/src/vm_runner/src/lib.rs b/src/vm_runner/src/lib.rs new file mode 100644 index 00000000..edf5c5b3 --- /dev/null +++ b/src/vm_runner/src/lib.rs @@ -0,0 +1,443 @@ +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt::Write as _; +use std::fs; +use std::io; +use std::mem; +use std::os::unix::process::CommandExt; +use std::path::Path; +use std::process::{Command, Child}; +use std::thread; +use std::time::Duration; +use log::trace; +use nix; +use nix::mount::MsFlags; +use nix::unistd::Pid; +use nix::sys::wait::{WaitStatus, WaitPidFlag}; +use shlex::Shlex; +use toml; +use crate::config::{Config, Mode, VmSerial}; + +pub mod config; + + +/// Helper for cleaning up child processes on drop. The caller is responsible for adding each +/// child as soon as it's spawned, and for removing children after `wait` indicates that they've +/// terminated. If an error occurs, the `ManagedProcesses` object will be dropped, and any child +/// processes currently registered with it will be killed. +struct ManagedProcesses { + children: HashMap, +} + +impl ManagedProcesses { + pub fn new() -> ManagedProcesses { + ManagedProcesses { + children: HashMap::new(), + } + } + + pub fn len(&self) -> usize { + self.children.len() + } + + pub fn add(&mut self, child: Child) { + let pid = child.id(); + self.children.insert(pid, child); + } + + pub fn remove(&mut self, pid: u32) -> Option { + self.children.remove(&pid) + } +} + +impl Drop for ManagedProcesses { + fn drop(&mut self) { + for (&pid, child) in &mut self.children { + let result = child.kill(); + match result { + Ok(()) => {}, + Err(e) => { + eprintln!("failed to kill child {}: {}", pid, e); + // Continue trying to kill remaining children. + }, + } + } + + for (pid, mut child) in mem::take(&mut self.children) { + let result = child.wait(); + match result { + Ok(_) => {}, + Err(e) => { + eprintln!("failed to wait for child {}: {}", pid, e); + // Continue waiting on remaining children. + }, + } + } + } +} + + +#[derive(Debug, Default)] +struct Commands { + /// Start these processes early, before the ones in `commands`. + early_commands: Vec, + commands: Vec, +} + +fn build_commands(processes: &[config::Process]) -> Commands { + let mut cmds = Commands::default(); + for process in processes { + match *process { + config::Process::Shell(ref shell) => { + let mut cmd = Command::new("/bin/sh"); + cmd.current_dir(&shell.cwd); + cmd.args(&["-c", &shell.command]); + cmds.commands.push(cmd); + }, + config::Process::Vm(ref vm) => { + build_vm_command(vm, &mut cmds); + }, + } + } + cmds +} + +fn needs_escaping_for_qemu(path: impl AsRef) -> bool { + let s = match path.as_ref().to_str() { + Some(x) => x, + // If the path isn't valid UTF-8, we can't examine it, so conservatively assume it might + // need escaping. + None => return true, + }; + s.contains(&[',', '=', ':']) +} + +fn build_vm_command(vm: &config::VmProcess, cmds: &mut Commands) { + let config::VmProcess { + ref kernel, ref initrd, ref append, + ram_mb, kvm, + ref disk, ref net, ref fs_9p, ref serial, ref gpio, + } = *vm; + + let mut vm_cmd = Command::new("qemu-system-aarch64"); + + macro_rules! args { + ($l:literal $($rest:tt)*) => {{ + vm_cmd.arg($l); + args!($($rest)*); + }}; + (($e:expr) $($rest:tt)*) => {{ + vm_cmd.arg($e); + args!($($rest)*); + }}; + ($i:ident $($rest:tt)*) => {{ + vm_cmd.arg($i); + args!($($rest)*); + }}; + () => { () }; + } + + // Basic machine configuration + args!("-M" "virt"); + args!("-smp" "4"); + args!("-m" (format!("{}", ram_mb))); + + // KVM + if kvm { + args!("-cpu" "host"); + args!("-enable-kvm"); + } else { + args!("-cpu" "cortex-a72"); + args!("-machine" "virtualization=true"); + args!("-machine" "virt,gic-version=3"); + } + + // Non-configurable devices + args!("-device" "virtio-scsi-pci,id=scsi0"); + args!("-object" "rng-random,filename=/dev/urandom,id=rng0"); + args!("-device" "virtio-rng-pci,rng=rng0"); + args!("-display" "none"); + + // Kernel and related flags + args!("-kernel" kernel); + if let Some(ref initrd) = initrd { + args!("-initrd" initrd); + } + if append.len() > 0 { + args!("-append" append); + } + + + // Serial ports + + // Set up a character device for stdio and use it for the QEMU monitor. + args!("-chardev" "stdio,mux=on,id=char_stdio,signal=off"); + args!("-mon" "chardev=char_stdio,mode=readline"); + + /// Handle serial port configuration. This will add `-chardev` definitions to `cmd` if needed, + /// and will return the `chardev` name for use with `-serial` or `-device`. + /// + /// `name` is the name of the device being configured, which is used to generate unique + /// `chardev` names and for error reporting. + fn handle_serial(vm_cmd: &mut Command, name: &str, s: &VmSerial) -> String { + match *s { + VmSerial::Stdio => "char_stdio".to_string(), + VmSerial::Passthrough(ref ps) => { + assert!(!needs_escaping_for_qemu(&ps.device), + "unsupported character in serial {} device: {:?}", name, ps.device); + let device = ps.device.to_str().unwrap(); + vm_cmd.args(&["-chardev", + &format!("serial,id=char_{},path={}", name, device)]); + format!("char_{}", name) + }, + VmSerial::Unix(ref us) => { + assert!(!needs_escaping_for_qemu(&us.path), + "unsupported character in serial {} path: {:?}", name, us.path); + let path = us.path.to_str().unwrap(); + vm_cmd.args(&["-chardev", + &format!("socket,id=char_{},path={},server=on,wait=off", name, path)]); + format!("char_{}", name) + }, + } + } + + let serial_range; + const DEFAULT_SERIAL_NAME: &str = "ttyAMA0"; + if let Some(s) = serial.get(DEFAULT_SERIAL_NAME) { + let chardev = handle_serial(&mut vm_cmd, DEFAULT_SERIAL_NAME, s); + args!("-serial" (format!("chardev:{}", chardev))); + serial_range = 0 .. serial.len() - 1; + } else { + // Default behavior: connect `ttyAMA0` to stdio + args!("-serial" "chardev:char_stdio"); + serial_range = 0 .. serial.len(); + } + + // A `virtio-serial-pci` device provides the QEMU-internal `virtio-serial-bus`, which later + // `virtconsole` devices attach to. + args!("-device" "virtio-serial-pci"); + // A single `virtio-serial-pci` provides 8 ports. + const MAX_SERIAL_DEVICES: usize = 8; + assert!(serial_range.end - serial_range.start <= MAX_SERIAL_DEVICES, + "too many serial devices (max = {})", MAX_SERIAL_DEVICES); + + for i in serial_range { + let name = format!("hvc{}", i); + let s = serial.get(&name) + .unwrap_or_else(|| panic!("non-contiguous serial port definitions: missing {}", name)); + let chardev = handle_serial(&mut vm_cmd, &name, s); + args!("-device" (format!("virtconsole,chardev={}", chardev))); + } + + + // Disks + for i in 0 .. disk.len() { + let i = u8::try_from(i).unwrap(); + let letter = (b'a' + i) as char; + let name = format!("vd{}", letter); + let d = disk.get(&name) + .unwrap_or_else(|| panic!("non-contiguous disk definitions: missing {}", name)); + // Forbid characters that require escaping in QEMU device arguments. + assert!(!needs_escaping_for_qemu(&d.path), + "unsupported character in disk {} path: {:?}", name, d.path); + assert!(["qcow2", "raw"].contains(&(&d.format as &str)), + "unsupported format for disk {}: {:?}", name, d.format); + let path = d.path.to_str().unwrap(); + let read_only = if d.read_only { "on" } else { "off" }; + args!("-drive" + (format!("if=virtio,format={},file={},read-only={}", d.format, path, read_only))); + } + + let config::VmNet { ref port_forward } = *net; + let mut netdev_str = format!("user,id=net0"); + for pf in port_forward.values() { + write!(netdev_str, ",hostfwd=tcp:127.0.0.1:{}-:{}", pf.outer_port, pf.inner_port) + .unwrap(); + } + args!("-device" "virtio-net-pci,netdev=net0"); + args!("-netdev" netdev_str); + + for (name, fs) in fs_9p { + assert!(!needs_escaping_for_qemu(name), + "unsupported character in 9p name {:?}", name); + assert!(!needs_escaping_for_qemu(&fs.path), + "unsupported character in 9p {} path: {:?}", name, fs.path); + let path = fs.path.to_str().unwrap(); + args!("-fsdev" + (format!("local,id=fs_9p__{},path={},security_model=mapped-xattr", name, path))); + args!("-device" + (format!("virtio-9p-pci,fsdev=fs_9p__{},mount_tag={}", name, name))); + } + + if gpio.len() > 0 { + args!("-object" + (format!("memory-backend-file,id=mem,size={}M,mem_path=/dev/shm,share=on", ram_mb))); + args!("-numa" "node,memdev=mem"); + } + for (_name, _g) in gpio { + // TODO: add vhost-device-gpio as an early_command, and add a -device flag to vm_cmd + todo!("gpio devices are not yet implemented"); + } + + cmds.commands.push(vm_cmd); +} + + +pub fn run_manage(cfg: &Config) -> io::Result<()> { + let mut children = ManagedProcesses::new(); + + let cmds = build_commands(&cfg.process); + + if cmds.early_commands.len() > 0 { + for mut cmd in cmds.early_commands { + trace!("spawn (early): {:?}", cmd); + let child = cmd.spawn()?; + trace!("spawned pid = {}", child.id()); + children.add(child); + } + + // Give daemons time to start up and open their sockets. + // TODO: Use a systemd-notify like protocol to wait for daemon startup. + thread::sleep(Duration::from_millis(200)); + } + + for mut cmd in cmds.commands { + trace!("spawn: {:?}", cmd); + let child = cmd.spawn()?; + trace!("spawned pid = {}", child.id()); + children.add(child); + } + + // Await all children. If any child returns nonzero, kill all other children. + while children.len() > 0 { + trace!("waitpid..."); + let status = nix::sys::wait::waitid(nix::sys::wait::Id::All, WaitPidFlag::WEXITED)?; + trace!("waitpid returned {:?}", status); + let mut remove_child = |pid: Pid| { + let pid = u32::try_from(pid.as_raw()).unwrap(); + children.remove(pid); + }; + match status { + WaitStatus::Exited(pid, exit_code) => { + if exit_code == 0 { + // Normal exit. Just remove the child. + remove_child(pid); + } else { + // Abnormal exit. + remove_child(pid); + panic!("process {} exited unexpectedly with code {}", pid, exit_code); + } + }, + WaitStatus::Signaled(pid, signal, _) => { + // Killed by a signal. + remove_child(pid); + panic!("process {} was killed unexpectedly by signal {:?}", pid, signal); + }, + WaitStatus::Stopped(pid, signal) => { + // Process suspended by SIGSTOP or similar. We should never receive this event, + // since we don't include `WUNTRACED`/`WSTOPPED` in the `waidpid` call above. + // + // The child is still alive, so don't remove it from `children`. + panic!("impossible: waitpid reported that {} stopped due to signal {:?}, \ + but we did not request such events", pid, signal); + }, + WaitStatus::Continued(pid) => { + // Process continued due to SIGCONT or similar. We should never receive this + // event, since we don't include `WCONTINUED` in the `waidpid` call above. + // + // The child is still alive, so don't remove it from `children`. + panic!("impossible: waitpid reported that {} continued due to a signal, \ + but we did not request such events", pid); + }, + WaitStatus::PtraceEvent(pid, _, event) => { + // Stopped by ptrace. This can happen when the child invokes `PTRACE_TRACEME`. + // + // The child is still alive, so don't remove it from `children`. + panic!("process {} unexpectedly stopped with ptrace event {}", pid, event); + }, + WaitStatus::PtraceSyscall(pid) => { + // Stopped by ptrace due to a syscall. We should never receive this event, since + // we don't enable syscall tracing on any children. + // + // The child is still alive, so don't remove it from `children`. + panic!("impossible: waitpid reported that {} stopped due to a ptrace syscall, \ + but we did not enable syscall tracing", pid); + }, + WaitStatus::StillAlive => { + // No state change; process is still alive. We should never receive this event, + // since we don't include `WNOHANG` in the `waidpid` call above. + panic!("impossible: waitpid reported no changes, \ + but we expected it to block until a change occurred"); + }, + } + } + + Ok(()) +} + +pub fn run_exec(cfg: &Config) -> io::Result<()> { + assert!(cfg.process.len() == 1, + "config error: `mode = 'exec'` requires exactly one entry in `processes`"); + + let mut cmds = build_commands(&cfg.process); + assert!(cmds.commands.len() == 1, + "impossible: one `Process` produced multiple main `Command`s"); + assert!(cmds.early_commands.len() == 0, + "process requires running helpers, which `mode = 'exec'` does not support"); + + let mut cmd = cmds.commands.pop().unwrap(); + trace!("exec: {:?}", cmd); + let err = cmd.exec(); + trace!("exec error: {}", err); + Err(err) +} + + +pub fn runner_main(config_path: impl AsRef) { + let config_path = config_path.as_ref(); + let config_str = fs::read_to_string(config_path).unwrap(); + let mut cfg: Config = toml::from_str(&config_str).unwrap(); + cfg.resolve_relative_paths(config_path.parent().unwrap()); + + trace!("parsed config = {:?}", cfg); + + match cfg.mode { + Mode::Manage => run_manage(&cfg).unwrap(), + Mode::Exec => run_exec(&cfg).unwrap(), + } +} + + +pub fn boot_main() { + // Find the device containing the application partition. + let cmdline = fs::read_to_string("/proc/cmdline").unwrap(); + let mut app_device = None; + for arg in Shlex::new(&cmdline) { + let (key, value) = match arg.split_once('=') { + Some(x) => x, + None => continue, + }; + if key != "opensut.app_device" { + continue; + } + app_device = Some(value.to_owned()); + } + let app_device = app_device + .unwrap_or_else(|| panic!("missing opensut.app_device in kernel command line")); + + // TODO: Open the device and check its signature + + // Mount the application device + const APP_MOUNT_POINT: &str = "/opt/opensut/app"; + fs::create_dir_all(APP_MOUNT_POINT).unwrap(); + nix::mount::mount( + Some(&app_device as &str), + APP_MOUNT_POINT, + Some("squashfs"), + MsFlags::MS_RDONLY, + None::<&str>, // No filesystem-specific data + ).unwrap(); + + // Start the runner using the application's config file + runner_main(Path::new(APP_MOUNT_POINT).join("runner.toml")); +} diff --git a/src/vm_runner/tests/hello/base_nested.toml b/src/vm_runner/tests/hello/base_nested.toml new file mode 100644 index 00000000..3580d1be --- /dev/null +++ b/src/vm_runner/tests/hello/base_nested.toml @@ -0,0 +1,21 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = false +kernel = "../../../pkvm_setup/vms/debian-boot/vmlinuz" +initrd = "../../../pkvm_setup/vms/debian-boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run=/opt/opensut/bin/opensut_boot opensut.app_device=/dev/vdc' + +[process.disk.vda] +format = "qcow2" +path = "../../../pkvm_setup/vms/disk_host.img" + +[process.disk.vdb] +format = "qcow2" +path = "../../../pkvm_setup/vms/disk_guest.img" + +[process.disk.vdc] +format = "raw" +path = "host.img" +read_only = true diff --git a/src/vm_runner/tests/hello/base_single.toml b/src/vm_runner/tests/hello/base_single.toml new file mode 100644 index 00000000..47e68965 --- /dev/null +++ b/src/vm_runner/tests/hello/base_single.toml @@ -0,0 +1,17 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = false +kernel = "../../../pkvm_setup/vms/debian-boot/vmlinuz" +initrd = "../../../pkvm_setup/vms/debian-boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run=/opt/opensut/bin/opensut_boot opensut.app_device=/dev/vdb' + +[process.disk.vda] +format = "qcow2" +path = "../../../pkvm_setup/vms/disk_host.img" + +[process.disk.vdb] +format = "raw" +path = "guest.img" +read_only = true diff --git a/src/vm_runner/tests/hello/build_img.sh b/src/vm_runner/tests/hello/build_img.sh new file mode 100644 index 00000000..2fecde1a --- /dev/null +++ b/src/vm_runner/tests/hello/build_img.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail + +hello_dir="$(dirname "$0")" + +python3 "$hello_dir/../../build_application_image.py" \ + -f "$hello_dir/guest.toml=runner.toml" \ + -f "$hello_dir/hello.sh" \ + -o "$hello_dir/guest.img" + +python3 "$hello_dir/../../build_application_image.py" \ + -f "$hello_dir/host.toml=runner.toml" \ + -f "$hello_dir/guest.img" \ + -o "$hello_dir/host.img" + diff --git a/src/vm_runner/tests/hello/guest.toml b/src/vm_runner/tests/hello/guest.toml new file mode 100644 index 00000000..9f51b2cf --- /dev/null +++ b/src/vm_runner/tests/hello/guest.toml @@ -0,0 +1,6 @@ +mode = "exec" + +[[process]] +type = "shell" +command = "./hello.sh" + diff --git a/src/vm_runner/tests/hello/hello.sh b/src/vm_runner/tests/hello/hello.sh new file mode 100755 index 00000000..1e1c90a8 --- /dev/null +++ b/src/vm_runner/tests/hello/hello.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo 'Hello, World!' diff --git a/src/vm_runner/tests/hello/host.toml b/src/vm_runner/tests/hello/host.toml new file mode 100644 index 00000000..1f443f7e --- /dev/null +++ b/src/vm_runner/tests/hello/host.toml @@ -0,0 +1,17 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = true +kernel = "/boot/vmlinuz" +initrd = "/boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run=/opt/opensut/bin/opensut_boot opensut.app_device=/dev/vdb' + +[process.disk.vda] +format = "raw" +path = "/dev/vdb" + +[process.disk.vdb] +format = "raw" +path = "/opt/opensut/app/guest.img" +read_only = true diff --git a/src/vm_runner/tests/manage_exit_error.toml b/src/vm_runner/tests/manage_exit_error.toml new file mode 100644 index 00000000..27e163c9 --- /dev/null +++ b/src/vm_runner/tests/manage_exit_error.toml @@ -0,0 +1,10 @@ +mode = "manage" + +[[process]] +type = "shell" +command = "sleep 10" + +[[process]] +type = "shell" +command = "false" + diff --git a/src/vm_runner/tests/manage_exit_ok.toml b/src/vm_runner/tests/manage_exit_ok.toml new file mode 100644 index 00000000..958a1622 --- /dev/null +++ b/src/vm_runner/tests/manage_exit_ok.toml @@ -0,0 +1,10 @@ +mode = "manage" + +[[process]] +type = "shell" +command = "sleep 2" + +[[process]] +type = "shell" +command = "sleep 1" + diff --git a/src/vm_runner/tests/mps/base_nested.toml b/src/vm_runner/tests/mps/base_nested.toml new file mode 100644 index 00000000..84ed17f3 --- /dev/null +++ b/src/vm_runner/tests/mps/base_nested.toml @@ -0,0 +1,25 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = false +kernel = "../../../pkvm_setup/vms/debian-boot/vmlinuz" +initrd = "../../../pkvm_setup/vms/debian-boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run=/opt/opensut/bin/opensut_boot opensut.app_device=/dev/vdc' + +[process.disk.vda] +format = "qcow2" +path = "../../../pkvm_setup/vms/disk_host.img" + +[process.disk.vdb] +format = "qcow2" +path = "../../../pkvm_setup/vms/disk_guest.img" + +[process.disk.vdc] +format = "raw" +path = "host.img" +read_only = true + +[process.serial.hvc0] +mode = "unix" +path = "../../serial.socket" diff --git a/src/vm_runner/tests/mps/base_single.toml b/src/vm_runner/tests/mps/base_single.toml new file mode 100644 index 00000000..acaae0c8 --- /dev/null +++ b/src/vm_runner/tests/mps/base_single.toml @@ -0,0 +1,21 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = false +kernel = "../../../pkvm_setup/vms/debian-boot/vmlinuz" +initrd = "../../../pkvm_setup/vms/debian-boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run=/opt/opensut/bin/opensut_boot opensut.app_device=/dev/vdb' + +[process.disk.vda] +format = "qcow2" +path = "../../../pkvm_setup/vms/disk_host.img" + +[process.disk.vdb] +format = "raw" +path = "guest.img" +read_only = true + +[process.serial.hvc0] +mode = "unix" +path = "../../serial.socket" diff --git a/src/vm_runner/tests/mps/build_img.sh b/src/vm_runner/tests/mps/build_img.sh new file mode 100644 index 00000000..b39a9629 --- /dev/null +++ b/src/vm_runner/tests/mps/build_img.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -euo pipefail + +mps_dir="$(dirname "$0")" + +python3 "$mps_dir/../../build_application_image.py" \ + -f "$mps_dir/guest.toml=runner.toml" \ + -f "$mps_dir/../../../../components/mission_protection_system/src/rts.no_self_test.aarch64=mps" \ + -f "$mps_dir/mps.sh" \ + -o "$mps_dir/guest.img" + +python3 "$mps_dir/../../build_application_image.py" \ + -f "$mps_dir/host.toml=runner.toml" \ + -f "$mps_dir/guest.img" \ + -o "$mps_dir/host.img" + diff --git a/src/vm_runner/tests/mps/guest.toml b/src/vm_runner/tests/mps/guest.toml new file mode 100644 index 00000000..7cfa38c6 --- /dev/null +++ b/src/vm_runner/tests/mps/guest.toml @@ -0,0 +1,6 @@ +mode = "exec" + +[[process]] +type = "shell" +command = "./mps.sh" + diff --git a/src/vm_runner/tests/mps/host.toml b/src/vm_runner/tests/mps/host.toml new file mode 100644 index 00000000..5431f3d8 --- /dev/null +++ b/src/vm_runner/tests/mps/host.toml @@ -0,0 +1,21 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = true +kernel = "/boot/vmlinuz" +initrd = "/boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run=/opt/opensut/bin/opensut_boot opensut.app_device=/dev/vdb' + +[process.disk.vda] +format = "raw" +path = "/dev/vdb" + +[process.disk.vdb] +format = "raw" +path = "/opt/opensut/app/guest.img" +read_only = true + +[process.serial.hvc0] +mode = "passthrough" +device = "/dev/hvc0" diff --git a/src/vm_runner/tests/mps/mps.sh b/src/vm_runner/tests/mps/mps.sh new file mode 100755 index 00000000..e7b1320f --- /dev/null +++ b/src/vm_runner/tests/mps/mps.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail + +mps_bin="$(dirname "$0")/mps" +sha256sum "$mps_bin" +echo "Starting mps" +setsid -c "$mps_bin" /dev/hvc0 2>/dev/hvc0 +echo "mps exited with code $?" diff --git a/src/vm_runner/tests/mps_tests/base_nested.toml b/src/vm_runner/tests/mps_tests/base_nested.toml new file mode 100644 index 00000000..3580d1be --- /dev/null +++ b/src/vm_runner/tests/mps_tests/base_nested.toml @@ -0,0 +1,21 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = false +kernel = "../../../pkvm_setup/vms/debian-boot/vmlinuz" +initrd = "../../../pkvm_setup/vms/debian-boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run=/opt/opensut/bin/opensut_boot opensut.app_device=/dev/vdc' + +[process.disk.vda] +format = "qcow2" +path = "../../../pkvm_setup/vms/disk_host.img" + +[process.disk.vdb] +format = "qcow2" +path = "../../../pkvm_setup/vms/disk_guest.img" + +[process.disk.vdc] +format = "raw" +path = "host.img" +read_only = true diff --git a/src/vm_runner/tests/mps_tests/base_single.toml b/src/vm_runner/tests/mps_tests/base_single.toml new file mode 100644 index 00000000..47e68965 --- /dev/null +++ b/src/vm_runner/tests/mps_tests/base_single.toml @@ -0,0 +1,17 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = false +kernel = "../../../pkvm_setup/vms/debian-boot/vmlinuz" +initrd = "../../../pkvm_setup/vms/debian-boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run=/opt/opensut/bin/opensut_boot opensut.app_device=/dev/vdb' + +[process.disk.vda] +format = "qcow2" +path = "../../../pkvm_setup/vms/disk_host.img" + +[process.disk.vdb] +format = "raw" +path = "guest.img" +read_only = true diff --git a/src/vm_runner/tests/mps_tests/build_img.sh b/src/vm_runner/tests/mps_tests/build_img.sh new file mode 100644 index 00000000..859a3c97 --- /dev/null +++ b/src/vm_runner/tests/mps_tests/build_img.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +mps_dir="$(dirname "$0")" + +python3 "$mps_dir/../../build_application_image.py" \ + -f "$mps_dir/guest.toml=runner.toml" \ + -f "$mps_dir/../../../../components/mission_protection_system/src/rts.self_test.aarch64=src/rts.self_test" \ + -f "$mps_dir/../../../../components/mission_protection_system/src/rts.no_self_test.aarch64=src/rts.no_self_test" \ + -d "$mps_dir/../../../../components/mission_protection_system/tests" \ + -f "$mps_dir/mps_tests.sh" \ + -o "$mps_dir/guest.img" + +python3 "$mps_dir/../../build_application_image.py" \ + -f "$mps_dir/host.toml=runner.toml" \ + -f "$mps_dir/guest.img" \ + -o "$mps_dir/host.img" + diff --git a/src/vm_runner/tests/mps_tests/guest.toml b/src/vm_runner/tests/mps_tests/guest.toml new file mode 100644 index 00000000..ecc2a851 --- /dev/null +++ b/src/vm_runner/tests/mps_tests/guest.toml @@ -0,0 +1,6 @@ +mode = "exec" + +[[process]] +type = "shell" +command = "./mps_tests.sh" + diff --git a/src/vm_runner/tests/mps_tests/host.toml b/src/vm_runner/tests/mps_tests/host.toml new file mode 100644 index 00000000..1f443f7e --- /dev/null +++ b/src/vm_runner/tests/mps_tests/host.toml @@ -0,0 +1,17 @@ +mode = "exec" + +[[process]] +type = "vm" +kvm = true +kernel = "/boot/vmlinuz" +initrd = "/boot/initrd.img" +append = 'earlycon root=/dev/vda2 systemd.run=/opt/opensut/bin/opensut_boot opensut.app_device=/dev/vdb' + +[process.disk.vda] +format = "raw" +path = "/dev/vdb" + +[process.disk.vdb] +format = "raw" +path = "/opt/opensut/app/guest.img" +read_only = true diff --git a/src/vm_runner/tests/mps_tests/mps_tests.sh b/src/vm_runner/tests/mps_tests/mps_tests.sh new file mode 100755 index 00000000..bb40cfc0 --- /dev/null +++ b/src/vm_runner/tests/mps_tests/mps_tests.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -euo pipefail + +apt install -y python3-pexpect + +echo "Starting test suite" +cd tests +RTS_DEBUG=1 python3 run_all.py