diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 294448e..e36a446 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -619,6 +619,19 @@ jobs: bash tools/run-probe.sh examples/threads-detached threads-detached grep -q 'detached: 8 started, 8 ended' examples/threads-detached/run.log + # ⭐ A LARGE ALLOCATION IS A MAPPING, AND A MAPPING IS WHOLE PAGES. + # + # musl's allocator uses a mapping up to the end of its last page; the port + # obtained only the length asked for, and on Windows the rest of that page + # was the next heap block. port/src/okm_syscall.c, `do_mmap`. + - name: A large allocation is a mapping and a mapping is whole pages + env: + MCPP_TARGET: ${{ matrix.target }} + run: | + bash tools/run-probe.sh examples/malloc-large malloc-large + grep -q 'growth: 24 blocks up to 8390650 bytes' examples/malloc-large/run.log + grep -q -- '-- failures: 0 --' examples/malloc-large/run.log + - name: A program may use the names the internal overlay defines env: MCPP_TARGET: ${{ matrix.target }} diff --git a/README.md b/README.md index 55395b1..38e8959 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ consumer needs to answer it without asking. | this package | is carried by | | --- | --- | +| 0.13.5 | `openkal-llvm-runtime = "0.9.6"` | | 0.13.4 | `openkal-llvm-runtime = "0.9.5"` | | 0.13.3 | `openkal-llvm-runtime = "0.9.4"` | | 0.13.2 | `openkal-llvm-runtime = "0.9.3"` | diff --git a/examples/malloc-large/mcpp.toml b/examples/malloc-large/mcpp.toml new file mode 100644 index 0000000..5815055 --- /dev/null +++ b/examples/malloc-large/mcpp.toml @@ -0,0 +1,13 @@ +[package] +name = "malloc-large" +version = "0.1.0" + +[dependencies] +openkal-musl = { path = "../.." } + +[targets.malloc-large] +kind = "bin" +main = "src/main.c" + +[build] +cxx_runtime = "host-coupled" diff --git a/examples/malloc-large/src/main.c b/examples/malloc-large/src/main.c new file mode 100644 index 0000000..bb3d9b8 --- /dev/null +++ b/examples/malloc-large/src/main.c @@ -0,0 +1,126 @@ +/* A large allocation is a mapping, and a mapping is whole pages. + * + * musl's allocator obtains an allocation of MMAP_THRESHOLD (131,052) bytes or + * more as a mapping of its own, and uses the mapping up to the end of its last + * page: the block may start up to a page into it, and the slot's footer sits + * just below that end. The port obtained exactly the length asked for, so on + * Windows, where that length comes from the process heap, the footer and up to + * a page of the block lay past the end of what was obtained --- over the next + * heap block's header. The program went on until the heap next walked there. + * + * ⭐ WHAT IS OBSERVED. + * (1) An anonymous mapping of a length that ends inside a page: the rest of + * that page reads as zero and can be written, and memory allocated beside + * it keeps its contents. + * (2) Blocks from half the threshold to eight megabytes, each grown from the + * one before the way a string grows, filled end to end, checked, and + * released while the next is held --- lengths on a page and inside one. + * (3) realloc across the threshold in both directions. + */ +#include +#include +#include +#include + +enum { PAGE = 4096 }; + +static int failures; + +static void fail(const char* what, size_t n) +{ + printf("FAIL: %s (%zu)\n", what, n); + ++failures; +} + +static int filled_with(const unsigned char* p, size_t n, unsigned char value) +{ + for (size_t i = 0; i < n; ++i) + if (p[i] != value) return 0; + return 1; +} + +static void mapping_tail(void) +{ + const size_t len = PAGE + 904; /* ends inside the second page */ + unsigned char* neighbours[16]; + for (int i = 0; i < 16; ++i) { + neighbours[i] = malloc(64); + if (neighbours[i]) memset(neighbours[i], 0x5a, 64); + } + unsigned char* m = mmap(0, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + if (m == MAP_FAILED) { fail("mmap", len); return; } + for (int i = 0; i < 16; ++i) { + unsigned char* after = malloc(64); + if (after) { memset(after, 0x5a, 64); free(after); } + } + if (!filled_with(m, 2 * PAGE, 0)) fail("the last page of a mapping reads as zero", 2 * PAGE); + memset(m, 0xa5, 2 * PAGE); + for (int i = 0; i < 16; ++i) + if (neighbours[i] && !filled_with(neighbours[i], 64, 0x5a)) fail("a block allocated before the mapping kept its contents", 64); + if (munmap(m, len) != 0) fail("munmap", len); + for (int i = 0; i < 16; ++i) free(neighbours[i]); + printf("mapping: %zu bytes asked, %d readable and writable\n", len, 2 * PAGE); +} + +static void growth(void) +{ + unsigned char* held = 0; + size_t held_size = 0; + int blocks = 0; + for (size_t size = 65536; size <= (size_t)8 << 20; size *= 2) { + for (size_t inside = 0; inside < 3; ++inside) { + const size_t n = size + inside * 1021; + unsigned char* p = malloc(n); + if (!p) { fail("malloc", n); continue; } + const unsigned char value = (unsigned char)(n % 251); + if (held) { + if (!filled_with(held, held_size, (unsigned char)(held_size % 251))) fail("the held block kept its contents", held_size); + memcpy(p, held, held_size); + } + memset(p + held_size, value, n - held_size); + memset(p, value, held_size); + /* Small allocations walk the heap beside the large ones. */ + for (int i = 0; i < 32; ++i) { + unsigned char* small = malloc(24 + i); + if (small) { memset(small, 0x33, 24 + i); free(small); } + } + if (!filled_with(p, n, value)) fail("a block kept its contents", n); + free(held); + held = p; + held_size = n; + ++blocks; + } + } + free(held); + printf("growth: %d blocks up to %zu bytes\n", blocks, held_size); +} + +static void reallocation(void) +{ + size_t n = 1000; + unsigned char* p = malloc(n); + if (!p) { fail("malloc", n); return; } + memset(p, 0x11, n); + const size_t sizes[] = { 140000, 600000, 131052, 131051, 2000, 300000, 4096 * 64 + 1, 100 }; + for (size_t i = 0; i < sizeof sizes / sizeof sizes[0]; ++i) { + const size_t m = sizes[i]; + unsigned char* q = realloc(p, m); + if (!q) { fail("realloc", m); free(p); return; } + const size_t kept = n < m ? n : m; + if (!filled_with(q, kept, 0x11)) fail("realloc kept the contents", m); + memset(q, 0x11, m); + p = q; + n = m; + } + free(p); + printf("realloc: %zu sizes across the threshold\n", sizeof sizes / sizeof sizes[0]); +} + +int main(void) +{ + mapping_tail(); + growth(); + reallocation(); + printf("-- failures: %d --\n", failures); + return failures == 0 ? 0 : 1; +} diff --git a/mcpp.toml b/mcpp.toml index 84bc29b..9f98be4 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-musl" -version = "0.13.4" +version = "0.13.5" description = "musl 1.2.5 redirected onto openkal: one C library, ported once, above every implementation of the specification rather than above one kernel." license = "Apache-2.0" diff --git a/port/src/okm_syscall.c b/port/src/okm_syscall.c index eadb5d8..d6e6890 100644 --- a/port/src/okm_syscall.c +++ b/port/src/okm_syscall.c @@ -511,12 +511,37 @@ static syscall_arg_t do_getdents(int fd, void* buf, size_t cap) /* --- memory ---------------------------------------------------------------- */ +/* ⚠️ A MAPPING IS WHOLE PAGES, AND THE CALLER USES ALL OF THEM. + * + * `mmap' maps every page the length touches, so the bytes from the length to + * the end of its last page are the caller's too, and they read as zero. musl's + * allocator counts on it: an allocation of MMAP_THRESHOLD bytes or more is a + * mapping of `n + IB + UNIT' bytes whose slot is `pages * 4096 - UNIT' long, and + * it places the block up to a page into that slot and writes the slot's footer + * just below the end of the last page. This layer used to obtain exactly the + * length asked for. Where kal_alloc hands out that length and no more --- the + * process heap on Windows --- the footer and up to a page of the block were + * written past the end of what was obtained, over the header of the heap's + * next block, and the program stopped at a later free or allocation with an + * access violation, or ended without a word. + * + * ⭐ Measured under Wine: a std::string grown by push_back past 196,607 bytes + * ended the program, and it passes with the length rounded up here and at + * SYS_munmap, whose length is the caller's as well. On windows-2022 the same + * fault stopped lsp-mcpp's conformance runner while it read a build tree. */ +static size_t okm_whole_pages(size_t len) +{ + return (len + OKM_PAGE - 1) & ~(size_t)(OKM_PAGE - 1); +} + static syscall_arg_t do_mmap(void* addr, size_t len, int prot, int flags, int fd, off_t off) { (void)prot; if (addr != 0 || fd >= 0 || off != 0) return -ENOSYS; if (!(flags & MAP_ANON) || !(flags & MAP_PRIVATE)) return -ENOSYS; if (len == 0) return -EINVAL; + if (len > (size_t)-1 - (OKM_PAGE - 1)) return -ENOMEM; + len = okm_whole_pages(len); void* p = kal_alloc(len, OKM_PAGE); if (!p) return -ENOMEM; /* An anonymous mapping reads as zero, and a caller relies on it: musl's @@ -1695,7 +1720,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, /* --- memory ----------------------------------------------------------- */ case SYS_mmap: return do_mmap((void*)a1, (size_t)a2, (int)a3, (int)a4, (int)a5, (off_t)a6); case SYS_munmap: - kal_free((void*)a1, (size_t)a2, OKM_PAGE); + kal_free((void*)a1, okm_whole_pages((size_t)a2), OKM_PAGE); return 0; case SYS_mprotect: /* openkal has no operation upon the protection of a mapping. Reporting