1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

[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"]`.
This commit is contained in:
Peter Zhu 2022-02-18 10:59:45 -05:00
parent 37d5890e49
commit 3df16924b4
Notes: git 2022-02-22 23:55:57 +09:00
8 changed files with 130 additions and 0 deletions

17
dln.c
View file

@ -426,12 +426,29 @@ dln_sym(void *handle, const char *symbol)
}
#endif
#if RUBY_DLN_CHECK_ABI
static bool
abi_check_enabled_p(void)
{
const char *val = getenv("RUBY_ABI_CHECK");
return val == NULL || !(val[0] == '0' && val[1] == '\0');
}
#endif
void *
dln_load(const char *file)
{
#if defined(_WIN32) || defined(USE_DLN_DLOPEN)
void *handle = dln_open(file);
#if RUBY_DLN_CHECK_ABI
unsigned long long (*abi_version_fct)(void) = (unsigned long long(*)(void))dln_sym(handle, "ruby_abi_version");
unsigned long long binary_abi_version = (*abi_version_fct)();
if (binary_abi_version != ruby_abi_version() && abi_check_enabled_p()) {
dln_loaderror("ABI version of binary is incompatible with this Ruby. Try rebuilding this binary.");
}
#endif
char *init_fct_name;
init_funcname(&init_fct_name, file);
void (*init_fct)(void) = (void(*)(void))dln_sym(handle, init_fct_name);

11
ext/-test-/abi/abi.c Normal file
View file

@ -0,0 +1,11 @@
#include <limits.h>
unsigned long long
ruby_abi_version(void)
{
return ULONG_MAX;
}
void
Init_abi(void)
{}

View file

@ -0,0 +1,3 @@
# frozen_string_literal: false
require_relative "../auto_ext.rb"
auto_ext(inc: true)

View file

@ -0,0 +1,45 @@
#ifndef RUBY_ABI_H
#define RUBY_ABI_H
/* This number represents Ruby's ABI version.
*
* In development Ruby, it should be bumped every time an ABI incompatible
* change is introduced. This will force other developers to rebuild extension
* gems.
*
* The following cases are considered as ABI incompatible changes:
* - Changing any data structures.
* - Changing macros or inline functions causing a change in behavior.
* - Deprecating or removing function declarations.
*
* The following cases are NOT considered as ABI incompatible changes:
* - Any changes that does not involve the header files in the `include`
* directory.
* - Adding macros, inline functions, or function declarations.
* - Backwards compatible refactors.
* - Editing comments.
*
* In released versions of Ruby, this number should not be changed since teeny
* versions of Ruby should guarantee ABI compatibility.
*/
#define RUBY_ABI_VERSION 0
/* Windows does not support weak symbols so ruby_abi_version will not exist
* in the shared library. */
#if defined(HAVE_FUNC_WEAK) && !defined(_WIN32) && !defined(__MINGW32__)
# define RUBY_DLN_CHECK_ABI 1
#else
# define RUBY_DLN_CHECK_ABI 0
#endif
#if RUBY_DLN_CHECK_ABI
RUBY_FUNC_EXPORTED unsigned long long __attribute__((weak))
ruby_abi_version(void)
{
return RUBY_ABI_VERSION;
}
#endif
#endif

View file

@ -23,6 +23,7 @@
#include <stdarg.h>
#include "defines.h"
#include "ruby/internal/abi.h"
#include "ruby/internal/anyargs.h"
#include "ruby/internal/arithmetic.h"
#include "ruby/internal/core.h"

43
test/-ext-/test_abi.rb Normal file
View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
class TestABI < Test::Unit::TestCase
def test_require_lib_with_incorrect_abi_on_dev_ruby
omit "ABI is not checked" unless abi_checking_supported?
assert_separately [], <<~RUBY
err = assert_raise(LoadError) { require "-test-/abi" }
assert_match(/ABI version of binary is incompatible with this Ruby/, err.message)
RUBY
end
def test_disable_abi_check_using_environment_variable
omit "ABI is not checked" unless abi_checking_supported?
assert_separately [{ "RUBY_ABI_CHECK" => "0" }], <<~RUBY
assert_nothing_raised { require "-test-/abi" }
RUBY
end
def test_enable_abi_check_using_environment_variable
omit "ABI is not checked" unless abi_checking_supported?
assert_separately [{ "RUBY_ABI_CHECK" => "1" }], <<~RUBY
err = assert_raise(LoadError) { require "-test-/abi" }
assert_match(/ABI version of binary is incompatible with this Ruby/, err.message)
RUBY
end
def test_require_lib_with_incorrect_abi_on_release_ruby
omit "ABI is enforced" if abi_checking_supported?
assert_separately [], <<~RUBY
assert_nothing_raised { require "-test-/abi" }
RUBY
end
private
def abi_checking_supported?
!(RUBY_PLATFORM =~ /mswin|mingw/)
end
end

View file

@ -229,6 +229,15 @@ end
print " CONFIG[#{v.dump}] = #{(versions[v]||vars[v]).dump}\n"
end
# Get the ABI version
File.foreach(File.join(srcdir, "include/ruby/internal/abi.h")) do |l|
m = /^\s*#\s*define\s+RUBY_ABI_VERSION\s+(\d+)/.match(l)
if m
print " CONFIG[\"ruby_abi_version\"] = \"#{m[1]}\"\n"
break
end
end
dest = drive ? %r'= "(?!\$[\(\{])(?i:[a-z]:)' : %r'= "(?!\$[\(\{])'
v_disabled = {}
v_others.collect! do |x|

View file

@ -41,6 +41,7 @@ module MJITHeader
IGNORED_FUNCTIONS = [
'rb_vm_search_method_slowpath', # This increases the time to compile when inlined. So we use it as external function.
'rb_equal_opt', # Not used from VM and not compilable
'ruby_abi_version',
]
ALWAYS_INLINED_FUNCTIONS = [