Snap for 15028875 from d41dba19d65193dcc81ad382eaa07a05717cdc9e to 26Q2-release

Change-Id: I1b51a94daf251cc8e15d58f670bf5cf8c24e18ea
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 2c7d3cd..232ef07 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -31,3 +31,7 @@
       run: |
         meson setup build-san -Db_sanitize=address,undefined -Dbuildtype=debugoptimized && ninja -C build-san test
         meson setup build-tsan -Db_sanitize=thread -Dbuildtype=debugoptimized && ninja -C build-tsan test
+
+    - name: Build SYS_MINIMAL
+      run: |
+        meson setup build-min -Dsys_minimal=true && ninja -C build-min
diff --git a/Android.bp b/Android.bp
index 061e586..35477b3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -64,6 +64,9 @@
         "-fno-strict-overflow",
         "-fvisibility=hidden",
     ],
+    shared_libs: [
+        "liblog",
+    ],
 }
 
 cc_library {
diff --git a/METADATA b/METADATA
index 2d62a56..0ab5541 100644
--- a/METADATA
+++ b/METADATA
@@ -8,14 +8,14 @@
   license_type: NOTICE
   last_upgrade_date {
     year: 2026
-    month: 2
-    day: 17
+    month: 3
+    day: 12
   }
   homepage: "https://github.com/lfi-project/lfi-runtime"
   identifier {
     type: "Git"
     value: "https://github.com/lfi-project/lfi-runtime"
-    version: "d0878d066b610ecf29b59b83939ae9dd9d3d9d19"
+    version: "dbe02592c0e860ffce90bbe76a7c1a2f37ef6750"
     primary_source: true
   }
 }
diff --git a/core/arch/arm64/callback.S b/core/arch/arm64/callback.S
index 59b7dc8..fa322e2 100644
--- a/core/arch/arm64/callback.S
+++ b/core/arch/arm64/callback.S
@@ -62,9 +62,11 @@
     mov REG_ADDR, REG_BASE
     get_ctx x9
 
+#ifndef CTXREG
     // Save and replace tpidr_el0 with host value.
     ldr x12, [x9, REGS_HOST_TP]
     write_ctx x12, x11
+#endif
 
 .if \with_struct
     str x0, [x9, #REGS_X(0)]
diff --git a/core/arch/arm64/callback.c b/core/arch/arm64/callback.c
index f36aebe..5fcdc09 100644
--- a/core/arch/arm64/callback.c
+++ b/core/arch/arm64/callback.c
@@ -185,3 +185,11 @@
     atomic_store_explicit(&box->cbinfo.dataentries_alias[slot].trampoline, 0,
         memory_order_seq_cst);
 }
+
+void
+lfi_box_cb_free(struct LFIBox *box)
+{
+    if (box->cbinfo.dataentries_alias)
+        munmap(box->cbinfo.dataentries_alias,
+            MAXCALLBACKS * sizeof(struct CallbackDataEntry));
+}
diff --git a/core/arch/arm64/runtime.S b/core/arch/arm64/runtime.S
index bf8819d..7d5f5d0 100644
--- a/core/arch/arm64/runtime.S
+++ b/core/arch/arm64/runtime.S
@@ -143,6 +143,7 @@
     stp x12, x13, [REG_BASE, REGS_X(12)]
     stp x14, x15, [REG_BASE, REGS_X(14)]
     stp x16, x17, [REG_BASE, REGS_X(16)]
+    str x18, [REG_BASE, REGS_X(18)]
     str REG_ADDR, [REG_BASE, REGS_ADDR]
     mov x1, sp
     stp x30, x1,  [REG_BASE, REGS_X(30)]
@@ -191,6 +192,7 @@
     ldp x12, x13, [x0, REGS_X(12)]
     ldp x14, x15, [x0, REGS_X(14)]
     ldp x16, x17, [x0, REGS_X(16)]
+    ldr x18, [x0, REGS_X(18)]
     ldr REG_ADDR, [x0, REGS_ADDR]
     ldr REG_BASE, [x0, REGS_BASE]
     ldp x30, x1,  [x0, REGS_X(30)]
diff --git a/core/arch/arm64/trampoline.S b/core/arch/arm64/trampoline.S
index cbb15be..be29b2d 100644
--- a/core/arch/arm64/trampoline.S
+++ b/core/arch/arm64/trampoline.S
@@ -208,12 +208,14 @@
     // load target into x10
     mov x10, x22
 
+#ifndef CTXREG
     // set the sandbox thread control block by saving the old value of
     // [tpidr_el0, TLS_SLOT_LFI] into the context and setting it to point to
     // the context itself.
     get_ctx x12
     str x12, [x13, REGS_HOST_TP]
     write_ctx x13, x12
+#endif
 
     // save user sp to stack
     ldr x12, [x13, REGS_SP]
@@ -232,6 +234,10 @@
     // set address pointer to a valid value
     mov REG_ADDR, REG_BASE
 
+#ifdef CTXREG
+    ldr x25, [x13, REGS_X(25)]
+#endif
+
 #ifndef STORES_ONLY
     // Zero non-argument registers for full LFI.
     // TODO: calling convention or LFI_INVOKE should enforce that all unused
@@ -240,8 +246,8 @@
 #endif
 
     // enter sandbox
-    add x10, REG_BASE, w10, uxtw
-    br x10
+    add x16, REG_BASE, w10, uxtw
+    br x16
     // the previous call should return through lfi_ret
     udf #0
 
@@ -257,9 +263,11 @@
 lfi_ret:
     bti jc
     get_ctx REG_BASE
+#ifndef CTXREG
     // restore host thread control block
     ldr x19, [REG_BASE, REGS_HOST_TP]
     write_ctx x19, x20
+#endif
 .lfi_ret_internal:
     // restore host stack
     ldr x19, [REG_BASE, REGS_HOST_SP]
diff --git a/core/arch/riscv64/callback.c b/core/arch/riscv64/callback.c
index 5ea9c61..bbf1293 100644
--- a/core/arch/riscv64/callback.c
+++ b/core/arch/riscv64/callback.c
@@ -29,3 +29,9 @@
 {
     assert(!"unimplemented");
 }
+
+void
+lfi_box_cb_free(struct LFIBox *box)
+{
+    (void) box;
+}
diff --git a/core/arch/x64/callback.S b/core/arch/x64/callback.S
index 28c5dea..2fd3ef1 100644
--- a/core/arch/x64/callback.S
+++ b/core/arch/x64/callback.S
@@ -73,6 +73,7 @@
 
     // Save %r11 across the call (and stack alignment).
     pushq %r11
+    cld
     callq *%r10
     popq %r11
 
diff --git a/core/arch/x64/callback.c b/core/arch/x64/callback.c
index 8bce9f4..dee06d8 100644
--- a/core/arch/x64/callback.c
+++ b/core/arch/x64/callback.c
@@ -151,3 +151,11 @@
     atomic_store_explicit(&box->cbinfo.cbentries_alias[slot].trampoline, 0,
         memory_order_seq_cst);
 }
+
+void
+lfi_box_cb_free(struct LFIBox *box)
+{
+    if (box->cbinfo.cbentries_alias)
+        munmap(box->cbinfo.cbentries_alias,
+            MAXCALLBACKS * sizeof(struct CallbackEntry));
+}
diff --git a/core/arch/x64/runtime.S b/core/arch/x64/runtime.S
index f0eb2a9..11237a5 100644
--- a/core/arch/x64/runtime.S
+++ b/core/arch/x64/runtime.S
@@ -92,6 +92,7 @@
 #endif
     // put val in the return register
     movq %rsi, %rax
+    cld
     // restore callee-saved registers saved in lfi_ctx_entry
     popq %rbp
     popq %rbx
@@ -151,6 +152,7 @@
 
     // restore host stack and call handler
     movq REGS_HOST_SP(%rcx), %rsp
+    cld
     pushq %rcx
     movq %rcx, %rdi
     callq lfi_syscall_handler
@@ -236,7 +238,7 @@
 .p2align 4
 lfi_set_tp:
 #ifdef HAVE_PKU
-    pushq %rax
+    pushq %rdx
     pushq %rcx
     pushq %rax
     pku_all_access
diff --git a/core/arch/x64/trampoline.S b/core/arch/x64/trampoline.S
index 043fcee..5dd5ebd 100644
--- a/core/arch/x64/trampoline.S
+++ b/core/arch/x64/trampoline.S
@@ -285,6 +285,7 @@
     movq %rbp, REGS_RSP(%rdi)
     popq %rbp // pop dummy value
     // return value should already be in %rax
+    cld
     popq %rbp
     popq %rbx
     popq %r12
diff --git a/core/box.c b/core/box.c
index 1446e47..860f95d 100644
--- a/core/box.c
+++ b/core/box.c
@@ -95,6 +95,10 @@
         null_rtcall->rtcalls[2] = (uintptr_t) &lfi_set_tp;
 #endif
         null_rtcall->rtcalls[3] = (uintptr_t) &lfi_ret;
+
+        int r2 = protectmem((void *) box->base, pagesize, PROT_READ,
+            box->pkey);
+        assert(r2 == 0);
     }
 
     // Map read-only.
@@ -113,8 +117,7 @@
     }
 
     size_t size = engine->opts.boxsize;
-    uintptr_t base = boxmap_addspace(engine->bm,
-        box_footprint(size, engine->opts));
+    uintptr_t base = boxmap_addspace(engine->bm, box_footprint(size));
     if (base == 0) {
         lfi_error = LFI_ERR_BOXMAP;
         goto err1;
@@ -129,7 +132,7 @@
         else
             LOG(engine, "could not allocate pkey: invalid argument");
         lfi_error = LFI_ERR_PKU;
-        return NULL;
+        goto err1;
     }
     assert(pkey != 0);
     int r = pkey_mprotect((void *) base, size, PROT_NONE, pkey);
@@ -141,9 +144,9 @@
         .base = base,
         .size = size,
         .engine = engine,
-        .min = base + engine->guardsize + engine->opts.pagesize,
-        .max = base + size - engine->guardsize,
-        .max_exec = base + size - engine->guardsize,
+        .min = base + engine->opts.pagesize,
+        .max = base + size - BOX_INTERNAL_GUARD,
+        .max_exec = base + size - BOX_INTERNAL_GUARD,
     };
     syssetup(box);
 
@@ -157,7 +160,7 @@
     return box;
 
 err2:
-    boxmap_rmspace(engine->bm, base, box_footprint(size, engine->opts));
+    boxmap_rmspace(engine->bm, base, box_footprint(size));
 err1:
     free(box);
     return NULL;
@@ -331,6 +334,15 @@
     return protectmem((void *) start, size, host_prot(prot), box->pkey);
 }
 
+static void
+cbunmap(uint64_t start, size_t len, struct MMInfo info, void *udata)
+{
+    (void) udata, (void) info;
+    void *p = mmap((void *) start, len, PROT_NONE,
+        MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED, -1, 0);
+    assert(p == (void *) start);
+}
+
 static lfiptr
 box_mapany(struct LFIBox *box, size_t size, int prot, int flags, int fd,
     off_t off, bool no_verify)
@@ -340,7 +352,7 @@
         return (lfiptr) -1;
     int r = mapverify(box, addr, size, prot, flags, fd, off, no_verify);
     if (r < 0) {
-        mm_unmap(&box->mm, addr, size);
+        mm_unmap_cb(&box->mm, addr, size, cbunmap, NULL);
         return (lfiptr) -1;
     }
     return p2l(box, addr);
@@ -360,20 +372,12 @@
     return box_mapany(box, size, prot, flags, fd, off, false);
 }
 
-static void
-cbunmap(uint64_t start, size_t len, struct MMInfo info, void *udata)
-{
-    (void) udata, (void) info;
-    void *p = mmap((void *) start, len, PROT_NONE,
-        MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED, -1, 0);
-    assert(p == (void *) start);
-}
-
 EXPORT lfiptr
 lfi_box_mapat(struct LFIBox *box, lfiptr addr, size_t size, int prot, int flags,
     int fd, off_t off)
 {
-    assert(l2p(box, addr) >= box->min && l2p(box, addr) + size <= box->max);
+    if (!lfi_box_bufvalid(box, addr, size))
+        return (lfiptr) -1;
 
     uintptr_t m_addr = mm_mapat_cb(&box->mm, l2p(box, addr), size, prot, flags,
         fd, off, cbunmap, NULL);
@@ -381,7 +385,7 @@
         return (lfiptr) -1;
     int r = mapverify(box, m_addr, size, prot, flags, fd, off, false);
     if (r < 0) {
-        mm_unmap(&box->mm, m_addr, size);
+        mm_unmap_cb(&box->mm, m_addr, size, cbunmap, NULL);
         return (lfiptr) -1;
     }
     return p2l(box, m_addr);
@@ -391,14 +395,15 @@
 lfi_box_mapat_register(struct LFIBox *box, lfiptr addr, size_t size, int prot,
     int flags, int fd, off_t off)
 {
-    assert(l2p(box, addr) >= box->min && l2p(box, addr) + size <= box->max);
+    if (!lfi_box_bufvalid(box, addr, size))
+        return (lfiptr) -1;
 
     uintptr_t m_addr = mm_mapat_cb(&box->mm, l2p(box, addr), size, prot, flags,
         fd, off, cbunmap, NULL);
     if (m_addr == (uintptr_t) -1)
         return (lfiptr) -1;
-    int r = verify(box, m_addr, size, prot);
-    if (r < 0) {
+    bool r = verify(box, m_addr, size, prot);
+    if (!r) {
         mm_unmap(&box->mm, m_addr, size);
         return (lfiptr) -1;
     }
@@ -408,22 +413,26 @@
 EXPORT int
 lfi_box_mprotect(struct LFIBox *box, lfiptr addr, size_t size, int prot)
 {
-    assert(l2p(box, addr) >= box->min && l2p(box, addr) + size <= box->max);
+    if (!lfi_box_bufvalid(box, addr, size))
+        return -1;
 
-    int r = mm_protect(&box->mm, l2p(box, addr), size, prot);
+    int r = protectverify(box, l2p(box, addr), size, prot, false);
     if (r < 0)
         return r;
-    return protectverify(box, l2p(box, addr), size, prot, false);
+    return mm_protect(&box->mm, l2p(box, addr), size, prot);
 }
 
 EXPORT int
 lfi_box_mprotect_noverify(struct LFIBox *box, lfiptr addr, size_t size,
     int prot)
 {
-    int r = mm_protect(&box->mm, l2p(box, addr), size, prot);
+    if (!lfi_box_bufvalid(box, addr, size))
+        return -1;
+
+    int r = protectverify(box, l2p(box, addr), size, prot, true);
     if (r < 0)
         return r;
-    return protectverify(box, l2p(box, addr), size, prot, true);
+    return mm_protect(&box->mm, l2p(box, addr), size, prot);
 }
 
 EXPORT bool
@@ -444,7 +453,7 @@
 EXPORT int
 lfi_box_munmap(struct LFIBox *box, lfiptr addr, size_t size)
 {
-    if (l2p(box, addr) >= box->min && l2p(box, addr) + size < box->max)
+    if (lfi_box_bufvalid(box, addr, size))
         return mm_unmap_cb(&box->mm, l2p(box, addr), size, cbunmap, NULL);
     return -1;
 }
@@ -452,11 +461,12 @@
 EXPORT void
 lfi_box_free(struct LFIBox *box)
 {
-    void *p = mmap((void *) box->base, box->size, PROT_NONE,
-        MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED, -1, 0);
-    assert(p == (void *) box->base);
-    boxmap_rmspace(box->engine->bm, box->base,
-        box_footprint(box->size, box->engine->opts));
+    size_t pagesize = box->engine->opts.pagesize;
+    void *p = mmap((void *) (box->base - pagesize), box->size + pagesize,
+        PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED, -1, 0);
+    assert(p == (void *) (box->base - pagesize));
+    lfi_box_cb_free(box);
+    boxmap_rmspace(box->engine->bm, box->base, box_footprint(box->size));
 #ifdef HAVE_PKU
     if (box->pkey != 0)
         pkey_free(box->pkey);
@@ -476,7 +486,7 @@
 lfi_box_bufvalid(struct LFIBox *box, lfiptr addr, size_t size)
 {
     lfiptr lp = l2p(box, addr);
-    return lp >= box->min && lp + size <= box->max;
+    return lp >= box->min && lp <= box->max && size <= box->max - lp;
 }
 
 EXPORT void *
diff --git a/core/boxmap.c b/core/boxmap.c
index dbfb716..20289c4 100644
--- a/core/boxmap.c
+++ b/core/boxmap.c
@@ -60,7 +60,7 @@
 {
     size_t total = 0;
     for (size_t i = 0; i < map->nregions; i++) {
-        total += map->regions[i].size - 2 * map->opts.guardsize;
+        total += map->regions[i].size;
     }
     return total;
 }
@@ -100,13 +100,14 @@
         return false;
     }
 
-    // Since mmap gives us something page-aligned, we need to find a region
-    // within it that is properly chunk-aligned.
-    uintptr_t alignbase = ceilp((uintptr_t) base, map->opts.chunksize);
-    size_t alignsize = truncp(alignbase +
-                               (size - (alignbase - (uintptr_t) base)),
-                           map->opts.chunksize) -
-        alignbase;
+    // Find a chunk-aligned sub-region that has at least guardsize bytes of
+    // space on each side. The guard regions are the space between the mmap
+    // edges and the chunk-aligned allocatable region.
+    uintptr_t start = (uintptr_t) base + map->opts.guardsize;
+    uintptr_t end = (uintptr_t) base + size - map->opts.guardsize;
+
+    uintptr_t alignbase = ceilp(start, map->opts.chunksize);
+    size_t alignsize = truncp(end, map->opts.chunksize) - alignbase;
 
     struct ExtAlloc *alloc = extalloc_new(alignbase, alignsize,
         map->opts.chunksize);
@@ -120,11 +121,6 @@
         return false;
     }
 
-    // Reserve the guard regions on either end of the new region.
-    extalloc_allocat(alloc, alignbase, map->opts.guardsize);
-    extalloc_allocat(alloc, alignbase + alignsize - map->opts.guardsize,
-        map->opts.guardsize);
-
     map->regions[map->nregions++] = (struct AddrRegion) {
         .base = (void *) alignbase,
         .size = alignsize,
diff --git a/core/core.c b/core/core.c
index 097fbd6..eacd621 100644
--- a/core/core.c
+++ b/core/core.c
@@ -94,9 +94,7 @@
 
     struct BoxMapOptions bm_opts = (struct BoxMapOptions) {
         .chunksize = gb(4),
-        // This is the guard size between the edge of the boxmap region and the
-        // outer world.
-        .guardsize = box_footprint(gb(4), opts),
+        .guardsize = REGION_GUARD,
     };
     struct BoxMap *bm = boxmap_new(bm_opts);
     if (!bm) {
@@ -104,11 +102,11 @@
         goto err1;
     }
 
-    // Reserve space for n sandboxes with appropriate footprint, 2 guard pages,
-    // and 2 chunks worth of slack because boxmap has to do internal alignment
-    // when mmap returns non-chunk-aligned region.
-    size_t reserve = nsandboxes * box_footprint(opts.boxsize, opts) +
-        bm_opts.chunksize * 2 + bm_opts.guardsize * 2;
+    // Reserve space for n sandboxes with appropriate footprint, region guards
+    // on each side, and 1 chunk worth of slack because boxmap has to do
+    // internal alignment when mmap returns non-chunk-aligned region.
+    size_t reserve = nsandboxes * box_footprint(opts.boxsize) +
+        bm_opts.chunksize + bm_opts.guardsize * 2;
 
     if (nsandboxes > 0) {
         if (!boxmap_reserve(bm, reserve)) {
@@ -126,7 +124,6 @@
     *engine = (struct LFIEngine) {
         .bm = bm,
         .opts = opts,
-        .guardsize = kb(192),
     };
 
     if (!engine->opts.no_init_sigaltstack)
diff --git a/core/core.h b/core/core.h
index 5056e9a..e6a07b6 100644
--- a/core/core.h
+++ b/core/core.h
@@ -21,7 +21,6 @@
     void (*sys_handler)(struct LFIContext *ctx);
     struct LFIContext *(*clone_cb)(struct LFIBox *box);
 
-    size_t guardsize;
     stack_t altstack;
 };
 
@@ -89,6 +88,8 @@
 extern thread_local int lfi_error;
 extern thread_local char *lfi_error_desc;
 
+void lfi_box_cb_free(struct LFIBox *box);
+
 static inline size_t
 kb(size_t x)
 {
@@ -107,31 +108,52 @@
     return x * 1024 * 1024 * 1024;
 }
 
-// Return the total amount of virtual address space needed for a sandbox of a
-// certain size. This depends on the architecture because it is effectively
-// reserving a guard region beyond the sandbox.
-static inline size_t
-box_footprint(size_t boxsize, struct LFIOptions opts)
-{
-    if (boxsize != gb(4)) {
-        LOG_(
-            "warning: box_footprint does not properly support non-4GiB sandboxes");
-        return boxsize;
-    }
+// Guard sizes for sandbox isolation. These are configurable per-architecture.
+//
+// BOX_INTERNAL_GUARD: space at the end of each sandbox (within the box's
+// allocated size) that is not usable by sandbox code. This reduces the
+// sandbox's usable address range.
+//
+// BOX_EXTERNAL_GUARD: additional space allocated beyond each sandbox's size,
+// increasing the per-sandbox footprint. This provides isolation between
+// adjacent sandboxes when hardware masking alone is insufficient.
+//
+// REGION_GUARD: space reserved on each side of the entire sandbox pool region,
+// protecting against accesses escaping the pool.
+
 #if defined(LFI_ARCH_ARM64)
-    return gb(8);
+
+#define BOX_INTERNAL_GUARD kb(192)
+#define BOX_EXTERNAL_GUARD 0
+#define REGION_GUARD       mb(512)
+
 #elif defined(LFI_ARCH_X64)
 
+#define BOX_INTERNAL_GUARD 0
 #ifdef HAVE_PKU
-    return gb(4);
+#define BOX_EXTERNAL_GUARD 0
+#define REGION_GUARD       0
 #else
-    // TODO: verify that we can safely reduce this to gb(4) if Segue is enabled.
-    return gb(44);
+// TODO: verify that we can safely reduce BOX_EXTERNAL_GUARD if Segue is
+// enabled.
+#define BOX_EXTERNAL_GUARD gb(40)
+#define REGION_GUARD       gb(40)
 #endif
 
 #elif defined(LFI_ARCH_RISCV64)
-    return gb(4);
+
+#define BOX_INTERNAL_GUARD 0
+#define BOX_EXTERNAL_GUARD 0
+#define REGION_GUARD       0
+
 #else
 #error "invalid architecture"
 #endif
+
+// Return the total amount of virtual address space needed for a sandbox of a
+// certain size. This is the sandbox size plus any external guard.
+static inline size_t
+box_footprint(size_t boxsize)
+{
+    return boxsize + BOX_EXTERNAL_GUARD;
 }
diff --git a/core/log.c b/core/log.c
index d3bec14..93f0f40 100644
--- a/core/log.c
+++ b/core/log.c
@@ -41,19 +41,3 @@
 
     return strp;
 }
-
-#ifdef __ANDROID__
-
-// This is a weak definition of __android_log_print in case it doesn't exist
-// that just prints to stderr. This is used when building for Android but when
-// liblog is not linked.
-__attribute__((weak, visibility("default"))) int
-__android_log_print(int prio, const char *tag, const char *fmt, ...)
-{
-    (void) prio;
-    (void) tag;
-    (void) fmt;
-    return 0;
-}
-
-#endif
diff --git a/core/log.h b/core/log.h
index 221809d..5b7b795 100644
--- a/core/log.h
+++ b/core/log.h
@@ -19,15 +19,22 @@
 
 #endif
 
-#if 0 && defined(__ANDROID__)
+#if defined(__ANDROID__)
 
 #include <android/log.h>
+#include <stdio.h>
 
-#define LOG_(...) \
-    ((void) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__))
+#define LOG_(fmt, ...) \
+    do { \
+        __android_log_print(ANDROID_LOG_INFO, LOG_TAG, fmt, ##__VA_ARGS__); \
+        fprintf(stderr, "[" LOG_TAG "] " fmt "\n", ##__VA_ARGS__); \
+    } while (0)
 
-#define ERROR(...) \
-    ((void) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
+#define ERROR(fmt, ...) \
+    do { \
+        __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, fmt, ##__VA_ARGS__); \
+        fprintf(stderr, "[" LOG_TAG "] " fmt "\n", ##__VA_ARGS__); \
+    } while (0)
 
 #else
 
diff --git a/core/mmap.c b/core/mmap.c
index 705c2b6..d9518b1 100644
--- a/core/mmap.c
+++ b/core/mmap.c
@@ -204,7 +204,7 @@
     length = mmceil(mm, length);
 
     if (!mmvalid(mm, addr, length))
-        return -LINUX_EINVAL;
+        return (uintptr_t) -1;
 
     struct MMNode *new = malloc(sizeof(struct MMNode));
     if (!new)
@@ -367,10 +367,11 @@
 {
     struct MMNode *node = mm->nodes;
     while (node) {
+        struct MMNode *next = node->next;
         if (!node->info.original)
             mm_unmap_cb(mm, node->base << mm->p2pagesize,
                 node->len << mm->p2pagesize, ufn, udata);
-        node = node->next;
+        node = next;
     }
 }
 
diff --git a/linux/fd.c b/linux/fd.c
index cb137a4..df59e68 100644
--- a/linux/fd.c
+++ b/linux/fd.c
@@ -79,10 +79,17 @@
         t->fds[newfd] = knewfd;
     } else {
         knewfd = t->fds[newfd];
-        if (knewfd == -1)
-            knewfd = newfd;
-        if (dup2(koldfd, knewfd) < 0)
-            goto err;
+        if (knewfd == -1) {
+            // Slot is unoccupied: allocate a new kernel fd rather than
+            // using the raw sandbox fd number, which could collide with
+            // monitor-internal file descriptors.
+            knewfd = dup(koldfd);
+            if (knewfd == -1)
+                goto err;
+        } else {
+            if (dup2(koldfd, knewfd) < 0)
+                goto err;
+        }
         t->fds[newfd] = knewfd;
         if (t->dirs[newfd]) {
             free(t->dirs[newfd]);
@@ -133,9 +140,11 @@
         t->fds[i] = -1;
     }
 
+#ifndef SYS_MINIMAL
     t->fds[0] = dup(STDIN_FILENO);
     t->fds[1] = dup(STDOUT_FILENO);
     t->fds[2] = dup(STDERR_FILENO);
+#endif
 
     t->passthrough = engine->opts.sys_passthrough;
 }
diff --git a/linux/include/lfi_linux.h b/linux/include/lfi_linux.h
index 3105f85..4dc30e6 100644
--- a/linux/include/lfi_linux.h
+++ b/linux/include/lfi_linux.h
@@ -69,6 +69,10 @@
 struct LFILinuxEngine *
 lfi_linux_lib_engine(void);
 
+// Frees the library engine and its underlying LFIEngine.
+void
+lfi_linux_lib_free(void);
+
 // Frees the resources allocated for the linux engine (but not the underlying
 // LFIEngine).
 void
diff --git a/linux/linux.c b/linux/linux.c
index 68bd60b..ff7aaee 100644
--- a/linux/linux.c
+++ b/linux/linux.c
@@ -42,7 +42,7 @@
 static struct LFILinuxEngine *lib_engine;
 
 // Total number of procs (libraries) that the lib_engine is initialized for.
-#define MAX_LIBRARIES 16
+#define MAX_LIBRARIES 1
 
 EXPORT bool
 lfi_linux_lib_init(struct LFIOptions opts, struct LFILinuxOptions linux_opts)
@@ -70,6 +70,16 @@
 }
 
 EXPORT void
+lfi_linux_lib_free(void)
+{
+    if (lib_engine) {
+        lfi_free(lib_engine->engine);
+        free(lib_engine);
+        lib_engine = NULL;
+    }
+}
+
+EXPORT void
 lfi_linux_free(struct LFILinuxEngine *engine)
 {
     free(engine);
diff --git a/linux/log.h b/linux/log.h
index 1bd8201..acba48e 100644
--- a/linux/log.h
+++ b/linux/log.h
@@ -16,7 +16,7 @@
 
 #endif
 
-#if 0 && defined(__ANDROID__)
+#if defined(__ANDROID__)
 
 #include <android/log.h>
 
diff --git a/linux/proc.c b/linux/proc.c
index e874db0..916088e 100644
--- a/linux/proc.c
+++ b/linux/proc.c
@@ -97,8 +97,8 @@
             // should do the same thing with the interpreter, even if we could
             // load it directly from the file.
             if (prog_fd == -1) {
-                interp.fd = -1;
                 close(interp.fd);
+                interp.fd = -1;
             }
             LOG(proc->engine, "using sandbox dynamic linker: %s", interp_path);
         } else {
diff --git a/linux/sys/sys.h b/linux/sys/sys.h
index f044478..59be41f 100644
--- a/linux/sys/sys.h
+++ b/linux/sys/sys.h
@@ -88,11 +88,16 @@
 {
     if (!ptrcheck(t, pathp))
         return NULL;
+    // Clamp the copy length to the sandbox boundary so we never read past
+    // the end of the sandbox's valid address range.
+    size_t maxlen = t->proc->box_info.max - pathp;
+    if (maxlen > FILENAME_MAX - 1)
+        maxlen = FILENAME_MAX - 1;
     char *host = malloc(FILENAME_MAX);
     if (!host)
         return NULL;
-    strncpy(host, (char *) lfi_box_p2l(t->proc->box, pathp), FILENAME_MAX - 1);
-    host[FILENAME_MAX - 1] = 0;
+    strncpy(host, (char *) lfi_box_l2p(t->proc->box, pathp), maxlen);
+    host[maxlen] = 0;
     return host;
 }
 
diff --git a/linux/sys/sys_fchdir.c b/linux/sys/sys_fchdir.c
index 38b156b..6da14e4 100644
--- a/linux/sys/sys_fchdir.c
+++ b/linux/sys/sys_fchdir.c
@@ -3,6 +3,8 @@
 int
 sys_fchdir(struct LFILinuxThread *t, int fd)
 {
+    if (fd < 0 || fd >= LINUX_NOFILE)
+        return -LINUX_EBADF;
     LOCK_WITH_DEFER(&t->proc->fdtable.lk, lk_fdtable);
     if (t->proc->fdtable.fds[fd] == -1)
         return -LINUX_EBADF;
diff --git a/linux/sys/sys_fchmod.c b/linux/sys/sys_fchmod.c
index 45cdf18..aec5fbf 100644
--- a/linux/sys/sys_fchmod.c
+++ b/linux/sys/sys_fchmod.c
@@ -8,5 +8,5 @@
     int kfd = fdget(&t->proc->fdtable, fd);
     if (kfd == -1)
         return -LINUX_EBADF;
-    return HOST_ERR(int, fchmod(fd, mode));
+    return HOST_ERR(int, fchmod(kfd, mode));
 }
diff --git a/linux/sys_minimal.c b/linux/sys_minimal.c
index 12a0421..418c6ee 100644
--- a/linux/sys_minimal.c
+++ b/linux/sys_minimal.c
@@ -20,10 +20,6 @@
 #endif
     SYS(brk,
             sys_brk(t, a0))
-    SYS(mmap,
-            sys_mmap(t, a0, a1, a2, a3, a4, a5))
-    SYS(munmap,
-            sys_munmap(t, a0, a1))
     SYS(exit_group,
             sys_exit_group(t, a0))
     SYS(exit,
diff --git a/linux/trampoline.c b/linux/trampoline.c
index d1ed95e..d9e3095 100644
--- a/linux/trampoline.c
+++ b/linux/trampoline.c
@@ -47,7 +47,11 @@
     t->stack_size = stacksize;
 
     // Set stack pointer to top of stack (stack grows down).
+# if defined(__aarch64__) || defined(_riscv)
     lfi_ctx_regs(t->ctx)->sp = t->stack + stacksize;
+# elif defined(__x86_64__)
+    lfi_ctx_regs(t->ctx)->rsp = t->stack + stacksize;
+# endif
 
     // Set tp to 0 (no TLS support in sys_minimal mode).
     lfi_ctx_set_tp(t->ctx, 0);
diff --git a/subprojects/packagefiles/lfi-verifier/meson.build b/subprojects/packagefiles/lfi-verifier/meson.build
index 32b9c6d..99bf8df 100644
--- a/subprojects/packagefiles/lfi-verifier/meson.build
+++ b/subprojects/packagefiles/lfi-verifier/meson.build
@@ -31,16 +31,14 @@
 c_defines = []
 
 if 'arm64' in arch
-  subdir('src/decode/disarm')
-  dependencies += disarm64.as_link_whole()
+  dependencies += subproject('disarm').get_variable('disarm64').as_link_whole()
   srcs += files(
     'src/arm64/verify.c',
   )
   c_defines += '-DARCH_ARM64'
 endif
 if 'x64' in arch
-  subdir('src/decode/fadec')
-  dependencies += fadec.as_link_whole()
+  dependencies += subproject('fadec').get_variable('fadec').as_link_whole()
   srcs += files(
     'src/x64/verify.c',
     'src/x64/bdd.c',