ilios 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4f2c920b53b519c07299b86d8c31c290d858609dfda93dcdec8b1e0b1dcd687
4
- data.tar.gz: c6070d007f1bdef3ba96dd5a2b4620e174f42c5cce6d662452be21fb5d54693d
3
+ metadata.gz: 119d5f16f005dbc317d42110162895ac90b7acc12fad39b4c2b33d04587ceeb9
4
+ data.tar.gz: d2b9caed7e97e5592737654171e6a06dea5cdecf872f5827b2547c6c4a704dc4
5
5
  SHA512:
6
- metadata.gz: d2a5d977c1ed2c756534697590f8affe5a93ded29a53225ff1f2b0ae41bce7ce9901a1310f5540fef037de53e3b3aff9fa5c073635d983f528558200bf38e6a7
7
- data.tar.gz: 8456a5c214c9a61bc0fcb5fd0558ea3e24a1345c3e77e8c3f94e4d85f2f7497b74bb1f1a5e755181521005d0102c52db92a3af0aa610be72b87296a32edb224f
6
+ metadata.gz: 291ee870b21e87e4d33056f4f5a08c513904cb967786115a3d618bfd61943f561d31744b3554b87f069dcc59d308c52366788561d15d8ae7266f83cb1b865d65
7
+ data.tar.gz: a3517d6c2d4f746dd66234e02a96b084069128442ddab5ad2044995fcb5418bbebbdc7f6d2d0b5dc9eeefe654646c7a42af36ffd607aa40dd8208726d3c8cb36
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.0.5
4
+
5
+ - Fix use-after-free when re-binding a statement with in-flight async executions (#24)
6
+
3
7
  ## 1.0.4
4
8
 
5
9
  - Fix macOS build failure with Apple Clang on macOS 26+ (#23)
data/ext/ilios/future.c CHANGED
@@ -116,6 +116,10 @@ static void future_result_success_yield(CassandraFuture *cassandra_future)
116
116
  cassandra_result_obj = CREATE_RESULT(cassandra_result);
117
117
  cassandra_result->result = cass_future_get_result(cassandra_future->future);
118
118
  cassandra_result->statement_obj = cassandra_future->statement_obj;
119
+ // Hand over the executed CassStatement so Result#next_page
120
+ // can reuse it and it gets freed exactly once.
121
+ cassandra_result->executed_statement = cassandra_future->executed_statement;
122
+ cassandra_future->executed_statement = NULL;
119
123
 
120
124
  obj = cassandra_result_obj;
121
125
  }
@@ -202,6 +206,7 @@ VALUE future_create(CassFuture *future, VALUE session, VALUE statement, future_k
202
206
  cassandra_future_obj = CREATE_FUTURE(cassandra_future);
203
207
  cassandra_future->kind = kind;
204
208
  cassandra_future->future = future;
209
+ cassandra_future->executed_statement = NULL;
205
210
  cassandra_future->session_obj = session;
206
211
  cassandra_future->statement_obj = statement;
207
212
  cassandra_future->proc_mutex = rb_mutex_new();
@@ -366,6 +371,11 @@ static void future_destroy(void *ptr)
366
371
  if (cassandra_future->future) {
367
372
  cass_future_free(cassandra_future->future);
368
373
  }
374
+ if (cassandra_future->executed_statement) {
375
+ // Safe even if the request is still in flight: the driver's request
376
+ // holds its own reference to the statement internals.
377
+ cass_statement_free(cassandra_future->executed_statement);
378
+ }
369
379
  uv_sem_destroy(&cassandra_future->sem);
370
380
  xfree(cassandra_future);
371
381
  }
data/ext/ilios/ilios.h CHANGED
@@ -27,6 +27,12 @@ typedef enum {
27
27
  execute_async
28
28
  } future_kind;
29
29
 
30
+ typedef enum {
31
+ idempotency_unset,
32
+ idempotency_false,
33
+ idempotency_true
34
+ } statement_idempotency;
35
+
30
36
  typedef struct
31
37
  {
32
38
  CassCluster* cluster;
@@ -40,21 +46,36 @@ typedef struct
40
46
 
41
47
  typedef struct
42
48
  {
49
+ // Scratch statement used for validating values in Statement#bind.
50
+ // It is never handed to the driver: each execution gets its own
51
+ // CassStatement built from `prepared` and `bound_values`, because the
52
+ // driver encodes values asynchronously on its IO thread and re-binding
53
+ // an in-flight statement is a use-after-free (issue #12).
43
54
  CassStatement* statement;
44
55
  const CassPrepared* prepared;
45
56
  VALUE session_obj;
57
+ VALUE bound_values;
58
+ int page_size;
59
+ statement_idempotency idempotent;
46
60
  } CassandraStatement;
47
61
 
48
62
  typedef struct
49
63
  {
50
64
  const CassResult *result;
51
65
  CassFuture *future;
66
+ // The CassStatement this result was executed with (owned, freed on destroy).
67
+ // Not to be confused with statement_obj, the Ruby Statement object.
68
+ CassStatement *executed_statement;
52
69
  VALUE statement_obj;
53
70
  } CassandraResult;
54
71
 
55
72
  typedef struct
56
73
  {
57
74
  CassFuture *future;
75
+ // The CassStatement this future's execution was submitted with (owned,
76
+ // freed on destroy unless handed over to the result). Not to be confused
77
+ // with statement_obj, the Ruby Statement object.
78
+ CassStatement *executed_statement;
58
79
  future_kind kind;
59
80
 
60
81
  VALUE session_obj;
@@ -108,6 +129,7 @@ extern CassFuture *nogvl_session_execute(CassSession* session, CassStatement* st
108
129
  extern void nogvl_sem_wait(uv_sem_t *sem);
109
130
 
110
131
  extern void statement_default_config(CassandraStatement *cassandra_statement);
132
+ extern CassStatement *statement_build_for_execution(CassandraStatement *cassandra_statement);
111
133
  extern void result_await(CassandraResult *cassandra_result);
112
134
 
113
135
 
data/ext/ilios/result.c CHANGED
@@ -55,12 +55,17 @@ static VALUE result_next_page(VALUE self)
55
55
  GET_STATEMENT(cassandra_result->statement_obj, cassandra_statement);
56
56
  GET_SESSION(cassandra_statement->session_obj, cassandra_session);
57
57
 
58
- cass_statement_set_paging_state(cassandra_statement->statement, cassandra_result->result);
58
+ // Reuse this result's own executed statement: it is not shared with other
59
+ // executions and its previous request has already completed, so setting
60
+ // the paging state cannot race with the driver's IO thread.
61
+ cass_statement_set_paging_state(cassandra_result->executed_statement, cassandra_result->result);
59
62
 
60
- result_future = nogvl_session_execute(cassandra_session->session, cassandra_statement->statement);
63
+ result_future = nogvl_session_execute(cassandra_session->session, cassandra_result->executed_statement);
61
64
 
62
65
  cass_result_free(cassandra_result->result);
63
- cass_future_free(cassandra_result->future);
66
+ if (cassandra_result->future) {
67
+ cass_future_free(cassandra_result->future);
68
+ }
64
69
  cassandra_result->result = NULL;
65
70
  cassandra_result->future = result_future;
66
71
 
@@ -245,6 +250,9 @@ static void result_destroy(void *ptr)
245
250
  if (cassandra_result->future) {
246
251
  cass_future_free(cassandra_result->future);
247
252
  }
253
+ if (cassandra_result->executed_statement) {
254
+ cass_statement_free(cassandra_result->executed_statement);
255
+ }
248
256
  xfree(cassandra_result);
249
257
  }
250
258
 
data/ext/ilios/session.c CHANGED
@@ -85,13 +85,24 @@ static VALUE session_execute_async(VALUE self, VALUE statement)
85
85
  {
86
86
  CassandraSession *cassandra_session;
87
87
  CassandraStatement *cassandra_statement;
88
+ CassandraFuture *cassandra_future;
89
+ CassStatement *executed_statement;
88
90
  CassFuture *result_future;
91
+ VALUE future;
89
92
 
90
93
  GET_SESSION(self, cassandra_session);
91
94
  GET_STATEMENT(statement, cassandra_statement);
92
95
 
93
- result_future = nogvl_session_execute(cassandra_session->session, cassandra_statement->statement);
94
- return future_create(result_future, self, statement, execute_async);
96
+ // Execute a dedicated statement so that later re-binds of `statement`
97
+ // cannot race with the driver's asynchronous encoding (issue #12).
98
+ executed_statement = statement_build_for_execution(cassandra_statement);
99
+ result_future = nogvl_session_execute(cassandra_session->session, executed_statement);
100
+
101
+ future = future_create(result_future, self, statement, execute_async);
102
+ GET_FUTURE(future, cassandra_future);
103
+ // The future owns the executed statement and frees it on destroy.
104
+ cassandra_future->executed_statement = executed_statement;
105
+ return future;
95
106
  }
96
107
 
97
108
  /**
@@ -107,15 +118,18 @@ static VALUE session_execute(VALUE self, VALUE statement)
107
118
  CassandraSession *cassandra_session;
108
119
  CassandraStatement *cassandra_statement;
109
120
  CassandraResult *cassandra_result;
121
+ CassStatement *executed_statement;
110
122
  CassFuture *result_future;
111
123
  VALUE cassandra_result_obj;
112
124
 
113
125
  GET_SESSION(self, cassandra_session);
114
126
  GET_STATEMENT(statement, cassandra_statement);
115
127
 
116
- result_future = nogvl_session_execute(cassandra_session->session, cassandra_statement->statement);
128
+ executed_statement = statement_build_for_execution(cassandra_statement);
129
+ result_future = nogvl_session_execute(cassandra_session->session, executed_statement);
117
130
 
118
131
  cassandra_result_obj = CREATE_RESULT(cassandra_result);
132
+ cassandra_result->executed_statement = executed_statement;
119
133
  cassandra_result->future = result_future;
120
134
  cassandra_result->statement_obj = statement;
121
135
 
@@ -17,14 +17,24 @@ const rb_data_type_t cassandra_statement_data_type = {
17
17
  RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FROZEN_SHAREABLE,
18
18
  };
19
19
 
20
+ typedef struct
21
+ {
22
+ const CassPrepared *prepared;
23
+ CassStatement *statement;
24
+ VALUE bound_values;
25
+ } statement_bind_context;
26
+
20
27
  void statement_default_config(CassandraStatement *cassandra_statement)
21
28
  {
29
+ cassandra_statement->bound_values = Qnil;
30
+ cassandra_statement->page_size = DEFAULT_PAGE_SIZE;
31
+ cassandra_statement->idempotent = idempotency_unset;
22
32
  cass_statement_set_paging_size(cassandra_statement->statement, DEFAULT_PAGE_SIZE);
23
33
  }
24
34
 
25
- static int hash_cb(VALUE key, VALUE value, VALUE statement)
35
+ static int hash_cb(VALUE key, VALUE value, VALUE arg)
26
36
  {
27
- CassandraStatement *cassandra_statement = (CassandraStatement *)statement;
37
+ statement_bind_context *ctx = (statement_bind_context *)arg;
28
38
  const CassDataType* data_type;
29
39
  CassValueType value_type;
30
40
  CassError result;
@@ -35,14 +45,14 @@ static int hash_cb(VALUE key, VALUE value, VALUE statement)
35
45
  }
36
46
  name = StringValueCStr(key);
37
47
 
38
- data_type = cass_prepared_parameter_data_type_by_name(cassandra_statement->prepared, name);
48
+ data_type = cass_prepared_parameter_data_type_by_name(ctx->prepared, name);
39
49
  if (data_type == NULL) {
40
50
  rb_raise(eStatementError, "Invalid name %s was given.", name);
41
51
  }
42
52
  value_type = cass_data_type_type(data_type);
43
53
 
44
54
  if (NIL_P(value)) {
45
- result = cass_statement_bind_null_by_name(cassandra_statement->statement, name);
55
+ result = cass_statement_bind_null_by_name(ctx->statement, name);
46
56
  goto result_check;
47
57
  }
48
58
 
@@ -54,7 +64,7 @@ static int hash_cb(VALUE key, VALUE value, VALUE statement)
54
64
  if (v < INT8_MIN || v > INT8_MAX) {
55
65
  rb_raise(rb_eRangeError, "Invalid value: %ld", v);
56
66
  }
57
- result = cass_statement_bind_int8_by_name(cassandra_statement->statement, name, (cass_int8_t)v);
67
+ result = cass_statement_bind_int8_by_name(ctx->statement, name, (cass_int8_t)v);
58
68
  }
59
69
  break;
60
70
 
@@ -66,7 +76,7 @@ static int hash_cb(VALUE key, VALUE value, VALUE statement)
66
76
  rb_raise(rb_eRangeError, "Invalid value: %ld", v);
67
77
  }
68
78
 
69
- result = cass_statement_bind_int16_by_name(cassandra_statement->statement, name, (cass_int16_t)v);
79
+ result = cass_statement_bind_int16_by_name(ctx->statement, name, (cass_int16_t)v);
70
80
  }
71
81
  break;
72
82
 
@@ -78,12 +88,12 @@ static int hash_cb(VALUE key, VALUE value, VALUE statement)
78
88
  rb_raise(rb_eRangeError, "Invalid value: %ld", v);
79
89
  }
80
90
 
81
- result = cass_statement_bind_int32_by_name(cassandra_statement->statement, name, (cass_int32_t)v);
91
+ result = cass_statement_bind_int32_by_name(ctx->statement, name, (cass_int32_t)v);
82
92
  }
83
93
  break;
84
94
 
85
95
  case CASS_VALUE_TYPE_BIGINT:
86
- result = cass_statement_bind_int64_by_name(cassandra_statement->statement, name, NUM2LONG(value));
96
+ result = cass_statement_bind_int64_by_name(ctx->statement, name, NUM2LONG(value));
87
97
  break;
88
98
 
89
99
  case CASS_VALUE_TYPE_FLOAT:
@@ -94,25 +104,25 @@ static int hash_cb(VALUE key, VALUE value, VALUE statement)
94
104
  rb_raise(rb_eRangeError, "Invalid value: %lf", v);
95
105
  }
96
106
 
97
- result = cass_statement_bind_float_by_name(cassandra_statement->statement, name, v);
107
+ result = cass_statement_bind_float_by_name(ctx->statement, name, v);
98
108
  }
99
109
  break;
100
110
 
101
111
  case CASS_VALUE_TYPE_DOUBLE:
102
- result = cass_statement_bind_double_by_name(cassandra_statement->statement, name, NUM2DBL(value));
112
+ result = cass_statement_bind_double_by_name(ctx->statement, name, NUM2DBL(value));
103
113
  break;
104
114
 
105
115
  case CASS_VALUE_TYPE_BOOLEAN:
106
116
  {
107
117
  cass_bool_t v = RTEST(value) ? cass_true : cass_false;
108
- result = cass_statement_bind_bool_by_name(cassandra_statement->statement, name, v);
118
+ result = cass_statement_bind_bool_by_name(ctx->statement, name, v);
109
119
  }
110
120
  break;
111
121
 
112
122
  case CASS_VALUE_TYPE_TEXT:
113
123
  case CASS_VALUE_TYPE_ASCII:
114
124
  case CASS_VALUE_TYPE_VARCHAR:
115
- result = cass_statement_bind_string_by_name(cassandra_statement->statement, name, StringValueCStr(value));
125
+ result = cass_statement_bind_string_by_name(ctx->statement, name, StringValueCStr(value));
116
126
  break;
117
127
 
118
128
  case CASS_VALUE_TYPE_TIMESTAMP:
@@ -123,7 +133,7 @@ static int hash_cb(VALUE key, VALUE value, VALUE statement)
123
133
  rb_raise(rb_eTypeError, "no implicit conversion of %"PRIsVALUE" to Time", rb_obj_class(value));
124
134
  }
125
135
  }
126
- result = cass_statement_bind_int64_by_name(cassandra_statement->statement, name, (cass_int64_t)(NUM2DBL(rb_Float(value)) * 1000));
136
+ result = cass_statement_bind_int64_by_name(ctx->statement, name, (cass_int64_t)(NUM2DBL(rb_Float(value)) * 1000));
127
137
  break;
128
138
 
129
139
  case CASS_VALUE_TYPE_UUID:
@@ -132,7 +142,7 @@ static int hash_cb(VALUE key, VALUE value, VALUE statement)
132
142
  const char *uuid_string = StringValueCStr(value);
133
143
 
134
144
  cass_uuid_from_string(uuid_string, &uuid);
135
- result = cass_statement_bind_uuid_by_name(cassandra_statement->statement, name, uuid);
145
+ result = cass_statement_bind_uuid_by_name(ctx->statement, name, uuid);
136
146
  }
137
147
  break;
138
148
 
@@ -145,9 +155,65 @@ result_check:
145
155
  rb_raise(eStatementError, "Failed to bind value: %s", cass_error_desc(result));
146
156
  }
147
157
 
158
+ if (!NIL_P(ctx->bound_values)) {
159
+ if (RB_TYPE_P(value, T_STRING)) {
160
+ // Snapshot the value so a later in-place mutation by the caller
161
+ // doesn't change what gets bound at execution time.
162
+ value = rb_str_new_frozen(value);
163
+ }
164
+ rb_hash_aset(ctx->bound_values, key, value);
165
+ }
166
+
148
167
  return ST_CONTINUE;
149
168
  }
150
169
 
170
+ typedef struct
171
+ {
172
+ statement_bind_context ctx;
173
+ VALUE hash;
174
+ } statement_rebind_args;
175
+
176
+ static VALUE statement_rebind_body(VALUE arg)
177
+ {
178
+ statement_rebind_args *args = (statement_rebind_args *)arg;
179
+
180
+ rb_hash_foreach(args->hash, hash_cb, (VALUE)&args->ctx);
181
+ return Qnil;
182
+ }
183
+
184
+ /*
185
+ * Builds a fresh CassStatement carrying the current configuration and bound
186
+ * values for a single execution. The returned statement must not be mutated
187
+ * once handed to the driver, and the caller owns it: it must stay alive until
188
+ * the execution's future resolves and be freed exactly once afterwards.
189
+ */
190
+ CassStatement *statement_build_for_execution(CassandraStatement *cassandra_statement)
191
+ {
192
+ CassStatement *statement = cass_prepared_bind(cassandra_statement->prepared);
193
+
194
+ cass_statement_set_paging_size(statement, cassandra_statement->page_size);
195
+ if (cassandra_statement->idempotent != idempotency_unset) {
196
+ cass_statement_set_is_idempotent(statement, cassandra_statement->idempotent == idempotency_true ? cass_true : cass_false);
197
+ }
198
+
199
+ if (!NIL_P(cassandra_statement->bound_values)) {
200
+ statement_rebind_args args;
201
+ int state = 0;
202
+
203
+ args.ctx.prepared = cassandra_statement->prepared;
204
+ args.ctx.statement = statement;
205
+ args.ctx.bound_values = Qnil;
206
+ args.hash = cassandra_statement->bound_values;
207
+
208
+ rb_protect(statement_rebind_body, (VALUE)&args, &state);
209
+ if (state) {
210
+ cass_statement_free(statement);
211
+ rb_jump_tag(state);
212
+ }
213
+ }
214
+ return statement;
215
+ }
216
+
151
217
  /**
152
218
  * Binds a specified column value to a query.
153
219
  * A hash object should be given with column name as key.
@@ -161,11 +227,27 @@ result_check:
161
227
  static VALUE statement_bind(VALUE self, VALUE hash)
162
228
  {
163
229
  CassandraStatement *cassandra_statement;
230
+ statement_bind_context ctx;
231
+ VALUE bound_values;
164
232
 
165
233
  Check_Type(hash, T_HASH);
166
234
  TypedData_Get_Struct(self, CassandraStatement, &cassandra_statement_data_type, cassandra_statement);
167
235
 
168
- rb_hash_foreach(hash, hash_cb, (VALUE)cassandra_statement);
236
+ // Merge into a copy instead of mutating in place: the previous hash may be
237
+ // shared with a frozen (Ractor-shareable) statement or be iterated by an
238
+ // execution on another thread.
239
+ if (NIL_P(cassandra_statement->bound_values)) {
240
+ bound_values = rb_hash_new();
241
+ } else {
242
+ bound_values = rb_hash_dup(cassandra_statement->bound_values);
243
+ }
244
+ RB_OBJ_WRITE(self, &cassandra_statement->bound_values, bound_values);
245
+
246
+ ctx.prepared = cassandra_statement->prepared;
247
+ ctx.statement = cassandra_statement->statement;
248
+ ctx.bound_values = bound_values;
249
+
250
+ rb_hash_foreach(hash, hash_cb, (VALUE)&ctx);
169
251
  return self;
170
252
  }
171
253
 
@@ -180,7 +262,8 @@ static VALUE statement_page_size(VALUE self, VALUE page_size)
180
262
  CassandraStatement *cassandra_statement;
181
263
 
182
264
  GET_STATEMENT(self, cassandra_statement);
183
- cass_statement_set_paging_size(cassandra_statement->statement, NUM2INT(page_size));
265
+ cassandra_statement->page_size = NUM2INT(page_size);
266
+ cass_statement_set_paging_size(cassandra_statement->statement, cassandra_statement->page_size);
184
267
  return self;
185
268
  }
186
269
 
@@ -197,7 +280,8 @@ static VALUE statement_idempotent(VALUE self, VALUE idempotent)
197
280
  CassandraStatement *cassandra_statement;
198
281
 
199
282
  GET_STATEMENT(self, cassandra_statement);
200
- cass_statement_set_is_idempotent(cassandra_statement->statement, RTEST(idempotent) ? cass_true : cass_false);
283
+ cassandra_statement->idempotent = RTEST(idempotent) ? idempotency_true : idempotency_false;
284
+ cass_statement_set_is_idempotent(cassandra_statement->statement, cassandra_statement->idempotent == idempotency_true ? cass_true : cass_false);
201
285
  return self;
202
286
  }
203
287
 
@@ -205,6 +289,7 @@ static void statement_mark(void *ptr)
205
289
  {
206
290
  CassandraStatement *cassandra_statement = (CassandraStatement *)ptr;
207
291
  rb_gc_mark_movable(cassandra_statement->session_obj);
292
+ rb_gc_mark_movable(cassandra_statement->bound_values);
208
293
  }
209
294
 
210
295
  static void statement_destroy(void *ptr)
@@ -230,6 +315,7 @@ static void statement_compact(void *ptr)
230
315
  CassandraStatement *cassandra_statement = (CassandraStatement *)ptr;
231
316
 
232
317
  cassandra_statement->session_obj = rb_gc_location(cassandra_statement->session_obj);
318
+ cassandra_statement->bound_values = rb_gc_location(cassandra_statement->bound_values);
233
319
  }
234
320
 
235
321
  void Init_statement(void)
data/lib/ilios/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ilios
4
- VERSION = '1.0.4'
4
+ VERSION = '1.0.5'
5
5
  public_constant :VERSION
6
6
 
7
7
  CASSANDRA_CPP_DRIVER_VERSION = '2.17.1'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ilios
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Watson
@@ -58,7 +58,7 @@ metadata:
58
58
  homepage_uri: https://github.com/Watson1978/ilios
59
59
  source_code_uri: https://github.com/Watson1978/ilios
60
60
  bug_tracker_uri: https://github.com/Watson1978/ilios/issues
61
- documentation_uri: https://www.rubydoc.info/gems/ilios/1.0.4
61
+ documentation_uri: https://www.rubydoc.info/gems/ilios/1.0.5
62
62
  rubygems_mfa_required: 'true'
63
63
  rdoc_options: []
64
64
  require_paths: