From 16ae0a993c9986a68a1ecaff5ca18491cfe9f221 Mon Sep 17 00:00:00 2001 From: professor-moody Date: Sun, 5 Jul 2026 10:24:18 -0500 Subject: [PATCH 1/2] tmfile: bounds-check offsets in the tensor loader The tm2 loader dereferenced file offsets and counts without checking them against the mapped size, so a crafted model caused out-of-bounds reads on load (#1449). Add tm2_off_ok() and route the load_graph_tensors reads through it: the tensor/buffer offset tables, the per-tensor offset, the name string (offset and length), the dims vector, and the buffer_id index. --- source/serializer/tmfile/tm2_serializer.c | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/source/serializer/tmfile/tm2_serializer.c b/source/serializer/tmfile/tm2_serializer.c index 3fc87d660..6e459d171 100644 --- a/source/serializer/tmfile/tm2_serializer.c +++ b/source/serializer/tmfile/tm2_serializer.c @@ -154,13 +154,29 @@ static int unregister_tm2_op_loader(struct tm2_serializer* s, int op_type, int o return -1; } +/* return 1 if [off, off+sz) is fully inside the mapped model file, else 0. + All offsets/counts in a tmfile are attacker-controlled; validate before dereferencing. */ +static inline int tm2_off_ok(const struct tm2_priv* priv, size_t off, size_t sz) +{ + if (priv->mem_len < 0) + return 0; + size_t len = (size_t)priv->mem_len; + return off <= len && sz <= len - off; +} + static int load_graph_tensors(struct tm2_serializer* tm2_s, struct graph* graph, struct tm2_priv* priv) { char* mem_base = (char*)priv->base; const TM2_Subgraph* tm_graph = priv->subgraph; + if (!tm2_off_ok(priv, tm_graph->offset_vo_tensors, sizeof(TM2_Vector_offsets)) || + !tm2_off_ok(priv, tm_graph->offset_vo_buffers, sizeof(TM2_Vector_offsets))) + return -1; const TM2_Vector_offsets* v_tensors = (TM2_Vector_offsets*)(mem_base + tm_graph->offset_vo_tensors); const TM2_Vector_offsets* v_buffers = (TM2_Vector_offsets*)(mem_base + tm_graph->offset_vo_buffers); + if (!tm2_off_ok(priv, tm_graph->offset_vo_tensors, sizeof(TM2_Vector_offsets) + (size_t)v_tensors->v_num * sizeof(v_tensors->offsets[0])) || + !tm2_off_ok(priv, tm_graph->offset_vo_buffers, sizeof(TM2_Vector_offsets) + (size_t)v_buffers->v_num * sizeof(v_buffers->offsets[0]))) + return -1; graph->graph_layout = tm_graph->graph_layout; graph->model_layout = tm_graph->model_layout; @@ -174,6 +190,8 @@ static int load_graph_tensors(struct tm2_serializer* tm2_s, struct graph* graph, for (int i = 0; i < v_tensors->v_num; i++) { + if (!tm2_off_ok(priv, v_tensors->offsets[i], sizeof(TM2_Tensor))) + return -1; const TM2_Tensor* tm_tensor = (TM2_Tensor*)(mem_base + v_tensors->offsets[i]); int flag_permute = 0; // flag the tensor has to be permute int dims_org[8] = {0}; @@ -192,14 +210,22 @@ static int load_graph_tensors(struct tm2_serializer* tm2_s, struct graph* graph, if (tm_tensor->offset_s_tname != TM2_NOT_SET) { // TODO: using update the TM2 model + if (!tm2_off_ok(priv, tm_tensor->offset_s_tname, sizeof(TM2_String))) + return -1; const TM2_String* tm_str = (TM2_String*)(mem_base + tm_tensor->offset_s_tname); + if (!tm2_off_ok(priv, tm_str->offset_data, tm_str->size)) + return -1; ir_tensor->name = strdup_name(mem_base + tm_str->offset_data, tm_str->size); } /* shape */ if (tm_tensor->offset_vd_dims != TM2_NOT_SET) { + if (!tm2_off_ok(priv, tm_tensor->offset_vd_dims, sizeof(TM2_Vector_dims))) + return -1; const TM2_Vector_dims* v_dims = (TM2_Vector_dims*)(mem_base + tm_tensor->offset_vd_dims); + if (!tm2_off_ok(priv, tm_tensor->offset_vd_dims, sizeof(TM2_Vector_dims) + (size_t)v_dims->v_num * sizeof(v_dims->dims[0]))) + return -1; if (tm_graph->model_layout == TENGINE_LAYOUT_NCHW) { @@ -235,6 +261,9 @@ static int load_graph_tensors(struct tm2_serializer* tm2_s, struct graph* graph, /* load const type of tensor, such as the weight or bias for convolution node */ if (ir_tensor->tensor_type == TENSOR_TYPE_CONST) { + if (tm_tensor->buffer_id >= v_buffers->v_num || + !tm2_off_ok(priv, v_buffers->offsets[tm_tensor->buffer_id], sizeof(TM2_Buffer))) + return -1; const TM2_Buffer* tm_buf = (TM2_Buffer*)(mem_base + v_buffers->offsets[tm_tensor->buffer_id]); /* fill temp data buffer to benchmark */ From 6930b66893e70b7341a4158ccf11b0e507ad5dea Mon Sep 17 00:00:00 2001 From: Nathan Keys Date: Fri, 31 Jul 2026 19:04:41 -0500 Subject: [PATCH 2/2] tmfile: bounds-check the quant-param vector in the tensor loader The previous commit did not route offect_vo_quantparams through tm2_off_ok, so the quant-param block at the tail of load_graph_tensors still dereferenced a file-controlled offset off mem_base. A crafted tmfile therefore still gets a heap out-of-bounds read inside the function this branch set out to bound: AddressSanitizer: heap-buffer-overflow, READ of size 4 #0 load_graph_tensors tm2_serializer.c:470 #1 load_graph tm2_serializer.c:875 #2 load_model tm2_serializer.c:941 #3 create_graph c_api.c:429 Reproduced on x86-64 with clang and ASan against the shipped googlenet_benchmark.tmfile with two bytes changed, so that tensor[0].offect_vo_quantparams points past the end of the file. Three dereferences in that block were unguarded, not one: the TM2_Vector_offsets header, offsets[0] on the single-param path, and offsets[j] in the multi-param loop. The offsets[] array extent that v_num describes was unbounded too; that bound is written as a division rather than a multiply so it cannot overflow, matching the subtraction form in tm2_off_ok. I missed this the first time because the check I ran, that the shipped benchmark models still load, cannot exercise this code at all: all 14 of them have offect_vo_quantparams == TM2_NOT_SET on every tensor, so none of them enters the block. Verified after the change: the crafted file is rejected instead of faulting, all 14 shipped models still load, and a model carrying legitimate in-bounds quant params (v_num=1, real scale and zero_point) still loads and runs, so the new bounds reject only out-of-range offsets. --- source/serializer/tmfile/tm2_serializer.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/source/serializer/tmfile/tm2_serializer.c b/source/serializer/tmfile/tm2_serializer.c index 6e459d171..3d295c994 100644 --- a/source/serializer/tmfile/tm2_serializer.c +++ b/source/serializer/tmfile/tm2_serializer.c @@ -164,6 +164,13 @@ static inline int tm2_off_ok(const struct tm2_priv* priv, size_t off, size_t sz) return off <= len && sz <= len - off; } +/* Largest offsets[] element count that still fits inside the mapping after the + TM2_Vector_offsets header at . Expressed as a division so the bound can + never overflow, mirroring tm2_off_ok above. */ +#define TM2_MAX_QUANTPARAMS(priv, off) \ + ((((size_t)(priv)->mem_len) - (size_t)(off) - sizeof(TM2_Vector_offsets)) \ + / sizeof(tm_uoffset_t)) + static int load_graph_tensors(struct tm2_serializer* tm2_s, struct graph* graph, struct tm2_priv* priv) { char* mem_base = (char*)priv->base; @@ -464,12 +471,23 @@ static int load_graph_tensors(struct tm2_serializer* tm2_s, struct graph* graph, /* load vector type of tensor */ if (tm_tensor->offect_vo_quantparams != TM2_NOT_SET) { + /* the vector header itself must lie inside the mapping before it is read */ + if (!tm2_off_ok(priv, tm_tensor->offect_vo_quantparams, sizeof(TM2_Vector_offsets))) + return -1; + const TM2_Vector_offsets* v_quantparams = (TM2_Vector_offsets*)(mem_base + tm_tensor->offect_vo_quantparams); + /* v_num is file controlled, so the offsets[] array it describes must fit too. + written as a division rather than a multiply so it cannot overflow. */ + if (v_quantparams->v_num > (TM2_MAX_QUANTPARAMS(priv, tm_tensor->offect_vo_quantparams))) + return -1; + /* currently only support one quant param */ ir_tensor->quant_param_num = v_quantparams->v_num; if (v_quantparams->v_num == 1) { + if (!tm2_off_ok(priv, v_quantparams->offsets[0], sizeof(TM2_QuantParam))) + return -1; const TM2_QuantParam* tm_qtparam = (TM2_QuantParam*)(mem_base + v_quantparams->offsets[0]); ir_tensor->scale = tm_qtparam->scale; ir_tensor->zero_point = tm_qtparam->zero_point; @@ -484,6 +502,8 @@ static int load_graph_tensors(struct tm2_serializer* tm2_s, struct graph* graph, for (int j = 0; j < v_quantparams->v_num; j++) { + if (!tm2_off_ok(priv, v_quantparams->offsets[j], sizeof(TM2_QuantParam))) + return -1; const TM2_QuantParam* tm_qtparam = (TM2_QuantParam*)(mem_base + v_quantparams->offsets[j]); ir_tensor->scale_list[j] = tm_qtparam->scale; ir_tensor->zp_list[j] = tm_qtparam->zero_point;