Skip to content

Commit 3eb33c6

Browse files
committed
Fixed error handling to prevent crashing ruby
Observed errors included: - Segmentation fault - cfp consistency error - object allocation during garbage collection phase These errors manifested themselves most consistently when a SQL error occurred while using threads, but would also happen occasionally without threads being involved. With TinyTds 0.6.0 changes were made to make the C code properly release the Global VM Lock (GVL) so that other threads could run while FreeTDS was waiting for results from the server (blocked on I/O). While this lock is released it is not possible to safely call the ruby C API. The way that FreeTDS deals with error messages is to define a handler to process the message when it is raised. The issue is that the error handler that TinyTds uses invokes the ruby C API to throw an exception. This accidental use of the ruby C API while TinyTds did not have the GVL caused various catastrophic conditions in the ruby VM. These conditions manifested themselves as the various error messages listed above. To fix these errors, the userdata struct is used to track if the client is currently "nonblocking" and thus does not have the GVL. In those situations, any errors are saved into a new tinytds_errordata struct that saves all necessary information. Once the GVL is re-obtained, TinyTds uses the information in the tinytds_errordata struct to throw an exception. To further complicate the issue, FreeTDS will send multiple messages/ errors if the error is severe enough. Normally, when exceptions are raised immediately, the developer will properly receive the first, detailed, message. However, when TinyTds does not have the GVL and the error is stored for later use, FreeTDS has the opportunity to send more messages, causing the first one to be overwritten with something more generic. Because of this, as soon as the first message is captured, all further messages are ignored until TinyTds::Client.execute() is called again.
1 parent 7af16cc commit 3eb33c6

File tree

4 files changed

+123
-25
lines changed

4 files changed

+123
-25
lines changed

ext/tiny_tds/client.c

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ VALUE opt_escape_regex, opt_escape_dblquote;
2424

2525
// Lib Backend (Helpers)
2626

27-
static VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, int cancel, char *error, char *source, int severity, int dberr, int oserr) {
27+
VALUE rb_tinytds_raise_error(DBPROCESS *dbproc, int cancel, char *error, char *source, int severity, int dberr, int oserr) {
2828
GET_CLIENT_USERDATA(dbproc);
2929
if (cancel && !dbdead(dbproc) && userdata && !userdata->closed) {
3030
userdata->dbsqlok_sent = 1;
@@ -92,14 +92,54 @@ int tinytds_err_handler(DBPROCESS *dbproc, int severity, int dberr, int oserr, c
9292
break;
9393
}
9494
}
95-
rb_tinytds_raise_error(dbproc, cancel, dberrstr, source, severity, dberr, oserr);
95+
/*
96+
When in non-blocking mode we need to store the exception data to throw it
97+
once the blocking call returns, otherwise we will segfault ruby since part
98+
of the contract of the ruby non-blocking indicator is that you do not call
99+
any of the ruby C API.
100+
*/
101+
if (userdata && userdata->nonblocking) {
102+
/*
103+
If we've already captured an error message, don't overwrite it. This is
104+
here because FreeTDS sends a generic "General SQL Server error" message
105+
that will overwrite the real message. This is not normally a problem
106+
because a ruby exception is normally thrown and we bail before the
107+
generic message can be sent.
108+
*/
109+
if (!userdata->nonblocking_error.is_set) {
110+
userdata->nonblocking_error.cancel = cancel;
111+
strcpy(userdata->nonblocking_error.error, dberrstr);
112+
strcpy(userdata->nonblocking_error.source, source);
113+
userdata->nonblocking_error.severity = severity;
114+
userdata->nonblocking_error.dberr = dberr;
115+
userdata->nonblocking_error.oserr = oserr;
116+
userdata->nonblocking_error.is_set = 1;
117+
}
118+
} else {
119+
rb_tinytds_raise_error(dbproc, cancel, dberrstr, source, severity, dberr, oserr);
120+
}
96121
return return_value;
97122
}
98123

99124
int tinytds_msg_handler(DBPROCESS *dbproc, DBINT msgno, int msgstate, int severity, char *msgtext, char *srvname, char *procname, int line) {
100125
static char *source = "message";
101-
if (severity > 10)
102-
rb_tinytds_raise_error(dbproc, 1, msgtext, source, severity, msgno, msgstate);
126+
GET_CLIENT_USERDATA(dbproc);
127+
if (severity > 10) {
128+
// See tinytds_err_handler() for info about why we do this
129+
if (userdata && userdata->nonblocking) {
130+
if (!userdata->nonblocking_error.is_set) {
131+
userdata->nonblocking_error.cancel = 1;
132+
strcpy(userdata->nonblocking_error.error, msgtext);
133+
strcpy(userdata->nonblocking_error.source, source);
134+
userdata->nonblocking_error.severity = severity;
135+
userdata->nonblocking_error.dberr = msgno;
136+
userdata->nonblocking_error.oserr = msgstate;
137+
userdata->nonblocking_error.is_set = 1;
138+
}
139+
} else {
140+
rb_tinytds_raise_error(dbproc, 1, msgtext, source, severity, msgno, msgstate);
141+
}
142+
}
103143
return 0;
104144
}
105145

@@ -108,6 +148,8 @@ static void rb_tinytds_client_reset_userdata(tinytds_client_userdata *userdata)
108148
userdata->dbsql_sent = 0;
109149
userdata->dbsqlok_sent = 0;
110150
userdata->dbcancel_sent = 0;
151+
userdata->nonblocking = 0;
152+
userdata->nonblocking_error.is_set = 0;
111153
}
112154

113155
static void rb_tinytds_client_mark(void *ptr) {

ext/tiny_tds/client.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@
44

55
void init_tinytds_client();
66

7+
typedef struct {
8+
short int is_set;
9+
int cancel;
10+
char error[1024];
11+
char source[1024];
12+
int severity;
13+
int dberr;
14+
int oserr;
15+
} tinytds_errordata;
16+
717
typedef struct {
818
short int closed;
919
short int timing_out;
1020
short int dbsql_sent;
1121
short int dbsqlok_sent;
1222
RETCODE dbsqlok_retcode;
1323
short int dbcancel_sent;
24+
short int nonblocking;
25+
tinytds_errordata nonblocking_error;
1426
} tinytds_client_userdata;
1527

1628
typedef struct {

ext/tiny_tds/result.c

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,35 +79,73 @@ VALUE rb_tinytds_new_result_obj(tinytds_client_wrapper *cwrap) {
7979

8080
#define NOGVL_DBCALL(_dbfunction, _client) ( \
8181
(RETCODE)rb_thread_blocking_region( \
82-
(rb_blocking_function_t*)nogvl_ ## _dbfunction, _client, \
82+
(rb_blocking_function_t*)_dbfunction, _client, \
8383
(rb_unblock_function_t*)dbcancel_ubf, _client ) \
8484
)
8585

86+
static void dbcancel_ubf(DBPROCESS *client) {
87+
GET_CLIENT_USERDATA(client);
88+
dbcancel(client);
89+
userdata->dbcancel_sent = 1;
90+
userdata->dbsql_sent = 0;
91+
}
92+
93+
static void nogvl_setup(DBPROCESS *client) {
94+
GET_CLIENT_USERDATA(client);
95+
userdata->nonblocking = 1;
96+
}
97+
98+
static void nogvl_cleanup(DBPROCESS *client) {
99+
GET_CLIENT_USERDATA(client);
100+
userdata->nonblocking = 0;
101+
/*
102+
Now that the blocking operation is done, we can finally throw any
103+
exceptions based on errors from SQL Server.
104+
*/
105+
if (userdata->nonblocking_error.is_set) {
106+
userdata->nonblocking_error.is_set = 0;
107+
rb_tinytds_raise_error(client,
108+
userdata->nonblocking_error.cancel,
109+
&userdata->nonblocking_error.error,
110+
&userdata->nonblocking_error.source,
111+
userdata->nonblocking_error.severity,
112+
userdata->nonblocking_error.dberr,
113+
userdata->nonblocking_error.oserr);
114+
}
115+
}
116+
86117
static RETCODE nogvl_dbsqlok(DBPROCESS *client) {
87118
int retcode = FAIL;
88119
GET_CLIENT_USERDATA(client);
89-
retcode = dbsqlok(client);
120+
nogvl_setup(client);
121+
retcode = NOGVL_DBCALL(dbsqlok, client);
122+
nogvl_cleanup(client);
90123
userdata->dbsqlok_sent = 1;
91124
return retcode;
92125
}
93126

94127
static RETCODE nogvl_dbsqlexec(DBPROCESS *client) {
95-
return dbsqlexec(client);
128+
int retcode = FAIL;
129+
nogvl_setup(client);
130+
retcode = NOGVL_DBCALL(dbsqlexec, client);
131+
nogvl_cleanup(client);
132+
return retcode;
96133
}
97134

98135
static RETCODE nogvl_dbresults(DBPROCESS *client) {
99-
return dbresults(client);
136+
int retcode = FAIL;
137+
nogvl_setup(client);
138+
retcode = NOGVL_DBCALL(dbresults, client);
139+
nogvl_cleanup(client);
140+
return retcode;
100141
}
101142

102143
static RETCODE nogvl_dbnextrow(DBPROCESS * client) {
103-
return dbnextrow(client);
104-
}
105-
106-
static void dbcancel_ubf(DBPROCESS *client) {
107-
GET_CLIENT_USERDATA(client);
108-
dbcancel(client);
109-
userdata->dbcancel_sent = 1;
110-
userdata->dbsql_sent = 0;
144+
int retcode = FAIL;
145+
nogvl_setup(client);
146+
retcode = NOGVL_DBCALL(dbnextrow, client);
147+
nogvl_cleanup(client);
148+
return retcode;
111149
}
112150

113151
// Lib Backend (Helpers)
@@ -118,7 +156,7 @@ static RETCODE rb_tinytds_result_dbresults_retcode(VALUE self) {
118156
RETCODE db_rc;
119157
ruby_rc = rb_ary_entry(rwrap->dbresults_retcodes, rwrap->number_of_results);
120158
if (NIL_P(ruby_rc)) {
121-
db_rc = NOGVL_DBCALL(dbresults, rwrap->client);
159+
db_rc = nogvl_dbresults(rwrap->client);
122160
ruby_rc = INT2FIX(db_rc);
123161
rb_ary_store(rwrap->dbresults_retcodes, rwrap->number_of_results, ruby_rc);
124162
} else {
@@ -130,7 +168,7 @@ static RETCODE rb_tinytds_result_dbresults_retcode(VALUE self) {
130168
static RETCODE rb_tinytds_result_ok_helper(DBPROCESS *client) {
131169
GET_CLIENT_USERDATA(client);
132170
if (userdata->dbsqlok_sent == 0) {
133-
userdata->dbsqlok_retcode = NOGVL_DBCALL(dbsqlok, client);
171+
userdata->dbsqlok_retcode = nogvl_dbsqlok(client);
134172
}
135173
return userdata->dbsqlok_retcode;
136174
}
@@ -373,7 +411,7 @@ static VALUE rb_tinytds_result_each(int argc, VALUE * argv, VALUE self) {
373411
/* Create rows for this result set. */
374412
unsigned long rowi = 0;
375413
VALUE result = rb_ary_new();
376-
while (NOGVL_DBCALL(dbnextrow, rwrap->client) != NO_MORE_ROWS) {
414+
while (nogvl_dbnextrow(rwrap->client) != NO_MORE_ROWS) {
377415
VALUE row = rb_tinytds_result_fetch_row(self, timezone, symbolize_keys, as_array);
378416
if (cache_rows)
379417
rb_ary_store(result, rowi, row);
@@ -406,7 +444,7 @@ static VALUE rb_tinytds_result_each(int argc, VALUE * argv, VALUE self) {
406444
} else {
407445
// If we do not find results, side step the rb_tinytds_result_dbresults_retcode helper and
408446
// manually populate its memoized array while nullifing any memoized fields too before loop.
409-
dbresults_rc = NOGVL_DBCALL(dbresults, rwrap->client);
447+
dbresults_rc = nogvl_dbresults(rwrap->client);
410448
rb_ary_store(rwrap->dbresults_retcodes, rwrap->number_of_results, INT2FIX(dbresults_rc));
411449
rb_ary_store(rwrap->fields_processed, rwrap->number_of_results, Qnil);
412450
}
@@ -466,10 +504,10 @@ static VALUE rb_tinytds_result_insert(VALUE self) {
466504
rb_tinytds_result_cancel_helper(rwrap->client);
467505
VALUE identity = Qnil;
468506
dbcmd(rwrap->client, rwrap->cwrap->identity_insert_sql);
469-
if (NOGVL_DBCALL(dbsqlexec, rwrap->client) != FAIL
470-
&& NOGVL_DBCALL(dbresults, rwrap->client) != FAIL
507+
if (nogvl_dbsqlexec(rwrap->client) != FAIL
508+
&& nogvl_dbresults(rwrap->client) != FAIL
471509
&& DBROWS(rwrap->client) != FAIL) {
472-
while (NOGVL_DBCALL(dbnextrow, rwrap->client) != NO_MORE_ROWS) {
510+
while (nogvl_dbnextrow(rwrap->client) != NO_MORE_ROWS) {
473511
int col = 1;
474512
BYTE *data = dbdata(rwrap->client, col);
475513
DBINT data_len = dbdatlen(rwrap->client, col);

test/thread_test.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,18 @@ class ThreadTest < TinyTds::TestCase
4444
start = Time.new
4545
threads << Thread.new do
4646
@pool.with do |client|
47-
result = client.execute "select dbname()"
48-
result.each { |r| puts r }
47+
begin
48+
result = client.execute "select dbname()"
49+
result.each { |r| puts r }
50+
rescue Exception => e
51+
# We are throwing an error on purpose here since 0.6.1 would
52+
# segfault on errors thrown in threads
53+
end
4954
end
5055
end
5156
end
5257
threads.each { |t| t.join }
58+
assert true
5359
end
5460

5561
end

0 commit comments

Comments
 (0)