import os from pathlib import Path import shutil import subprocess import tempfile import unittest PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload" PROGRAM = r""" #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include static const char *ctl; static size_t size, page; static volatile int stop; static unsigned char *shared; static long status(const char *key) { FILE *f = fopen("/proc/self/status", "r"); char line[256]; long v = -1; while (f && fgets(line, sizeof(line), f)) if (!strncmp(line, key, strlen(key))) v = atol(line + strlen(key)); if (f) fclose(f); return v; } static void request(char mode) { int fd = open(ctl, O_WRONLY | O_TRUNC); if (fd < 0 || write(fd, &mode, 1) != 1) exit(3); close(fd); kill(getpid(), SIGURG); } static uint64_t sum(const unsigned char *p, size_t n) { uint64_t s = 0; for (size_t i = 0; i < n; i += 64) s = s * 31 + p[i]; return s; } static void *writer(void *arg) { unsigned char *mine = shared + (size_t)(uintptr_t)arg * (size / 4); for (unsigned char round = 1; !stop; round++) { for (size_t i = 0; i < size / 4; i += page) { if (round > 1 && mine[i] != (unsigned char)(round - 1)) { printf("corrupt\n"); exit(4); } mine[i] = round; } } return NULL; } int main(int argc, char **argv) { ctl = argv[1]; size = strtoul(argv[2], NULL, 0) << 20; int cycles = atoi(argv[3]); page = (size_t)sysconf(_SC_PAGESIZE); unsigned char *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); for (size_t i = 0; i < size; i++) data[i] = (unsigned char)(i * 7 + i / 4096); uint64_t before = sum(data, size); shared = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); memset(shared, 0, size); pthread_t t[2]; for (uintptr_t i = 0; i < 2; i++) pthread_create(&t[i], NULL, writer, (void *)i); struct sigaction sa; sigaction(SIGURG, NULL, &sa); printf("handler %d\n", (sa.sa_flags & SA_SIGINFO) ? 1 : 0); if (argc > 4) { unsigned char *ro = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); memset(ro, 0x5a, size); mprotect(ro, size, PROT_READ); madvise(ro, size, 21); printf("elsewhere %ld\n", status("VmSwap:")); request('0'); printf("back %ld\n", status("VmSwap:")); } for (int c = 0; c < cycles; c++) { request('1'); printf("out %ld\n", status("VmSwap:")); request('0'); printf("back %ld\n", status("VmSwap:")); } stop = 1; for (int i = 0; i < 2; i++) pthread_join(t[i], NULL); printf("intact %d\n", sum(data, size) == before); return 0; } """ def swap_total_kb(): for line in Path("/proc/meminfo").read_text().splitlines(): if line.startswith("SwapTotal:"): return int(line.split()[1]) return 0 @unittest.skipUnless(shutil.which("cc"), "needs a C compiler") class ZramPreloadTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.dir = Path(tempfile.mkdtemp()) cls.ctl = cls.dir / "ctl" build = cls.dir / "build" build.mkdir() (build / "program.c").write_text(PROGRAM) cls.lib = build / "libblsession.so" subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-Wall", "-pthread", "-DZR_CTL=\"%s\"" % cls.ctl, "-o", str(cls.lib), *map(str, sorted(PRELOAD.glob("*.c"))), "-ldl"], check=True) cls.steam = cls.dir / "steam" subprocess.run(["cc", "-O2", "-pthread", "-o", str(cls.steam), str(build / "program.c")], check=True) cls.game = cls.dir / "game" shutil.copy(cls.steam, cls.game) @classmethod def tearDownClass(cls): shutil.rmtree(cls.dir, True) def setUp(self): self.ctl.write_text("0") for ack in self.dir.glob("ctl.ack.*"): ack.unlink() def run_program(self, program=None, mb=48, cycles=1, *extra): environment = dict(os.environ, LD_PRELOAD=str(self.lib)) result = subprocess.run([str(program or self.steam), str(self.ctl), str(mb), str(cycles), *extra], env=environment, capture_output=True, text=True, check=True, timeout=120) out = {} for line in result.stdout.splitlines(): key, value = line.split() out.setdefault(key, []).append(int(value)) return out def test_every_process_takes_the_signal(self): self.assertEqual([1], self.run_program()["handler"]) self.assertEqual([1], self.run_program(self.game)["handler"]) self.assertEqual(2, len(list(self.dir.glob("ctl.ack.*")))) def test_nothing_is_installed_without_the_control_file(self): self.ctl.unlink() try: self.assertEqual([0], self.run_program(cycles=0)["handler"]) finally: self.ctl.write_text("0") @unittest.skipUnless(swap_total_kb() > 256 * 1024, "needs swap") def test_pages_out_and_reads_back(self): out = self.run_program(mb=48) self.assertGreater(out["out"][0], 32 * 1024) self.assertLess(out["back"][0], out["out"][0] // 4) self.assertEqual([1], out["intact"]) self.assertEqual(["1"], [ack.read_text() for ack in self.dir.glob("ctl.ack.*")]) @unittest.skipUnless(swap_total_kb() > 256 * 1024, "needs swap") def test_reads_back_what_the_kernel_swapped_elsewhere(self): out = self.run_program(None, 32, 0, "read-only") self.assertGreater(out["elsewhere"][0], 16 * 1024) self.assertLess(out["back"][0], out["elsewhere"][0] // 4) def test_memory_stays_intact_under_writers(self): out = self.run_program(mb=32, cycles=6) self.assertEqual([1], out["intact"]) self.assertEqual(6, len(out["out"])) if __name__ == "__main__": unittest.main()