commit f503ba86cba88381176a19e8fc2a2db9bba59afd
Author: Dmitry Goncharov <dgoncharov@users.sf.net>
Date:   Sat Jun 10 20:18:04 2023 -0400

    [SV 64295] Detect plugin binary compatibility at load time.
    
    At load time detect if a plugin is binary compatible and exit
    with an error in the case of a binary incompatible plugin.
    
    * src/gnumake.h: Add GMK_ABI_AGE equal zero and related documentation.
    * src/gnumake_abi_version.h: A facility to embed make abi version info to
    plugins.
    * src/load.c (load_object): At load time check the needed abi of the plugin
    and exit with an error message in the case of an incompatibility.
    * src/makeint.h: Add macro OSSNN.
    * doc/make.texi: Document the new feature.
    * Makefile.am: Add gnumake_abi_version.h to release tarballs.
    * tests/scripts/features/loadabi: Add new tests.
    * tests/scripts/features/load: Include gnumake_abi_version.h.
    Suppress unused parameter warnings.
    Port from srcdir to srcpath.
    * tests/scripts/features/loadapi: Ditto.

diff --git a/Makefile.am b/Makefile.am
index 23a48e47..8f1e0854 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -24,7 +24,7 @@ MAKE_HOST =	@MAKE_HOST@
 SUBDIRS =	lib po doc
 
 bin_PROGRAMS =	make
-include_HEADERS = src/gnumake.h
+include_HEADERS = src/gnumake.h src/gnumake_abi_version.h
 
 man_MANS =	doc/make.1
 
diff --git a/doc/make.texi b/doc/make.texi
index 57c13077..8e2f5b66 100644
--- a/doc/make.texi
+++ b/doc/make.texi
@@ -12166,6 +12166,17 @@ If not NULL, the major version number is placed here.
 If not NULL, the minor version number is placed here.
 @end table
 
+@subsubheading ABI Versioning
+
+Your plugin should include gnumake_abi_version.h once and only once in one of
+its source files.  Including gnumake_abi_version.h more than once will result
+in an error.
+
+gnumake_abi_version.h will embed the version information of the needed GNU
+@code{make} abi in your plugin.
+
+At load time GNU @code{make} will be able to check needed abi version and exit
+with and error message in the case of a binary incompatibility.
 
 @subsubheading Registering Functions
 @findex gmk_add_function
@@ -12297,6 +12308,7 @@ take a prefix as an argument.  First we can write the function in a file
 #include <errno.h>
 
 #include <gnumake.h>
+#include <gnumake_abi_version.h>
 
 int plugin_is_GPL_compatible;
 
diff --git a/src/gnumake.h b/src/gnumake.h
index b448a3c1..2ba0289f 100644
--- a/src/gnumake.h
+++ b/src/gnumake.h
@@ -18,7 +18,16 @@ this program.  If not, see <https://www.gnu.org/licenses/>.  */
 #ifndef _GNUMAKE_H_
 #define _GNUMAKE_H_
 
+/* GMK_ABI_VERSION is the current version of GNU Make abi.
+   GMK_ABI_AGE tells how many prior versions the current version is binary
+   compatible with.  E.g. GMK_ABI_VERSION equal 5 and GMK_ABI_AGE equal 3 means
+   "this is version 5 and it is binary compatible with versions 4, 3 and 2".
+   Whenever a binary compatible change is introduced, increment both version
+   and age.  Whenever a binary incompatible change is introduced, increment
+   version and set age to 0.  */
+
 #define GMK_ABI_VERSION 1
+#define GMK_ABI_AGE 0
 
 /* Specify the location of elements read from makefiles.  */
 typedef struct
diff --git a/src/gnumake_abi_version.h b/src/gnumake_abi_version.h
new file mode 100644
index 00000000..41708aa2
--- /dev/null
+++ b/src/gnumake_abi_version.h
@@ -0,0 +1,29 @@
+/* Versioning of GNU Make abi.
+Copyright (C) 2023 Free Software Foundation, Inc.
+This file is part of GNU Make.
+
+GNU Make is free software; you can redistribute it and/or modify it under the
+terms of the GNU General Public License as published by the Free Software
+Foundation; either version 3 of the License, or (at your option) any later
+version.
+
+GNU Make is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with
+this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#ifdef GNUMAKE_ABI_VERSION_INCLUDED
+#error gnumake_abi_version.h can only be included once.
+#endif
+#define GNUMAKE_ABI_VERSION_INCLUDED
+
+#include "gnumake.h"
+
+#define GMK_STR(s) #s
+#define GMK_STRINGIFY(s) GMK_STR(s)
+const char gmk_needed_abi_version__[] =
+    GMK_STRINGIFY (GMK_ABI_VERSION) "." GMK_STRINGIFY (GMK_ABI_AGE);
+#undef GMK_STRINGIFY
+#undef GMK_STR
diff --git a/src/load.c b/src/load.c
index ab55feaf..32c1f84f 100644
--- a/src/load.c
+++ b/src/load.c
@@ -60,6 +60,7 @@ load_object (const floc *flocp, int noerror, const char *ldname,
   void *dlp;
   struct load_list *new;
   setup_func_t symp;
+  const char *vsym;
 
   if (! global_dl)
     {
@@ -141,6 +142,28 @@ load_object (const floc *flocp, int noerror, const char *ldname,
     OS (fatal, flocp,
         _("loaded object %s is not declared to be GPL compatible"), ldname);
 
+  /* If the interface in gnumake.h ever changes in an incompatible fashion,
+   * exit with an error code, unless vsym is found.  */
+  vsym = (const char *) dlsym (dlp, "gmk_needed_abi_version__");
+  if (vsym)
+    {
+      int n, plver, plage;
+      DB (DB_VERBOSE, (_("Make abi %d.%d, %s needed abi %s\n"),
+                       GMK_ABI_VERSION, GMK_ABI_AGE, ldname, vsym));
+      n = sscanf (vsym, "%d.%d", &plver, &plage);
+      if (n != 2 ||
+          GMK_ABI_VERSION < plver ||
+         (GMK_ABI_VERSION == plver && GMK_ABI_AGE < plage) ||
+          GMK_ABI_VERSION - GMK_ABI_AGE > plver)
+        OSSNN (fatal, flocp,
+                _("%s needed abi %s is incompatible with make abi %d.%d"),
+                ldname, vsym, GMK_ABI_VERSION, GMK_ABI_AGE);
+    }
+  else
+      DB (DB_VERBOSE,
+          (_("Make abi %d.%d, %s has no needed abi\n"),
+          GMK_ABI_VERSION, GMK_ABI_AGE, ldname));
+
   symp = (setup_func_t) dlsym (dlp, setupnm);
   if (! symp)
     {
diff --git a/src/makeint.h b/src/makeint.h
index d55ccb6f..7b3ed2d0 100644
--- a/src/makeint.h
+++ b/src/makeint.h
@@ -572,6 +572,8 @@ void out_of_memory (void) NORETURN;
                                  (_f), (_s), (_n))
 #define ONS(_t,_a,_f,_n,_s)   _t((_a), INTSTR_LENGTH + strlen (_s), \
                                  (_f), (_n), (_s))
+#define OSSNN(_t,_a,_f,_s1,_s2,_n1,_n2)   _t((_a), strlen (_s1) + strlen (_s2) + INTSTR_LENGTH*2, \
+                                 (_f), (_s1), (_s2), (_n1), (_n2))
 
 enum variable_origin;
 struct variable;
diff --git a/tests/scripts/features/load b/tests/scripts/features/load
index 41333a54..2da3e83b 100644
--- a/tests/scripts/features/load
+++ b/tests/scripts/features/load
@@ -20,6 +20,7 @@ unlink(qw(testload.c testload.so));
 open(my $F, '> testload.c') or die "open: testload.c: $!\n";
 print $F <<'EOF' ;
 #include "gnumake.h"
+#include "gnumake_abi_version.h"
 
 char* getenv (const char*);
 
@@ -31,6 +32,7 @@ int explicit_setup (unsigned int, gmk_floc *);
 int
 testload_gmk_setup (unsigned int abi, gmk_floc *pos)
 {
+    (void) abi;
     (void)pos;
     gmk_eval ("TESTLOAD = implicit", 0);
     if (getenv("TESTAPI_KEEP"))
@@ -41,6 +43,7 @@ testload_gmk_setup (unsigned int abi, gmk_floc *pos)
 int
 explicit_setup (unsigned int abi, gmk_floc *pos)
 {
+    (void) abi;
     (void)pos;
     gmk_eval ("TESTLOAD = explicit", 0);
     if (getenv("TESTAPI_KEEP"))
@@ -52,7 +55,7 @@ close($F) or die "close: testload.c: $!\n";
 
 # Make sure we can compile
 
-my $cppflags = get_config('CPPFLAGS') . ($srcdir ? " -I$srcdir/src" : '');
+my $cppflags = get_config('CPPFLAGS') . ($srcpath ? " -I$srcpath/../src" : '');
 my $cflags = get_config('CFLAGS') . ' -fPIC';
 my $ldflags = get_config('LDFLAGS') . ' -shared';
 my $sobuild = "$cc $cppflags $cflags $ldflags -o testload.so testload.c";
diff --git a/tests/scripts/features/loadabi b/tests/scripts/features/loadabi
new file mode 100644
index 00000000..132070a0
--- /dev/null
+++ b/tests/scripts/features/loadabi
@@ -0,0 +1,127 @@
+#                                                                    -*-perl-*-
+$description = "Test versioning of make abi.";
+
+$details = ".";
+
+# Don't do anything if this system doesn't support "load"
+exists $FEATURES{'load'} or return -1;
+
+my $cc = get_config('CC');
+if (! $cc) {
+    $verbose and print "Skipping load test: no CC defined\n";
+    return -1;
+}
+
+sub generate_srcfile
+{
+    my ($include_version) = @_;
+
+    unlink('testabi.c');
+
+    my $contents = '
+#include <gnumake.h>
+int plugin_is_GPL_compatible;
+int
+testabi_gmk_setup (gmk_floc *pos)
+{
+    (void) pos;
+    return 1;
+}
+';
+    $include_version == 1 and $contents .= "#include <gnumake_abi_version.h>\n";
+    create_file('testabi.c', "$contents");
+}
+
+sub build_so
+{
+    my ($fakeinc) = @_;
+
+    unlink('testapi.so');
+    my $cppflags = "$fakeinc" . get_config('CPPFLAGS') . ($srcpath ? " -I$srcpath/../src" : '');
+    my $cflags = get_config('CFLAGS') . ' -fPIC';
+    my $ldflags = get_config('LDFLAGS') . ' -shared';
+    my $sobuild = "$cc $cppflags $cflags $ldflags -o testabi.so testabi.c";
+
+    my $clog = `$sobuild 2>&1`;
+    $verbose && printf "$sobuild\n$clog\n";
+    $? == 0 or die "cannot build testabi.so:\n$sobuild\n$clog";
+}
+
+
+# testabi.so does not contain needed abi.
+# Successful load.
+
+generate_srcfile(0);
+build_so('');
+
+run_make_test(q!
+load testabi.so
+all:
+!,
+'', "#MAKE#: Nothing to be done for 'all'.\n");
+
+# testabi.so needs the same abi as make provides.
+# Successful load.
+
+generate_srcfile(1);
+build_so('');
+
+run_make_test(q!
+load testabi.so
+all:
+!,
+'', "#MAKE#: Nothing to be done for 'all'.\n");
+
+# To test how make detects incompatible versions create a fake
+# gnumake_abi_version.h with various values of version and age in a temporary
+# directory and have this temporary directory preceed the real directory in
+# compiler header search path.
+
+my $verfile = $srcpath ? "$srcpath/../src/gnumake.h" : 'gnumake.h';
+open my $verinfo, $verfile or die "cannot open $verfile: $!";
+
+my $gmk_version = -1;
+my $gmk_age = -1;
+while(my $line = <$verinfo>)
+{
+    if ($line =~ /GMK_ABI_VERSION\s+(\d+)/) {
+        $gmk_version = $1;
+    }
+    if ($line =~ /GMK_ABI_AGE\s+(\d+)/) {
+        $gmk_age = $1;
+    }
+}
+
+$gmk_version == -1 and die "cannot read gmk_abi_version from gnumake.h";
+$gmk_age == -1 and die "cannot read gmk_abi_age from gnumake.h";
+
+rmdir('fake_inc');
+mkdir('fake_inc', 0777);
+my @vers = ($gmk_version - 1, $gmk_version, $gmk_version + 1);
+for my $v (@vers) {
+    my $age = $gmk_age;
+    # If version is the same as make's version, increment age to cause an
+    # incompatibility.
+    $v == $gmk_version and $age = $gmk_age + 1;
+
+    create_file('fake_inc/gnumake_abi_version.h',
+                "const char gmk_needed_abi_version__[] = \"$v.$age\";\n");
+    build_so('-Ifake_inc');
+
+    run_make_test(q!
+load testabi.so
+all:
+!,
+    '',
+    "#MAKEFILE#:2: *** testabi.so needed abi $v.$age is incompatible with make abi $gmk_version.$gmk_age.  Stop.\n",
+    512);
+}
+
+#TODO: Once make's abi age exceeds zero, test that the new version minus
+# age is compatible with the prior version.
+unlink(qw(testabi.c testabi.so)) unless $keep;
+unlink('fake_inc/gnumake_abi_version.h') unless $keep;
+rmdir('fake_inc') unless $keep;
+
+# This tells the test driver that the perl test script executed properly.
+1;
diff --git a/tests/scripts/features/loadapi b/tests/scripts/features/loadapi
index 50e5d05f..23979acd 100644
--- a/tests/scripts/features/loadapi
+++ b/tests/scripts/features/loadapi
@@ -23,6 +23,7 @@ print $F <<'EOF' ;
 #include <stdio.h>
 
 #include "gnumake.h"
+#include "gnumake_abi_version.h"
 
 char *getenv (const char*);
 
@@ -55,6 +56,8 @@ static char *
 func_test (const char *funcname, unsigned int argc, char **argv)
 {
     char *mem;
+    (void) argc;
+    (void) argv;
 
     if (strcmp (funcname, "test-expand") == 0)
         return test_expand (argv[0]);
@@ -74,6 +77,7 @@ int
 testapi_gmk_setup (unsigned int abi, const gmk_floc *floc)
 {
     const char *verbose = getenv ("TESTAPI_VERBOSE");
+    (void) abi;
 
     gmk_add_function ("test-expand", func_test, 1, 1, GMK_FUNC_DEFAULT);
     gmk_add_function ("test-noexpand", func_test, 1, 1, GMK_FUNC_NOEXPAND);
@@ -127,7 +131,7 @@ close($F) or die "close: testapi.c: $!\n";
 my $cflags = get_config('CFLAGS');
 my $cppflags = get_config('CPPFLAGS');
 my $ldflags = get_config('LDFLAGS');
-my $sobuild = "$cc ".($srcdir? "-I$srcdir/src":'')." $cppflags $cflags -shared -fPIC $ldflags -o testapi.so testapi.c";
+my $sobuild = "$cc ".($srcpath? "-I$srcpath/../src":'')." $cppflags $cflags -shared -fPIC $ldflags -o testapi.so testapi.c";
 
 my $clog = `$sobuild 2>&1`;
 if ($? != 0) {
