Skip to content

Commit 3df1692

Browse files
committed
[Feature #18249] Implement ABI checking
Header file include/ruby/internal/abi.h contains RUBY_ABI_VERSION which is the ABI version. This value should be bumped whenever an ABI incompatible change is introduced. When loading dynamic libraries, Ruby will compare its own `ruby_abi_version` and the `ruby_abi_version` of the loaded library. If these two values don't match it will raise a `LoadError`. This feature can also be turned off by setting the environment variable `RUBY_RUBY_ABI_CHECK=0`. This feature will prevent cases where previously installed native gems fail in unexpected ways due to incompatibility of changes in header files. This will force the developer to recompile their gems to use the same header files as the built Ruby. In Ruby, the ABI version is exposed through `RbConfig::CONFIG["ruby_abi_version"]`.
1 parent 37d5890 commit 3df1692

File tree

8 files changed

+130
-0
lines changed

8 files changed

+130
-0
lines changed

dln.c

+17
Original file line numberDiff line numberDiff line change
@@ -426,12 +426,29 @@ dln_sym(void *handle, const char *symbol)
426426
}
427427
#endif
428428

429+
#if RUBY_DLN_CHECK_ABI
430+
static bool
431+
abi_check_enabled_p(void)
432+
{
433+
const char *val = getenv("RUBY_ABI_CHECK");
434+
return val == NULL || !(val[0] == '0' && val[1] == '\0');
435+
}
436+
#endif
437+
429438
void *
430439
dln_load(const char *file)
431440
{
432441
#if defined(_WIN32) || defined(USE_DLN_DLOPEN)
433442
void *handle = dln_open(file);
434443

444+
#if RUBY_DLN_CHECK_ABI
445+
unsigned long long (*abi_version_fct)(void) = (unsigned long long(*)(void))dln_sym(handle, "ruby_abi_version");
446+
unsigned long long binary_abi_version = (*abi_version_fct)();
447+
if (binary_abi_version != ruby_abi_version() && abi_check_enabled_p()) {
448+
dln_loaderror("ABI version of binary is incompatible with this Ruby. Try rebuilding this binary.");
449+
}
450+
#endif
451+
435452
char *init_fct_name;
436453
init_funcname(&init_fct_name, file);
437454
void (*init_fct)(void) = (void(*)(void))dln_sym(handle, init_fct_name);

ext/-test-/abi/abi.c

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#include <limits.h>
2+
3+
unsigned long long
4+
ruby_abi_version(void)
5+
{
6+
return ULONG_MAX;
7+
}
8+
9+
void
10+
Init_abi(void)
11+
{}

ext/-test-/abi/extconf.rb

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# frozen_string_literal: false
2+
require_relative "../auto_ext.rb"
3+
auto_ext(inc: true)

include/ruby/internal/abi.h

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#ifndef RUBY_ABI_H
2+
#define RUBY_ABI_H
3+
4+
/* This number represents Ruby's ABI version.
5+
*
6+
* In development Ruby, it should be bumped every time an ABI incompatible
7+
* change is introduced. This will force other developers to rebuild extension
8+
* gems.
9+
*
10+
* The following cases are considered as ABI incompatible changes:
11+
* - Changing any data structures.
12+
* - Changing macros or inline functions causing a change in behavior.
13+
* - Deprecating or removing function declarations.
14+
*
15+
* The following cases are NOT considered as ABI incompatible changes:
16+
* - Any changes that does not involve the header files in the `include`
17+
* directory.
18+
* - Adding macros, inline functions, or function declarations.
19+
* - Backwards compatible refactors.
20+
* - Editing comments.
21+
*
22+
* In released versions of Ruby, this number should not be changed since teeny
23+
* versions of Ruby should guarantee ABI compatibility.
24+
*/
25+
#define RUBY_ABI_VERSION 0
26+
27+
/* Windows does not support weak symbols so ruby_abi_version will not exist
28+
* in the shared library. */
29+
#if defined(HAVE_FUNC_WEAK) && !defined(_WIN32) && !defined(__MINGW32__)
30+
# define RUBY_DLN_CHECK_ABI 1
31+
#else
32+
# define RUBY_DLN_CHECK_ABI 0
33+
#endif
34+
35+
#if RUBY_DLN_CHECK_ABI
36+
37+
RUBY_FUNC_EXPORTED unsigned long long __attribute__((weak))
38+
ruby_abi_version(void)
39+
{
40+
return RUBY_ABI_VERSION;
41+
}
42+
43+
#endif
44+
45+
#endif

include/ruby/ruby.h

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <stdarg.h>
2424

2525
#include "defines.h"
26+
#include "ruby/internal/abi.h"
2627
#include "ruby/internal/anyargs.h"
2728
#include "ruby/internal/arithmetic.h"
2829
#include "ruby/internal/core.h"

test/-ext-/test_abi.rb

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
class TestABI < Test::Unit::TestCase
4+
def test_require_lib_with_incorrect_abi_on_dev_ruby
5+
omit "ABI is not checked" unless abi_checking_supported?
6+
7+
assert_separately [], <<~RUBY
8+
err = assert_raise(LoadError) { require "-test-/abi" }
9+
assert_match(/ABI version of binary is incompatible with this Ruby/, err.message)
10+
RUBY
11+
end
12+
13+
def test_disable_abi_check_using_environment_variable
14+
omit "ABI is not checked" unless abi_checking_supported?
15+
16+
assert_separately [{ "RUBY_ABI_CHECK" => "0" }], <<~RUBY
17+
assert_nothing_raised { require "-test-/abi" }
18+
RUBY
19+
end
20+
21+
def test_enable_abi_check_using_environment_variable
22+
omit "ABI is not checked" unless abi_checking_supported?
23+
24+
assert_separately [{ "RUBY_ABI_CHECK" => "1" }], <<~RUBY
25+
err = assert_raise(LoadError) { require "-test-/abi" }
26+
assert_match(/ABI version of binary is incompatible with this Ruby/, err.message)
27+
RUBY
28+
end
29+
30+
def test_require_lib_with_incorrect_abi_on_release_ruby
31+
omit "ABI is enforced" if abi_checking_supported?
32+
33+
assert_separately [], <<~RUBY
34+
assert_nothing_raised { require "-test-/abi" }
35+
RUBY
36+
end
37+
38+
private
39+
40+
def abi_checking_supported?
41+
!(RUBY_PLATFORM =~ /mswin|mingw/)
42+
end
43+
end

tool/mkconfig.rb

+9
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,15 @@ module RbConfig
229229
print " CONFIG[#{v.dump}] = #{(versions[v]||vars[v]).dump}\n"
230230
end
231231

232+
# Get the ABI version
233+
File.foreach(File.join(srcdir, "include/ruby/internal/abi.h")) do |l|
234+
m = /^\s*#\s*define\s+RUBY_ABI_VERSION\s+(\d+)/.match(l)
235+
if m
236+
print " CONFIG[\"ruby_abi_version\"] = \"#{m[1]}\"\n"
237+
break
238+
end
239+
end
240+
232241
dest = drive ? %r'= "(?!\$[\(\{])(?i:[a-z]:)' : %r'= "(?!\$[\(\{])'
233242
v_disabled = {}
234243
v_others.collect! do |x|

tool/transform_mjit_header.rb

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ module MJITHeader
4141
IGNORED_FUNCTIONS = [
4242
'rb_vm_search_method_slowpath', # This increases the time to compile when inlined. So we use it as external function.
4343
'rb_equal_opt', # Not used from VM and not compilable
44+
'ruby_abi_version',
4445
]
4546

4647
ALWAYS_INLINED_FUNCTIONS = [

0 commit comments

Comments
 (0)