From: Dmitry Volyntsev Date: Fri, 12 Jun 2026 01:31:27 +0000 (-0700) Subject: Buffer: fix type confusion in concat() with element getters X-Git-Tag: 1.0.0~5 X-Git-Url: http://git.kaiwu.me/postgresql/log/contrib/postgres_fdw/stylesheets/print.css?a=commitdiff_plain;h=943a9f358feda2e9b28445a781464a291d0b763e;p=njs.git Buffer: fix type confusion in concat() with element getters njs_buffer_concat() validated each list element as a typed array in the length pass, then re-read the same elements with njs_value_property_i64() in the copy pass and cast them with njs_typed_array() without rechecking. For a non-fast array with an accessor element, a getter returning a typed array during validation and a non-typed-array (or a detached buffer) during the copy yielded a wild pointer and an out-of-bounds read. Revalidate the type and detached state in the copy pass, matching the length pass and the QuickJS implementation. --- diff --git a/src/njs_buffer.c b/src/njs_buffer.c index e88d108c..988919fb 100644 --- a/src/njs_buffer.c +++ b/src/njs_buffer.c @@ -918,7 +918,20 @@ njs_buffer_concat(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return ret; } + /* The getter above may have changed the element type. */ + + if (njs_slow_path(!njs_is_typed_array(&val))) { + njs_type_error(vm, "\"list[%L]\" argument must be an " + "instance of Buffer or Uint8Array", i); + return NJS_ERROR; + } + arr = njs_typed_array(&val); + if (njs_slow_path(njs_is_detached_buffer(arr->buffer))) { + njs_type_error(vm, "detached buffer"); + return NJS_ERROR; + } + n = njs_min((size_t) len, arr->byte_length); src = &njs_typed_array_buffer(arr)->u.u8[arr->offset]; diff --git a/test/buffer.t.js b/test/buffer.t.js index be74083a..898e259a 100644 --- a/test/buffer.t.js +++ b/test/buffer.t.js @@ -98,6 +98,59 @@ let concat_tsuite = { }; +let concatRevalidate_tsuite = { + name: "Buffer.concat() element revalidation tests", + skip: () => (!has_buffer()), + T: async (params) => { + let stage = 0; + let ta = Buffer.from('abcdef'); + + /* + * A real but non-fast array (accessor on index 0) whose getter + * returns a valid typed array during the length pass and something + * else during the copy pass (TOCTOU). + */ + let list = []; + Object.defineProperty(list, 0, { + enumerable: true, + configurable: true, + get() { + if (stage++ === 0) { + return ta; + } + + if (params.detach) { + detach(ta.buffer); + return ta; + } + + return params.evil; + }, + }); + + try { + Buffer.concat(list); + + } catch (e) { + if (e instanceof TypeError) { + return 'SUCCESS'; + } + + throw e; + } + + throw Error('concat() did not revalidate the swapped element'); + }, + + tests: [ + { evil: {} }, + { evil: "not a typed array" }, + { evil: [0, 1, 2] }, + { skip: () => !is_detach_available(), detach: true }, + ], +}; + + let compare_tsuite = { name: "Buffer.compare() tests", skip: () => (!has_buffer()), @@ -1069,6 +1122,7 @@ run([ alloc_tsuite, byteLength_tsuite, concat_tsuite, + concatRevalidate_tsuite, compare_tsuite, comparePrototype_tsuite, copy_tsuite,