Check for a class id before reading a stream's vtable word

The stream probes for untyped values (pipe destinations, pipeline
stages, readline input and output, and the checks against the stream
classes) read the vtable word of any boxed object. Instances with a
class id in the rc word have no vtable word, so a hierarchy instance
whose first field is a number crashed those probes. scr_stream_vt_of
answers NULL for them, and each probe treats that as no stream.
This commit is contained in:
Chris Tate
2026-10-20 18:09:22 -05:00
parent 85936f9035
commit fa098689f1
7 changed files with 77 additions and 18 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/class-id-instances-as-streams.ts"
],
"diags": []
}
@@ -140,9 +140,11 @@ static void *bcs_capsule_retain(void *p) { return scr_stream_retain(p); }
/* Also the capsule's tag. */
static void bcs_capsule_release(void *p) { scr_stream_release(p); }
static bool bcs_stream_vt(const ScrVt *vt) {
return (vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
(vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post);
/* Whether `stream`, boxed as an "object:" typed ref, is a runtime stream. */
static bool bcs_stream_vt(const ScrStream *stream) {
const ScrVt *vt = scr_stream_vt_of(stream);
return vt != NULL && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
(vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post));
}
static void bcs_rec_release(void *p);
@@ -154,7 +156,7 @@ static ScrStream *bcs_of(const ScrDyn *value) {
if (value != NULL && value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
memcmp(value->v.typed_ref.type_key, "object:", 7) == 0) {
ScrStream *stream = value->v.typed_ref.ptr;
return bcs_stream_vt(stream->vt) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL;
return bcs_stream_vt(stream) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL;
}
if (value == NULL || value->kind != SCR_DYN_OBJ || value->symbol_properties == NULL) return NULL;
ScrDyn *capsule = scr_dyn_obj_get(value->symbol_properties, bcs_hidden_key, BCS_HIDDEN_LEN);
@@ -184,7 +186,7 @@ static ScrStream *bcs_native_of(const ScrDyn *value) {
memcmp(value->v.typed_ref.type_key, "object:", 7) != 0)
return NULL;
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
if (bcs_stream_vt(stream->vt)) return stream;
if (bcs_stream_vt(stream)) return stream;
value->v.typed_ref.release(stream);
return NULL;
}
+12 -12
View File
@@ -4740,12 +4740,14 @@ ScrStream *scr_stream_rl_from_dyn(const ScrDyn *value, bool output) {
if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
const ScrVt *vt = stream->vt;
bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt ||
scr_vt_within(vt, &scr_duplex_vt);
bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt)
: vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt));
if (ok) return stream;
const ScrVt *vt = scr_stream_vt_of(stream);
if (vt) {
bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt ||
scr_vt_within(vt, &scr_duplex_vt);
bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt)
: vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt));
if (ok) return stream;
}
value->v.typed_ref.release(stream);
}
static const char in_m[] = "createInterface with an input that is not a readable stream has no lowering";
@@ -6411,9 +6413,7 @@ static ScrStream *scr_stream_view_receiver(const ScrDyn *value) {
if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
/* A class instance with a class id in its rc word (scr_rc.h) has no
* vtable word: its class is no stream. */
const ScrVt *vt = scr_rc_class_id(stream) == 0 ? stream->vt : NULL;
const ScrVt *vt = scr_stream_vt_of(stream);
if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
(vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream;
value->v.typed_ref.release(stream);
@@ -6718,9 +6718,9 @@ ScrStream *scr_stream_unbox_dyn(const ScrDyn *value, const ScrStr *message) {
if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
const ScrVt *vt = stream->vt;
if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
(vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream;
const ScrVt *vt = scr_stream_vt_of(stream);
if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
(vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream;
value->v.typed_ref.release(stream);
}
scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020");
+7
View File
@@ -48,6 +48,13 @@ typedef struct ScrStream {
ScrStreamState *st;
} ScrStream;
/* The vtable of an object boxed as an "object:" typed ref, which may be any
* program class instance: NULL for one with a class id in its rc word
* (scr_rc.h), which has no vtable word and is no stream. */
static inline const ScrVt *scr_stream_vt_of(const ScrStream *s) {
return scr_rc_class_id(s) == 0 ? s->vt : NULL;
}
extern SCR_TL ScrVt scr_readable_vt, scr_writable_vt, scr_duplex_vt,
scr_transform_vt, scr_passthrough_vt; /* main() stamps pre/post */
+5 -1
View File
@@ -23,7 +23,11 @@ ScrStream *scr_stream_dyn_native(const ScrDyn *value, double side) {
memcmp(value->v.typed_ref.type_key, "object:", 7) != 0)
return NULL;
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
const ScrVt *vt = stream->vt;
const ScrVt *vt = scr_stream_vt_of(stream);
if (vt == NULL) {
value->v.typed_ref.release(stream);
return NULL;
}
bool duplex = scr_stream_dyn_within(vt, &scr_duplex_vt) ||
scr_stream_dyn_within(vt, &scr_transform_vt) ||
scr_stream_dyn_within(vt, &scr_passthrough_vt);
@@ -0,0 +1,39 @@
// Instances of a class hierarchy, held as untyped values, where the stream
// adapters probe for a native stream: pipe, pipeline, readline, and
// instanceof checks against the stream classes. Their first field is a
// number, which sits where a stream's vtable word would be.
import { Duplex, PassThrough, Readable, Transform, Writable, pipeline } from "node:stream";
import { createInterface } from "node:readline";
class Shape {
size = 3.5;
area(): number {
return 0;
}
}
class Square extends Shape {
override area(): number {
return this.size * this.size;
}
}
function attempt(label: string, run: () => unknown): void {
try {
run();
console.log(label, "ok");
} catch (error) {
console.log(label, "throws", error instanceof Error);
}
}
const shape: any = new Square();
try {
Readable.from(["a"]).pipe(shape);
} catch (error) {
console.log("pipe", (error as Error).name);
}
attempt("pipeline", () => pipeline(shape, new PassThrough(), () => {}));
attempt("readline-input", () => createInterface({ input: shape }));
console.log("instanceof", shape instanceof Readable, shape instanceof Writable, shape instanceof Duplex);
console.log("instanceof-transform", shape instanceof Transform, shape instanceof PassThrough);
console.log("area", (shape as Square).area());
@@ -0,0 +1 @@
{ "plain": 4000, "san": 4500 }