/* * nanowasm, a tiny WebAssembly/Wasm interpreter * Copyright (C) 2023-2024 Xavier Del Campo Romero * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const enum opcode initexpr_opcodes[] = { OP_NOP, OP_END, OP_I32_CONST, OP_I64_CONST, OP_F32_CONST, OP_F64_CONST }; const struct interp_set interp_initexpr_set = { .opcodes = initexpr_opcodes, .n = sizeof initexpr_opcodes / sizeof *initexpr_opcodes }; static int (*const ops[])(struct nw_interp *) = { [OP_UNREACHABLE] = op_unreachable, [OP_NOP] = op_nop, [OP_BLOCK] = op_block, [OP_LOOP] = op_loop, [OP_IF] = op_if, [OP_ELSE] = op_else, [OP_END] = op_end, [OP_BR] = op_br, [OP_BR_IF] = op_br_if, [OP_BR_TABLE] = op_br_table, [OP_RETURN] = op_return, [OP_CALL] = op_call, [OP_CALL_INDIRECT] = op_call_indirect, [OP_GET_LOCAL] = op_get_local, [OP_SET_LOCAL] = op_set_local, [OP_TEE_LOCAL] = op_tee_local, [OP_GET_GLOBAL] = op_get_global, [OP_SET_GLOBAL] = op_set_global, [OP_I32_LOAD] = op_i32_load, [OP_I32_STORE] = op_i32_store, [OP_I32_CONST] = op_i32_const, [OP_I64_CONST] = op_i64_const, [OP_F32_CONST] = op_f32_const, [OP_F64_CONST] = op_f64_const, [OP_I32_SUB] = op_i32_sub }; int interp_run(struct nw_interp *const i) { uint8_t op; if (!fread(&op, sizeof op, 1, i->f)) { LOG("%s: fread(3) failed, feof=%d, ferror=%d\n", __func__, feof(i->f), ferror(i->f)); i->exception = "I/O error"; return -1; } else if (op >= sizeof ops / sizeof *ops) { LOG("%s: invalid opcode %#" PRIx8 "\n", __func__, op); i->exception = "invalid opcode"; return -1; } else if (!ops[op]) { LOG("%s: unsupported opcode %#" PRIx8 "\n", __func__, op); i->exception = "invalid opcode"; return -1; } return ops[op](i); } static int run_opcode_limited(struct nw_interp *const in, const struct interp_set *const set) { uint8_t op; if (!fread(&op, sizeof op, 1, in->f)) { LOG("%s: fread(3) failed, feof=%d, ferror=%d\n", __func__, feof(in->f), ferror(in->f)); in->exception = "I/O error"; return -1; } for (size_t i = 0; i < set->n; i++) if (op == set->opcodes[i]) return ops[op](in); LOG("%s: unexpected opcode %#" PRIx8 "\n", __func__, op); in->exception = "invalid opcode"; return -1; } int interp_run_limited(struct nw_interp *const i, const struct interp_set *const set) { while (!i->exit) if (run_opcode_limited(i, set)) { LOG("%s: run_opcode_limited failed\n", __func__); return -1; } return 0; } int interp_check_opcode(const uint8_t op, FILE *const f) { static int (*const checks[])(FILE *) = { [OP_UNREACHABLE] = check_unreachable, [OP_NOP] = check_nop, [OP_BLOCK] = check_block, [OP_LOOP] = check_loop, [OP_IF] = check_if, [OP_ELSE] = check_else, [OP_END] = check_end, [OP_BR] = check_br, [OP_BR_IF] = check_br_if, [OP_BR_TABLE] = check_br_table, [OP_RETURN] = check_return, [OP_CALL] = check_call, [OP_CALL_INDIRECT] = check_call_indirect, [OP_GET_LOCAL] = check_get_local, [OP_SET_LOCAL] = check_set_local, [OP_TEE_LOCAL] = check_tee_local, [OP_GET_GLOBAL] = check_get_global, [OP_SET_GLOBAL] = check_set_global, [OP_I32_LOAD] = check_i32_load, [OP_I32_STORE] = check_i32_store, [OP_I32_CONST] = check_i32_const, [OP_I64_CONST] = check_i64_const, [OP_F32_CONST] = check_f32_const, [OP_F64_CONST] = check_f64_const, [OP_I32_SUB] = check_i32_sub }; if (op >= sizeof checks / sizeof *checks) { LOG("%s: invalid opcode %#" PRIx8 "\n", __func__, op); return 1; } else if (!checks[op]) { LOG("%s: unsupported opcode %#" PRIx8 "\n", __func__, op); return 1; } return checks[op](f); } int interp_start(const struct nw_interp_cfg *const cfg, FILE *const f, struct nw_interp *const i) { *i = (const struct nw_interp) { .f = f, .cfg = *cfg }; return 0; } void *interp_stackptr(const struct nw_interp *const i) { return (char *)i->cfg.stack.buf + i->stack_i; } static int push_frame(struct nw_interp *const i, const struct nw_frame *const src) { struct nw_frame *const dst = interp_stackptr(i); if (interp_stack_push(i, src, sizeof (*src))) { LOG("%s: interp_stack_push failed\n", __func__); return -1; } else if (i->fp) i->fp->next = dst; i->fp = dst; return 0; } static bool mul_overflow(const size_t a, const size_t b) { return a && (a * b) / a != b; } static void init_i32(const varuint32 count, void *const p) { for (int32_t *i = p; i - (const int32_t *)p < count; i++) *i = 0; } static void init_i64(const varuint32 count, void *const p) { for (int64_t *i = p; i - (const int64_t *)p < count; i++) *i = 0; } static void init_f32(const varuint32 count, void *const p) { for (float *i = p; i - (const float *)p < count; i++) *i = 0; } static void init_f64(const varuint32 count, void *const p) { for (double *i = p; i - (const double *)p < count; i++) *i = 0; } static void init_locals(const enum value_type type, const varuint32 count, void *const p) { static void (*const init[])(varuint32, void *) = { [VALUE_TYPE_I32] = init_i32, [VALUE_TYPE_I64] = init_i64, [VALUE_TYPE_F32] = init_f32, [VALUE_TYPE_F64] = init_f64 }; init[type](count, p); } static int push_locals(struct nw_interp *const i, const struct nw_frame *const f) { const enum value_type t = f->local_type; const size_t typesz = get_type_size(t); const unsigned long n = f->n_locals; if (mul_overflow(typesz, f->n_locals)) { LOG("%s: local variables size mul overflow", __func__); i->exception = "mul overfllow"; return -1; } const size_t totalsz = typesz * n, max = i->cfg.stack.n; if (totalsz > max || i->stack_i > max - totalsz) { LOG("%s: cannot allocate locals\n", __func__); i->exception = "stack overflow"; return 1; } init_locals(t, n, interp_stackptr(i)); i->stack_i += totalsz; return 0; } static int do_push_labels(struct nw_interp *const i, const struct nw_frame *const fr) { size_t n = 0; for (;;) { interp_check_opcode(); } } static int push_labels(struct nw_interp *const i, const struct nw_frame *const fr) { FILE *const f = i->f; const long orig = ftell(f); if (orig < 0) { LOG("%s: ftell(3): %s\n", __func__, strerror(errno)); return -1; } else if (do_push_labels(i, fr)) { LOG("%s: do_push_labels failed\n", __func__); return -1; } else if (fseek(f, orig, SEEK_SET)) { LOG("%s: fseek(3): %s\n", __func__, strerror(errno)); return -1; } return -1; } int interp_push(struct nw_interp *const i, const struct nw_frame *const f) { if (push_frame(i, f)) { LOG("%s: push_frame failed\n", __func__); return -1; } else if (push_locals(i, f)) { LOG("%s: push_locals failed\n", __func__); return -1; } else if (push_labels(i, f)) { LOG("%s: push_labels failed\n", __func__); return -1; } return 0; } struct retval_priv { enum value_type type; union { int32_t i32; int64_t i64; float f32; double f64; } u; }; static void get_i32(const void *const buf, struct retval_priv *const rp) { rp->u.i32 = *(((const int32_t *)buf) - 1); } static void get_i64(const void *const buf, struct retval_priv *const rp) { rp->u.i64 = *(((const int64_t *)buf) - 1); } static void get_f32(const void *const buf, struct retval_priv *const rp) { rp->u.f32 = *(((const float *)buf) - 1); } static void get_f64(const void *const buf, struct retval_priv *const rp) { rp->u.f64 = *(((const double *)buf) - 1); } static int get_retval(struct nw_interp *const i, struct retval_priv *const rp) { const struct retval *const r = &i->fp->retval; const size_t sz = get_type_size(r->type), max = i->cfg.stack.n; if (max < sz || i->stack_i >= max - sz) { static const char exc[] = "stack underflow"; LOG("%s: %s\n", __func__, exc); i->exception = exc; return -1; } const void *const buf = (const char *)interp_stackptr(i) - sz; static void (*const get[])(const void *, struct retval_priv *) = { [VALUE_TYPE_I32] = get_i32, [VALUE_TYPE_I64] = get_i64, [VALUE_TYPE_F32] = get_f32, [VALUE_TYPE_F64] = get_f64 }; get[r->type](buf, rp); rp->type = r->type; i->stack_i -= sz; return 0; } int interp_pop(struct nw_interp *const i) { if (!i->fp) { static const char exc[] = "no frame pointer"; LOG("%s: %s\n", __func__, exc); i->exception = exc; return -1; } const struct retval *const r = &i->fp->retval; struct retval_priv rp; if (r->returns && get_retval(i, &rp)) { LOG("%s: get_retval failed\n", __func__); return -1; } const char *const stackptr = interp_stackptr(i); const ptrdiff_t diff = stackptr - (const char *)i->fp; i->stack_i -= diff; if (!(i->fp = i->fp->prev)) { /* Entry point is always assumed to return int, which is * defined as an i32 variable according to Wasm. */ if (rp.type != VALUE_TYPE_I32) { LOG("%s: expected i32 return type, got %s\n", __func__, value_type_tostr(rp.type)); i->exception = "unexpected return type"; return -1; } i->retval = rp.u.i32; i->exit = true; } return 0; } int interp_stack_push(struct nw_interp *const i, const void *const src, const size_t n) { void *const dst = interp_stackptr(i); if (i->cfg.stack.n < n || i->stack_i > i->cfg.stack.n - n) { LOG("%s: stack overflow\n", __func__); i->exception = "stack overflow"; return -1; } memcpy(dst, src, n); i->stack_i += n; return 0; } int interp_stack_pop(struct nw_interp *const i, void *const dst, const size_t n) { if (i->stack_i < n) { LOG("%s: stack underflow\n", __func__); i->exception = "stack underflow"; return -1; } i->stack_i -= n; memcpy(dst, interp_stackptr(i), n); return 0; } int interp_heap_store(struct nw_interp *const i, const size_t addr, const void *const src, const size_t n) { if (i->cfg.heap.n < n || addr > i->cfg.heap.n - n) { static const char exc[] = "heap overflow"; LOG("%s: %s\n", __func__, exc); i->exception = exc; return -1; } void *const dst = (char *)i->cfg.heap.buf + addr; memcpy(dst, src, n); return 0; } int interp_heap_load(struct nw_interp *const i, const size_t addr, void *const dst, const size_t n) { const size_t max = i->cfg.heap.n; if (max < n || addr > max - n) { static const char exc[] = "heap out-of-bounds access"; LOG("%s: %s (%lu, max %zu)\n", __func__, exc, (unsigned long)addr, max); i->exception = exc; return -1; } const void *const src = (const char *)i->cfg.heap.buf + addr; memcpy(dst, src, n); return 0; }