/* * Copyright (C) 2014 Apple Inc. All rights reserved. * * This document is the property of Apple Inc. * It is considered confidential and proprietary. * * This document may not be reproduced or transmitted in any form, * in whole or in part, without the express written permission of * Apple Inc. */ #include #include #include #include #include #include #include #include #include #include #define VERBOSITY FAILURE #define FAILURE 0 #define INFO 1 #define SPEW 2 static size_t tests_run, tests_passed; enum Reason { REASON_OK = 0, REASON_EXPECTED_PANIC, REASON_UNEXPECTED_PANIC, REASON_PARAMS, REASON_LEVEL, REASON_OOM, REASON_PTE_ALIGNMENT, REASON_PTE_BASE, REASON_PTE_INDEX, REASON_PTE_ACCESS, REASON_PTE_MISMATCH }; static void test_unwind(enum Reason reason) __attribute__((noreturn)); #define TEST_ASSERT(reason, c) \ do { \ if (!(c)) { \ printf("!!! Test assertion failure: %s\n", \ #c ); \ test_unwind(reason); \ } \ } while (0) struct State { // Environment constants for an individual test. const char *test_name; const char *expect_panic_func; const char *expect_panic_substr; size_t level_entries[4]; void *page_tables_base; size_t page_tables_size; size_t total_stack_size; unsigned page_granule_shift; jmp_buf jmp_env; // Updated during test. bool arm_flush_tlbs_called; // Callbacks from test. void (^mmu_setup_func)(); void (^get_table_entry_hook)(); void (^set_table_entry_hook)(); }; static struct State state; unsigned get_page_granule_shift(void) { return state.page_granule_shift; } void mmu_get_memmap(addr_t *page_tables_base, size_t *page_tables_size) { *page_tables_base = (addr_t) state.page_tables_base; *page_tables_size = state.page_tables_size; } // Access to mmu.c static functions and variables. uint64_t *mmu_get_tt_top_level(void); unsigned mmu_get_start_level(void); uint64_t mmu_get_tcr(void); addr_t mmu_tt_alloc(unsigned level); size_t mmu_get_level_entries(unsigned level); uint64_t mmu_get_table_flags(void); uint64_t mmu_get_block_flags(unsigned level, arm_mmu_extended_attr_t attr); size_t mmu_map_at_level(unsigned level, addr_t table_base, addr_t vaddr, addr_t paddr, size_t size, arm_mmu_extended_attr_t attr); void mmu_get_tt_alloc_range(addr_t *start, addr_t *next, addr_t *end); void mmu_reset_state(void); const char *reason_to_string(int reason) { switch (reason) { case REASON_OK: return "OK"; case REASON_EXPECTED_PANIC: return "Expected panic() called"; case REASON_UNEXPECTED_PANIC: return "Unexpected panic() called"; case REASON_PARAMS: return "Bad parameters detected"; case REASON_LEVEL: return "Invalid level used"; case REASON_OOM: return "Test ran out of memory for structures"; case REASON_PTE_ALIGNMENT: return "Bad page table alignment"; case REASON_PTE_BASE: return "Page table base does not match allocated"; case REASON_PTE_INDEX: return "Page table index out of bounds"; case REASON_PTE_ACCESS: return "Page table unexpected access"; case REASON_PTE_MISMATCH: return "Page table value mismatch"; default: printf("Bad reason code %d\n", (int) reason); abort(); } } void mmu_printf(int verbosity, const char *fmt, ...) { if (VERBOSITY >= verbosity) { va_list va; va_start(va, fmt); vprintf(fmt, va); va_end(va); } } static void test_unwind(enum Reason reason) { assert(reason != REASON_OK); longjmp(state.jmp_env, (int) reason); } void _panic(const char *func, const char *fmt, ...) { char *buf = malloc(4096); va_list va; va_start(va, fmt); vsnprintf(buf, 4096, fmt, va); va_end(va); bool expected = state.expect_panic_func && state.expect_panic_substr && strstr(func, state.expect_panic_func) != NULL && strstr(buf, state.expect_panic_substr) != NULL; if (!expected) { mmu_printf(FAILURE, "!!! Unexpected panic in function %s: %s\n", func, buf); } else { mmu_printf(INFO, "Expected panic in function %s: %s\n", func, buf); } free(buf); if (expected) { test_unwind(REASON_EXPECTED_PANIC); } else if (strstr(buf, "Out of table allocation space")) { test_unwind(REASON_OOM); } else { test_unwind(REASON_UNEXPECTED_PANIC); } } static void expect_panic(const char *func, const char *substr) { state.expect_panic_func = func; state.expect_panic_substr = substr; } uint64_t check_valid_index(unsigned level, addr_t table_base, size_t index) { if (level < mmu_get_start_level() || level > 3) { mmu_printf(FAILURE, "L%u invalid level\n", level); test_unwind(REASON_LEVEL); } void *p = NULL; size_t table_size = mmu_get_level_entries(level) * sizeof(uint64_t); size_t table_entries = mmu_get_level_entries(level); uint64_t alloc_start, alloc_next, alloc_end; mmu_get_tt_alloc_range(&alloc_start, &alloc_next, &alloc_end); if (table_base < alloc_start || table_size > alloc_next || table_base > alloc_next - table_size) { mmu_printf(FAILURE, "L%u base 0x%" PRIx64 " size 0x%" PRIx64 " outside allocated space\n", level, (uint64_t) table_base, (uint64_t) table_size); test_unwind(REASON_PTE_BASE); } if (index > table_entries) { mmu_printf(FAILURE, "L%u entry %u >= table_entries %u\n", level, (unsigned) index, (unsigned) table_entries); test_unwind(REASON_PTE_INDEX); } p = (void *) (table_base + index * 8); uint64_t result; memcpy(&result, p, sizeof(result)); return result; } static void get_table_entry_internal(unsigned level, addr_t table_base, size_t index) { mmu_printf(SPEW, "get_table_entry(%u, 0x%" PRIx64 ", %u) ", level, (uint64_t) table_base, (unsigned) index); uint64_t result = check_valid_index(level, table_base, index); mmu_printf(SPEW, "= 0x%016" PRIx64 "\n", result); } void get_table_entry_hook(unsigned level, addr_t table_base, size_t index) { if (state.get_table_entry_hook) { state.get_table_entry_hook(level, table_base, index); return; } else { get_table_entry_internal(level, table_base, index); } } static void set_table_entry_internal(unsigned level, addr_t table_base, size_t index, uint64_t value) { mmu_printf(SPEW, "set_table_entry(%u, 0x%" PRIx64 ", %u, " "0x%" PRIx64 ") ", level, (uint64_t) table_base, (unsigned) index, value); uint64_t old = check_valid_index(level, table_base, index); mmu_printf(SPEW, " was 0x%016" PRIx64 "\n", old); } void set_table_entry_hook(unsigned level, addr_t table_base, size_t index, uint64_t value) { if (state.set_table_entry_hook) { state.set_table_entry_hook(level, table_base, index, value); return; } else { set_table_entry_internal(level, table_base, index, value); } } // Mock HAL. uint64_t arm_read_sctlr(void) { mmu_printf(INFO, "arm_read_sctlr()\n"); return 0; } void arm_write_mair(uint64_t mair) { mmu_printf(INFO, "arm_write_mair(0x%016" PRIx64 ")\n", (uint64_t) mair); } void arm_write_tcr(uint64_t tcr) { mmu_printf(INFO, "arm_write_tcr(0x%016" PRIx64 ")\n", (uint64_t) tcr); } void arm_write_ttbr0(void *ttbr0) { mmu_printf(INFO, "arm_write_ttbr0(%p)\n", ttbr0); if ((addr_t) ttbr0 != (addr_t) mmu_get_tt_top_level()) { mmu_printf(FAILURE, "TTBR0 written with %p expected %p\n", ttbr0, mmu_get_tt_top_level()); test_unwind(REASON_PTE_BASE); } } void arm_flush_tlbs(void) { mmu_printf(INFO, "arm_flush_tlbs()\n"); state.arm_flush_tlbs_called = true; } void platform_mmu_setup(bool resume) { (void) resume; uint64_t alloc_start, alloc_next, alloc_end; mmu_get_tt_alloc_range(&alloc_start, &alloc_next, &alloc_end); mmu_printf(INFO, "platform_mmu_setup(%d)\n", (int) resume); mmu_printf(INFO, "tt_top_level = %p\n", mmu_get_tt_top_level()); mmu_printf(INFO, "page_tables_base = %p\n", state.page_tables_base); mmu_printf(INFO, "page_tables_size = 0x%" PRIx64 "\n", (uint64_t) state.page_tables_size); mmu_printf(INFO, "tt_alloc_start = 0x%" PRIx64 "\n" "tt_alloc_next = 0x%" PRIx64 "\n" "tt_alloc_end = 0x%" PRIx64 "\n", (uint64_t) alloc_start, (uint64_t) alloc_next, (uint64_t) alloc_end); if (state.mmu_setup_func) { state.mmu_setup_func(); } } // Test setup functions. static void reset_state(void) { mmu_reset_state(); assert(state.page_tables_base == NULL); // Prevent leak. memset(&state, 0, sizeof(state)); } static void mmu_setup_flat(void) { uint64_t io_base = 0x200000000ULL; uint64_t io_size = 256ULL * 1024 * 1024; uint64_t sdram_base = 0x800000000ULL; uint64_t sdram_len = 1ULL << 30; arm_mmu_map_rw(sdram_base, sdram_len); arm_mmu_map_device_rw(io_base, io_size); } static void mmu_setup_fail_remap_existing(void) { uint64_t io_base = 0x200000000ULL; uint64_t io_size = 256ULL * 1024 * 1024; arm_mmu_map_rw(io_base, io_size); // Map again arm_mmu_map_device_rw(io_base, io_size); } static void mmu_setup_fail_breaking_down(void) { uint64_t io_base = 0x200000000ULL; uint64_t io_size = 256ULL * 1024 * 1024; arm_mmu_map_rw(io_base, io_size); // Map one L3 page again arm_mmu_map_rw(io_base, 4096); } static void mmu_setup_misaligned(void) { uint64_t sdram_base = 0x800000000ULL; uint64_t sdram_len = 1ULL << 30; arm_mmu_map_rw(sdram_base, sdram_len + 512); } static void test_delete(void) { uint64_t alloc_start, alloc_next, alloc_end; mmu_get_tt_alloc_range(&alloc_start, &alloc_next, &alloc_end); mmu_printf(INFO, "Allocation space used: 0x%" PRIx64 "\n", alloc_next - alloc_start); mmu_printf(INFO, "Allocation space remaining: 0x%" PRIx64 "\n", alloc_end - alloc_next); if (state.page_tables_base) { free(state.page_tables_base); state.page_tables_base = NULL; } reset_state(); } static int mmu_test(void) { mmu_printf(INFO, "Test \"%s\" running\n", state.test_name); int ret = posix_memalign(&state.page_tables_base, 1 << state.page_granule_shift, state.page_tables_size); assert(ret == 0); memset(state.page_tables_base, 0, state.page_tables_size); arm_mmu_init(false); // Calls back on platform_mmu_setup(resume) if (state.arm_flush_tlbs_called) { return true; } else { mmu_printf(FAILURE, "!!! arm_flush_tlbs() was not called\n"); return false; } } static void do_test(enum Reason expect_reason, int (^test_func)(void)) { assert(state.test_name != NULL); mmu_printf(INFO, "----------------------------------------" "--------------------------------------\n"); mmu_printf(INFO, "Test: %s\n", state.test_name); // Increment number of tests run. ++tests_run; // Set a jump environment for panic/abort exit path. int reason_code = setjmp(state.jmp_env); if (reason_code != 0) { // Returns non-zero if longjmp() invoked to return here. if (reason_code == (int) expect_reason) { // Failure for an expected reason: exercising error // detection. mmu_printf(INFO, "Test \"%s\" aborted for expected reason: %s\n", state.test_name, reason_to_string(expect_reason)); ++tests_passed; } else { mmu_printf(FAILURE, "!!! Test \"%s\" failed: %s\n", state.test_name, reason_to_string(reason_code)); mmu_printf(FAILURE, "!!! Expected outcome: %s\n", reason_to_string(expect_reason)); } test_delete(); reset_state(); return; } if (test_func()) { mmu_printf(INFO, "Test \"%s\" completed, passed\n", state.test_name); ++tests_passed; } else { mmu_printf(FAILURE, "!!! Test \"%s\" failed at completion\n", state.test_name); mmu_printf(FAILURE, "!!! Expected outcome: %s\n", reason_to_string(expect_reason)); } test_delete(); reset_state(); } static void setup_standard_granule(size_t granule) { switch (granule) { case 4096: state.page_granule_shift = 12; state.page_tables_size = 4096 * 4; break; case 16384: state.page_granule_shift = 14; state.page_tables_size = 16384 * 4; break; case 65536: state.page_granule_shift = 16; state.page_tables_size = 65536 * 4; break; default: mmu_printf(FAILURE, "Bad granule %u\n", (unsigned) granule); abort(); } } static void test_granule(size_t bytes) { state.mmu_setup_func = ^{ mmu_setup_flat(); }; setup_standard_granule(bytes); do_test(REASON_OK, ^{ return mmu_test(); }); } static void test_granule_4kb(void) { state.test_name = "4KB granule"; test_granule(4096); } static void test_granule_16kb(void) { state.test_name = "16KB granule"; test_granule(16384); } static void test_granule_64kb(void) { state.test_name = "64KB granule"; test_granule(65536); } static void test_fail_oom(void) { state.test_name = "Detect out-of-memory PTE allocation"; state.mmu_setup_func = ^{ mmu_setup_flat(); }; setup_standard_granule(4096); // Too small. state.page_tables_size = 16384; expect_panic("tt_alloc", "Out of table allocation space L2"); do_test(REASON_EXPECTED_PANIC, ^{ return mmu_test(); }); } static void test_fail_range_misaligned(void) { state.test_name = "Detect misaligned mapping request"; state.mmu_setup_func = ^{ mmu_setup_misaligned(); }; setup_standard_granule(4096); // Expect vaddr, paddr, size to misaligned. expect_panic("arm_mmu_map_range", "vaddr | paddr | size"); do_test(REASON_EXPECTED_PANIC, ^{ return mmu_test(); }); } static void test_fail_bad_granule(void) { state.test_name = "Detect bad granule size"; state.mmu_setup_func = ^{ // Alter the granule to an illegal value. ++state.page_granule_shift; mmu_setup_flat(); }; setup_standard_granule(4096); expect_panic("get_level_shift", "Granule 2^13"); do_test(REASON_EXPECTED_PANIC, ^{ return mmu_test(); }); } static void test_fail_remap_existing(void) { state.test_name = "Detect remapping existing page"; state.mmu_setup_func = ^{ mmu_setup_fail_remap_existing(); }; setup_standard_granule(4096); expect_panic("map_at_level", "Remapping an existing L2"); do_test(REASON_EXPECTED_PANIC, ^{ return mmu_test(); }); } static void test_fail_breaking_down(void) { state.test_name = "Detect breaking down existing mapping"; state.mmu_setup_func = ^{ mmu_setup_fail_breaking_down(); }; setup_standard_granule(4096); expect_panic("map_at_level", "Breaking down"); do_test(REASON_EXPECTED_PANIC, ^{ return mmu_test(); }); } static void test_get_tcr(void) { state.test_name = "get_tcr()"; do_test(REASON_OK, ^{ state.page_granule_shift = 12; mmu_get_tcr(); state.page_granule_shift = 14; mmu_get_tcr(); state.page_granule_shift = 16; mmu_get_tcr(); return true; }); } static void test_fail_get_tcr_bad_granule(void) { state.test_name = "Detect get_tcr() with bad granule"; expect_panic("get_tcr", "Bad granule"); state.page_granule_shift = 13; do_test(REASON_EXPECTED_PANIC, ^{ mmu_get_tcr(); return false; }); } static void test_get_block_flags_for_level(unsigned level) { // [63:59] SBZ // [58:55] S/W use SBZ // [54:53] XN,PXN = execute never, 0 for RX, 3 for R/RW // [52] Contiguous hint, SBZ // [51:12] SBZ and address bits, also SBZ // [11] nG - non-global, we don't use ASID - SBZ // [10] AF - access flag, should set - SBO // [9:8] SH - shareability - 0 for device, 2 for normal // [7:6] AP - access permission - 0 for RW, 2 for RO // [5] NS - non-secure, should set - SBO // [4:2] AttrIndx - 0 for device, 1 for normal // [1] Walk - L0,L1,L2 0=Block; L3 1=Block // [0] V - 0=fault, 1=valid const uint64_t sbo = (1 << 10) | (1 << 5) | (1 << 0); const uint64_t normal = (2 << 8) | (1 << 2); const uint64_t device = (0 << 8) | (0 << 2); const uint64_t read_write = 0 << 6; const uint64_t read_only = 2 << 6; const uint64_t execute = 0ULL << 53; const uint64_t no_execute = 3ULL << 53; static const struct { arm_mmu_extended_attr_t attr; uint64_t expect; } tests[] = { { kARMMMUDeviceR, sbo | device | read_only | no_execute }, { kARMMMUDeviceRX, sbo | device | read_only | execute }, { kARMMMUDeviceRW, sbo | device | read_write | no_execute }, { kARMMMUNormalR, sbo | normal | read_only | no_execute }, { kARMMMUNormalRX, sbo | normal | read_only | execute }, { kARMMMUNormalRW, sbo | normal | read_write | no_execute }, }; for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); ++i) { arm_mmu_extended_attr_t attr = tests[i].attr; uint64_t expect = tests[i].expect; if (level == 3) { expect |= 1 << 1; // "Walk" flag } uint64_t value = mmu_get_block_flags(level, attr); if (value != expect) { mmu_printf(FAILURE, "get_block_flags level %u test %u enum %u\n", level, (unsigned) i, (unsigned) attr); mmu_printf(FAILURE, "got 0x%" PRIx64 "\n", value); mmu_printf(FAILURE, "expect 0x%" PRIx64 "\n", expect); test_unwind(REASON_PTE_MISMATCH); } } } static void test_get_block_flags(void) { state.test_name = "get_block_flags()"; state.page_granule_shift = 12; do_test(REASON_OK, ^{ for (unsigned level = 1; level <= 3; ++level) { test_get_block_flags_for_level(level); } return true; }); } static void test_get_table_flags(void) { state.test_name = "get_tte_flags()"; state.page_granule_shift = 12; // [63] NS // [62:61] AP // [60] XN // [59] PXN // [58:52] ignored // [51:48] zero, sbz // [47:12] TableOutputAddress // [11:2] ignored // [1] type=1, sbo // [0] valid=1, sbo const uint64_t expect = (1ULL << 63) | (1ULL << 1) | (1ULL << 0); do_test(REASON_OK, ^{ uint64_t value = mmu_get_table_flags(); if (value != expect) { mmu_printf(FAILURE, "get_table_flags() mismatch\n"); mmu_printf(FAILURE, "Got 0x%" PRIx64 "\n", value); mmu_printf(FAILURE, "Expect 0x%" PRIx64 "\n", expect); test_unwind(REASON_PTE_MISMATCH); } return true; }); } static void test_fail_get_block_flags_bad_attr(void) { state.test_name = "Detect get_block_flags() bad attribute"; state.page_granule_shift = 12; expect_panic("get_block_flags", "Bad attr"); do_test(REASON_EXPECTED_PANIC, ^{ mmu_get_block_flags(3, -1); return false; }); } static void test_fail_get_block_flags_bad_level(void) { state.test_name = "Detect get_pte_flags() bad level"; state.page_granule_shift = 16; expect_panic("get_block_flags", "level_permits_blocks"); // Should not allow a block at L1 with 64KB granule. do_test(REASON_EXPECTED_PANIC, ^{ mmu_get_block_flags(1, kARMMMUNormalRW); return false; }); } static void test_tt_alloc(unsigned granule_shift) { state.test_name = "tt_alloc()"; state.page_granule_shift = granule_shift; state.page_tables_size = (1 << granule_shift) * 3; state.mmu_setup_func = ^{}; do_test(REASON_OK, ^{ arm_mmu_init(false); if (granule_shift == 12) mmu_tt_alloc(2); mmu_tt_alloc(3); return true; }); } static void test_tt_alloc_bad_level(void) { state.test_name = "Detect tt_alloc() bad level"; state.page_granule_shift = 14; state.page_tables_size = 16384 * 3; state.mmu_setup_func = ^{}; expect_panic("tt_alloc", "level > "); do_test(REASON_EXPECTED_PANIC, ^{ arm_mmu_init(false); mmu_tt_alloc(1); return false; }); } static void test_tt_alloc_bad_alignment(void) { state.test_name = "Detect tt_alloc() bad alignment"; state.page_granule_shift = 14; state.page_tables_size = 65536 * 3; // Enough space. state.mmu_setup_func = ^{}; expect_panic("tt_alloc", "Bad aligned pte alloc"); do_test(REASON_EXPECTED_PANIC, ^{ arm_mmu_init(false); mmu_tt_alloc(3); // Allocation is aligned, so only way to trip it up // is to artificially tamper with the granule. state.page_granule_shift = 16; mmu_tt_alloc(3); return false; }); } static void test_map_at_top_level_large(void) { state.test_name = "map_at_level() at top level traverses only L1"; setup_standard_granule(4096); __block uint64_t alloc_start, alloc_next, alloc_end; state.mmu_setup_func = ^{ mmu_get_tt_alloc_range(&alloc_start, &alloc_next, &alloc_end); mmu_map_at_level(1, (addr_t) mmu_get_tt_top_level(), 1ULL << 30, 1ULL << 30, 1ULL << 30, kARMMMUNormalRW); }; __block size_t calls = 0; state.set_table_entry_hook = ^(unsigned level, uint64_t base, size_t index, uint64_t value) { (void) value; ++calls; mmu_printf(INFO, "calls:%u %u 0x%" PRIx64 " 0x%" PRIx64 " 0x%" PRIx64 "\n", (unsigned) calls, level, base, (uint64_t) index, value); if (calls == 1) { if (level != 1 || base != (uint64_t) mmu_get_tt_top_level() || index != 1) { mmu_printf(FAILURE, "Did not write L0 index 0\n"); test_unwind(REASON_PTE_ACCESS); } } else { mmu_printf(FAILURE, "Too many calls to get_table_entry()\n"); test_unwind(REASON_PTE_ACCESS); } }; do_test(REASON_OK, ^{ if (!mmu_test()) { return false; } return true; }); } static void test_map_at_l3(void) { state.test_name = "map_at_level() at l3"; setup_standard_granule(16384); __block uint64_t alloc_start = 0, alloc_next = 0, alloc_end = 0; __block uint64_t l3_base = 0; state.mmu_setup_func = ^{ mmu_get_tt_alloc_range(&alloc_start, &alloc_next, &alloc_end); l3_base = mmu_tt_alloc(3); mmu_map_at_level(3, l3_base, (1ULL << 30) + 16384 * 3, 16384 * 7, 16384, kARMMMUDeviceRW); }; __block size_t calls = 0; state.set_table_entry_hook = ^(unsigned level, uint64_t base, size_t index, uint64_t value) { (void) value; ++calls; mmu_printf(INFO, "calls:%u %u 0x%" PRIx64 " 0x%" PRIx64 " 0x%" PRIx64 "\n", (unsigned) calls, level, base, (uint64_t) index, value); if (calls == 1) { if (level != 3 || base != l3_base || index != 3) { mmu_printf(FAILURE, "Did not write L3 index 3\n"); test_unwind(REASON_PTE_ACCESS); } } else { mmu_printf(FAILURE, "Too many calls to get_table_entry()\n"); test_unwind(REASON_PTE_ACCESS); } }; do_test(REASON_OK, ^{ if (!mmu_test()) { return false; } return true; }); } int main() { reset_state(); test_granule_4kb(); test_granule_16kb(); test_granule_64kb(); test_fail_oom(); test_fail_range_misaligned(); test_fail_bad_granule(); test_fail_remap_existing(); test_fail_breaking_down(); test_get_tcr(); // test_fail_get_tcr_bad_granule(); !!!FIXME!!! Unexpected panic in function get_virt_address_bits: Bad granule test_get_block_flags(); test_get_table_flags(); test_fail_get_block_flags_bad_attr(); test_fail_get_block_flags_bad_level(); test_tt_alloc(12); test_tt_alloc(14); test_tt_alloc(16); test_tt_alloc_bad_level(); test_tt_alloc_bad_alignment(); test_map_at_top_level_large(); test_map_at_l3(); mmu_printf(INFO, "%" PRIu64 "/%" PRIu64 " tests passed\n", (uint64_t) tests_passed, (uint64_t) tests_run); assert(tests_passed <= tests_run); if (tests_passed == tests_run) { mmu_printf(INFO, "Pass\n"); return 0; } else { mmu_printf(INFO, "!!! Fail\n"); return 1; } }