Skip to content

Commit 5d5d8a7

Browse files
committed
ossl.c: implement OpenSSL::OpenSSLError#detailed_message
An OpenSSL function sometimes puts more than one error entry into the thread-local error queue. We currently use the last, the highest level entry for generating the exception message. The rest is silently dropped unless OpenSSL.debug is set to true, which makes it print to rb_warn(). Capture all current OpenSSL error queue contents into OpenSSL::OpenSSLError#errors in ossl_make_error(), and extend OpenSSL::OpenSSLError#detailed_message to include the information. An example: $ ruby -Ilib -ropenssl -e'OpenSSL::X509::ExtensionFactory.new.create_ext("a", "b")' -e:1:in 'OpenSSL::X509::ExtensionFactory#create_ext': a = b: error in extension (name=a, value=b) (OpenSSL::X509::ExtensionError) OpenSSL error queue reported 2 errors: error:11000082:X509 V3 routines:do_ext_nconf:unknown extension name error:11000080:X509 V3 routines:X509V3_EXT_nconf_int:error in extension (name=a, value=b) from -e:1:in '<main>'
1 parent 3e4c23e commit 5d5d8a7

File tree

2 files changed

+101
-15
lines changed

2 files changed

+101
-15
lines changed

ext/openssl/ossl.c

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,17 @@ ossl_to_der_if_possible(VALUE obj)
254254
/*
255255
* Errors
256256
*/
257+
static ID id_i_errors;
258+
259+
static void collect_errors_into(VALUE ary);
260+
257261
VALUE
258262
ossl_make_error(VALUE exc, VALUE str)
259263
{
260264
unsigned long e;
261265
const char *data;
262266
int flags;
267+
VALUE errors = rb_ary_new();
263268

264269
if (NIL_P(str))
265270
str = rb_str_new(NULL, 0);
@@ -276,10 +281,12 @@ ossl_make_error(VALUE exc, VALUE str)
276281
rb_str_cat_cstr(str, msg ? msg : "(null)");
277282
if (flags & ERR_TXT_STRING && data)
278283
rb_str_catf(str, " (%s)", data);
279-
ossl_clear_error();
284+
collect_errors_into(errors);
280285
}
281286

282-
return rb_exc_new_str(exc, str);
287+
VALUE obj = rb_exc_new_str(exc, str);
288+
rb_ivar_set(obj, id_i_errors, errors);
289+
return obj;
283290
}
284291

285292
void
@@ -300,13 +307,12 @@ ossl_raise(VALUE exc, const char *fmt, ...)
300307
rb_exc_raise(ossl_make_error(exc, err));
301308
}
302309

303-
void
304-
ossl_clear_error(void)
310+
static void
311+
collect_errors_into(VALUE ary)
305312
{
306-
if (dOSSL == Qtrue) {
313+
if (dOSSL == Qtrue || !NIL_P(ary)) {
307314
unsigned long e;
308315
const char *file, *data, *func, *lib, *reason;
309-
char append[256] = "";
310316
int line, flags;
311317

312318
#ifdef HAVE_ERR_GET_ERROR_ALL
@@ -318,20 +324,76 @@ ossl_clear_error(void)
318324
lib = ERR_lib_error_string(e);
319325
reason = ERR_reason_error_string(e);
320326

327+
VALUE str = rb_sprintf("error:%08lX:%s:%s:%s", e, lib ? lib : "",
328+
func ? func : "", reason ? reason : "");
321329
if (flags & ERR_TXT_STRING) {
322330
if (!data)
323331
data = "(null)";
324-
snprintf(append, sizeof(append), " (%s)", data);
332+
rb_str_catf(str, " (%s)", data);
325333
}
326-
rb_warn("error on stack: error:%08lX:%s:%s:%s%s", e, lib ? lib : "",
327-
func ? func : "", reason ? reason : "", append);
334+
335+
if (dOSSL == Qtrue)
336+
rb_warn("error on stack: %"PRIsVALUE, str);
337+
if (!NIL_P(ary))
338+
rb_ary_push(ary, str);
328339
}
329340
}
330341
else {
331342
ERR_clear_error();
332343
}
333344
}
334345

346+
void
347+
ossl_clear_error(void)
348+
{
349+
collect_errors_into(Qnil);
350+
}
351+
352+
static VALUE
353+
call_super(VALUE args_)
354+
{
355+
VALUE *args = (VALUE *)args_;
356+
int argc = (int)args[0];
357+
VALUE *argv = (VALUE *)args[1];
358+
return rb_call_super_kw(argc, argv, RB_PASS_CALLED_KEYWORDS);
359+
}
360+
361+
static VALUE
362+
call_message(VALUE self, VALUE exc)
363+
{
364+
return rb_funcall(self, rb_intern("message"), 0);
365+
}
366+
367+
/*
368+
* call-seq:
369+
* ossl_error.detailed_message(**) -> string
370+
*
371+
* Returns the exception message with the captured \OpenSSL error queue
372+
* entries.
373+
*/
374+
static VALUE
375+
osslerror_detailed_message(int argc, VALUE *argv, VALUE self)
376+
{
377+
// Exception#detailed_message is in Ruby >= 3.2
378+
VALUE args[] = { (VALUE)argc, (VALUE)argv };
379+
VALUE str = rb_rescue2(call_super, (VALUE)args, call_message, self,
380+
rb_eNoMethodError, (VALUE)0);
381+
VALUE errors = rb_attr_get(self, id_i_errors);
382+
383+
// OpenSSLError was not created by ossl_make_error()
384+
if (!RB_TYPE_P(errors, T_ARRAY))
385+
return str;
386+
387+
str = rb_str_resurrect(str);
388+
rb_str_catf(str, "\nOpenSSL error queue reported %ld errors:",
389+
RARRAY_LEN(errors));
390+
for (long i = 0; i < RARRAY_LEN(errors); i++) {
391+
VALUE err = RARRAY_AREF(errors, i);
392+
rb_str_catf(str, "\n%"PRIsVALUE, err);
393+
}
394+
return str;
395+
}
396+
335397
/*
336398
* call-seq:
337399
* OpenSSL.errors -> [String...]
@@ -1009,10 +1071,26 @@ Init_openssl(void)
10091071

10101072
rb_global_variable(&eOSSLError);
10111073
/*
1012-
* Generic error,
1013-
* common for all classes under OpenSSL module
1074+
* Generic error class for OpenSSL. All error classes in this library
1075+
* inherit from this class.
1076+
*
1077+
* This class indicates that an error was reported by the underlying
1078+
* \OpenSSL library.
1079+
*/
1080+
eOSSLError = rb_define_class_under(mOSSL, "OpenSSLError", rb_eStandardError);
1081+
/*
1082+
* \OpenSSL error queue entries captured at the time the exception was
1083+
* raised. The same information is printed to stderr if OpenSSL.debug is
1084+
* set to +true+.
1085+
*
1086+
* This is an array of zero or more strings, ordered from the oldest to the
1087+
* newest. The format of the strings is not stable and may vary across
1088+
* versions of \OpenSSL or versions of this Ruby extension.
1089+
*
1090+
* See also the man page ERR_get_error(3).
10141091
*/
1015-
eOSSLError = rb_define_class_under(mOSSL,"OpenSSLError",rb_eStandardError);
1092+
rb_attr(eOSSLError, rb_intern_const("errors"), 1, 0, 0);
1093+
rb_define_method(eOSSLError, "detailed_message", osslerror_detailed_message, -1);
10161094

10171095
/*
10181096
* Init debug core
@@ -1028,6 +1106,7 @@ Init_openssl(void)
10281106
* Get ID of to_der
10291107
*/
10301108
ossl_s_to_der = rb_intern("to_der");
1109+
id_i_errors = rb_intern("@errors");
10311110

10321111
/*
10331112
* Init components

test/openssl/test_ossl.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,23 @@ def test_memcmp_timing
6666
end if ENV["OSSL_TEST_ALL"] == "1"
6767

6868
def test_error_data
69-
# X509V3_EXT_nconf_nid() called from OpenSSL::X509::ExtensionFactory#create_ext is a function
70-
# that uses ERR_raise_data() to append additional information about the error.
69+
# X509V3_EXT_nconf_nid() called from
70+
# OpenSSL::X509::ExtensionFactory#create_ext is a function that uses
71+
# ERR_raise_data() to append additional information about the error.
7172
#
7273
# The generated message should look like:
7374
# "subjectAltName = IP:not.a.valid.ip.address: bad ip address (value=not.a.valid.ip.address)"
7475
# "subjectAltName = IP:not.a.valid.ip.address: error in extension (name=subjectAltName, value=IP:not.a.valid.ip.address)"
76+
#
77+
# The string inside parentheses is the ERR_TXT_STRING data, and is appended
78+
# by ossl_make_error(), so we check it here.
7579
ef = OpenSSL::X509::ExtensionFactory.new
76-
assert_raise_with_message(OpenSSL::X509::ExtensionError, /value=(IP:)?not.a.valid.ip.address\)/) {
80+
e = assert_raise(OpenSSL::X509::ExtensionError) {
7781
ef.create_ext("subjectAltName", "IP:not.a.valid.ip.address")
7882
}
83+
assert_match(/not.a.valid.ip.address\)\z/, e.message)
84+
assert_instance_of(Array, e.errors)
85+
assert_match(/\Aerror:.*not.a.valid.ip.address\)\z/, e.errors.last)
7986
end
8087
end
8188

0 commit comments

Comments
 (0)