From 559bdc0ac8b1698eb0da0651537c29d542af8261 Mon Sep 17 00:00:00 2001
From: Simon McVittie <smcv@collabora.com>
Date: Wed, 18 Nov 2020 12:56:15 +0000
Subject: [PATCH] system-info: Detect whether the expected runtime linker is
 present

On operating systems with unusual path layouts, such as Exherbo and
NixOS, the de facto standard path for the runtime linker is not
necessarily present for all architectures.

(Of course, if the x86_64 runtime linker is not present, we won't
normally be able to run steam-runtime-system-info either, but at least
users of such operating systems can compile it from source code if they
need to provide diagnostics.)

Signed-off-by: Simon McVittie <smcv@collabora.com>
---
 bin/system-info.c                           |  29 +++
 bin/system-info.md                          |  26 +++
 steam-runtime-tools/architecture-internal.h |   8 +
 steam-runtime-tools/architecture.c          |  61 ++++++
 steam-runtime-tools/architecture.h          |  31 +++
 steam-runtime-tools/system-info.c           | 227 ++++++++++++++++++++
 steam-runtime-tools/system-info.h           |   6 +
 tests/architecture.c                        |   7 +
 tests/generate-sysroots.py                  |   7 +
 tests/json-report/full-good-report.json     |  10 +
 tests/json-report/partial-report-2.json     |   4 +
 tests/json-report/partial-report.json       |   3 +
 tests/system-info.c                         | 226 +++++++++++++++++++
 13 files changed, 645 insertions(+)

diff --git a/bin/system-info.c b/bin/system-info.c
index 0a4e8510f..346c0fde7 100644
--- a/bin/system-info.c
+++ b/bin/system-info.c
@@ -1079,6 +1079,7 @@ main (int argc,
       GList *va_api_list = NULL;
       GList *vdpau_list = NULL;
       GList *glx_list = NULL;
+      const char *ld_so;
 
       json_builder_set_member_name (builder, multiarch_tuples[i]);
       json_builder_begin_object (builder);
@@ -1086,6 +1087,34 @@ main (int argc,
       can_run = srt_system_info_can_run (info, multiarch_tuples[i]);
       json_builder_add_boolean_value (builder, can_run);
 
+      ld_so = srt_architecture_get_expected_runtime_linker (multiarch_tuples[i]);
+
+      if (ld_so != NULL)
+        {
+          json_builder_set_member_name (builder, "runtime-linker");
+          json_builder_begin_object (builder);
+            {
+              g_autoptr(GError) local_error = NULL;
+              g_autofree gchar *real = NULL;
+
+              json_builder_set_member_name (builder, "path");
+              json_builder_add_string_value (builder, ld_so);
+
+              if (srt_system_info_check_runtime_linker (info,
+                                                        multiarch_tuples[i],
+                                                        &real, &local_error))
+                {
+                  json_builder_set_member_name (builder, "resolved");
+                  json_builder_add_string_value (builder, real);
+                }
+              else
+                {
+                  _srt_json_builder_add_error_members (builder, local_error);
+                }
+            }
+          json_builder_end_object (builder);
+        }
+
       if (can_run)
         {
           json_builder_set_member_name (builder, "library-issues-summary");
diff --git a/bin/system-info.md b/bin/system-info.md
index 327abf05a..94e04b7fa 100644
--- a/bin/system-info.md
+++ b/bin/system-info.md
@@ -354,6 +354,32 @@ keys:
     :   A boolean value indicating whether a simple executable for this
         architecture was run successfully.
 
+    **runtime-linker**
+    :   If the multiarch tuple is a known one, an object with the
+        following keys and values describing the runtime linker **ld.so**(8):
+
+        **path**
+        :   The path required to run Steam binaries on this architecture,
+            for example `/lib64/ld-linux-x86-64.so.2` on x86_64
+
+        **resolved**
+        :   The result of resolving symbolic links in **path** if possible,
+            for example `/lib/x86_64-linux-gnu/ld-2.28.so` on
+            Debian 10 x86_64
+
+        If the runtime linker is detected to be missing or unusable,
+        details of the error appear here:
+
+        **error-domain**
+        :   A machine-readable string indicating a category of errors.
+
+        **error-code**
+        :   A small integer indicating a specific error. Its meaning depends
+            on the **error-domain**.
+
+        **error**
+        :   A human-readable error message.
+
     **library-issues-summary**
     :   A summary of issues found when loading the expected libraries,
         as an array of strings. If empty, no problems were found.
diff --git a/steam-runtime-tools/architecture-internal.h b/steam-runtime-tools/architecture-internal.h
index 8461ab924..e1fb688cb 100644
--- a/steam-runtime-tools/architecture-internal.h
+++ b/steam-runtime-tools/architecture-internal.h
@@ -29,6 +29,14 @@
 #include <glib.h>
 #include <json-glib/json-glib.h>
 
+typedef struct
+{
+  const char *multiarch_tuple;
+  const char *interoperable_runtime_linker;
+} SrtKnownArchitecture;
+
+G_GNUC_INTERNAL const SrtKnownArchitecture *_srt_architecture_get_known (void);
+
 G_GNUC_INTERNAL gboolean _srt_architecture_can_run (gchar **envp,
                                                     const char *helpers_path,
                                                     const char *multiarch);
diff --git a/steam-runtime-tools/architecture.c b/steam-runtime-tools/architecture.c
index 5f278b147..e4a00292f 100644
--- a/steam-runtime-tools/architecture.c
+++ b/steam-runtime-tools/architecture.c
@@ -44,6 +44,38 @@
  * operating system.
  */
 
+G_DEFINE_QUARK (srt-architecture-error-quark, srt_architecture_error)
+
+static const SrtKnownArchitecture known_architectures[] =
+{
+    {
+      .multiarch_tuple = SRT_ABI_X86_64,
+      .interoperable_runtime_linker = "/lib64/ld-linux-x86-64.so.2",
+    },
+
+    {
+      .multiarch_tuple = SRT_ABI_I386,
+      .interoperable_runtime_linker = "/lib/ld-linux.so.2",
+    },
+
+    {
+      .multiarch_tuple = "x86_64-linux-gnux32",
+      .interoperable_runtime_linker = "/libx32/ld-linux-x32.so.2",
+    },
+
+    { NULL }
+};
+
+/*
+ * Returns: A table of known architectures, terminated by one
+ *  with @multiarch_tuple set to %NULL.
+ */
+const SrtKnownArchitecture *
+_srt_architecture_get_known (void)
+{
+  return &known_architectures[0];
+}
+
 gboolean
 _srt_architecture_can_run (gchar **envp,
                            const char *helpers_path,
@@ -164,6 +196,35 @@ srt_architecture_can_run_x86_64 (void)
                                     NULL, SRT_ABI_X86_64);
 }
 
+/**
+ * srt_architecture_get_expected_runtime_linker:
+ * @multiarch_tuple: A multiarch tuple defining an ABI, as printed
+ *  by `gcc -print-multiarch` in the Steam Runtime
+ *
+ * Return the interoperable path to the runtime linker `ld.so(8)`,
+ * if known. For example, for x86_64, this returns
+ * `/lib64/ld-linux-x86-64.so.2`.
+ *
+ * Returns: (type filename) (transfer none): An absolute path,
+ *  or %NULL if not known
+ */
+const char *
+srt_architecture_get_expected_runtime_linker (const char *multiarch_tuple)
+{
+  gsize i;
+
+  g_return_val_if_fail (multiarch_tuple != NULL, NULL);
+
+  for (i = 0; known_architectures[i].multiarch_tuple != NULL; i++)
+    {
+      if (strcmp (multiarch_tuple,
+                  known_architectures[i].multiarch_tuple) == 0)
+        return known_architectures[i].interoperable_runtime_linker;
+    }
+
+  return NULL;
+}
+
 /**
  * _srt_architecture_can_run_from_report:
  * @json_obj: (not nullable): A JSON Object used to search for "can-run"
diff --git a/steam-runtime-tools/architecture.h b/steam-runtime-tools/architecture.h
index 178d3567f..5e6bfa87a 100644
--- a/steam-runtime-tools/architecture.h
+++ b/steam-runtime-tools/architecture.h
@@ -53,3 +53,34 @@ _SRT_PUBLIC
 gboolean srt_architecture_can_run_i386 (void);
 _SRT_PUBLIC
 gboolean srt_architecture_can_run_x86_64 (void);
+
+/**
+ * SrtArchitectureError:
+ * @SRT_ARCHITECTURE_ERROR_FAILED: Generic error
+ * @SRT_ARCHITECTURE_ERROR_INTERNAL_ERROR: An internal error occurred
+ * @SRT_ARCHITECTURE_ERROR_NO_INFORMATION: It is unknown whether the
+ *  given architecture, ld.so, etc. is available or not, for example
+ *  because the interoperable ld.so path for the architecture is unknown,
+ *  or because SrtSystemInfo is reading a JSON report that does not
+ *  contain this information
+ *
+ * Errors raised when checking facts about an architecture.
+ *
+ * Errors in the #GIOErrorEnum domain can also be raised: for example,
+ * if srt_system_info_check_runtime_linker() raises %G_IO_ERROR_NOT_FOUND,
+ * it means the interoperable path for ld.so does not exist.
+ */
+typedef enum
+{
+  SRT_ARCHITECTURE_ERROR_FAILED = 0,
+  SRT_ARCHITECTURE_ERROR_INTERNAL_ERROR,
+  SRT_ARCHITECTURE_ERROR_NO_INFORMATION,
+} SrtArchitectureError;
+
+_SRT_PUBLIC
+GQuark srt_architecture_error_quark (void);
+
+#define SRT_ARCHITECTURE_ERROR (srt_architecture_error_quark ())
+
+_SRT_PUBLIC
+const char *srt_architecture_get_expected_runtime_linker (const char *multiarch_tuple);
diff --git a/steam-runtime-tools/system-info.c b/steam-runtime-tools/system-info.c
index c966f37b5..a90d02944 100644
--- a/steam-runtime-tools/system-info.c
+++ b/steam-runtime-tools/system-info.c
@@ -251,6 +251,9 @@ typedef struct
 typedef struct
 {
   GQuark multiarch_tuple;
+  const SrtKnownArchitecture *known_architecture;
+  gchar *runtime_linker_resolved;
+  GError *runtime_linker_error;
   Tristate can_run;
   GHashTable *cached_results;
   SrtLibraryIssues cached_combined_issues;
@@ -267,6 +270,7 @@ static Abi *
 ensure_abi_unless_immutable (SrtSystemInfo *self,
                              const char *multiarch_tuple)
 {
+  const SrtKnownArchitecture *iter;
   GQuark quark;
   guint i;
   Abi *abi = NULL;
@@ -286,6 +290,18 @@ ensure_abi_unless_immutable (SrtSystemInfo *self,
 
   abi = g_slice_new0 (Abi);
   abi->multiarch_tuple = quark;
+
+  for (iter = _srt_architecture_get_known ();
+       iter->multiarch_tuple != NULL;
+       iter++)
+    {
+      if (strcmp (iter->multiarch_tuple, multiarch_tuple) == 0)
+        {
+          abi->known_architecture = iter;
+          break;
+        }
+    }
+
   abi->can_run = TRI_MAYBE;
   abi->cached_results = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
   abi->cached_combined_issues = SRT_LIBRARY_ISSUES_NONE;
@@ -319,6 +335,8 @@ abi_free (gpointer self)
   for (i = 0; i < G_N_ELEMENTS (abi->graphics_modules); i++)
     g_list_free_full (abi->graphics_modules[i].modules, g_object_unref);
 
+  g_free (abi->runtime_linker_resolved);
+  g_clear_error (&abi->runtime_linker_error);
   g_slice_free (Abi, self);
 }
 
@@ -595,6 +613,79 @@ static gchar ** _srt_system_info_get_pinned_libs_from_report (JsonObject *json_o
                                                               const gchar *which,
                                                               gchar ***messages);
 
+static void
+get_runtime_linker_from_report (SrtSystemInfo *self,
+                                Abi *abi,
+                                JsonObject *json_arch_obj)
+{
+  JsonObject *runtime_linker_obj;
+  const char *path;
+  const char *resolved;
+
+  g_return_if_fail (abi->runtime_linker_resolved == NULL);
+  g_return_if_fail (abi->runtime_linker_error == NULL);
+
+  if (abi->known_architecture == NULL)
+    return;
+
+  if (abi->known_architecture->interoperable_runtime_linker == NULL)
+    return;
+
+  if (!json_object_has_member (json_arch_obj, "runtime-linker"))
+    return;
+
+  runtime_linker_obj = json_object_get_object_member (json_arch_obj, "runtime-linker");
+
+  path = json_object_get_string_member_with_default (runtime_linker_obj,
+                                                     "path", NULL);
+  resolved = json_object_get_string_member_with_default (runtime_linker_obj,
+                                                         "resolved", NULL);
+
+  if (json_object_has_member (runtime_linker_obj, "error"))
+    {
+      GQuark error_domain;
+      int error_code;
+      const char *error_message;
+
+      error_domain = g_quark_from_string (json_object_get_string_member_with_default (runtime_linker_obj, "error-domain", NULL));
+      error_code = json_object_get_int_member_with_default (runtime_linker_obj,
+                                                            "error-code",
+                                                            -1);
+      error_message = json_object_get_string_member_with_default (runtime_linker_obj,
+                                                                  "error",
+                                                                  NULL);
+
+      if (error_domain == 0)
+        {
+          error_domain = SRT_ARCHITECTURE_ERROR;
+          error_code = SRT_ARCHITECTURE_ERROR_INTERNAL_ERROR;
+        }
+
+      if (error_message == NULL)
+        error_message = "Unknown error";
+
+      g_set_error_literal (&abi->runtime_linker_error, error_domain, error_code,
+                           error_message);
+    }
+  else if (path == NULL)
+    {
+      /* no information */
+      return;
+    }
+  else if (g_strcmp0 (path, abi->known_architecture->interoperable_runtime_linker) != 0)
+    {
+      g_set_error (&abi->runtime_linker_error, SRT_ARCHITECTURE_ERROR,
+                   SRT_ARCHITECTURE_ERROR_INTERNAL_ERROR,
+                   "Expected \"%s\" in report, but got \"%s\"",
+                   abi->known_architecture->interoperable_runtime_linker,
+                   path);
+    }
+  else
+    {
+      abi->runtime_linker_resolved = g_strdup (resolved);
+    }
+}
+
 /**
  * srt_system_info_new_from_json:
  * @path: (not nullable) (type filename): Path to a JSON report
@@ -706,6 +797,8 @@ srt_system_info_new_from_json (const char *path,
           abi->libraries_cache_available = TRUE;
           abi->cached_combined_issues = _srt_library_get_issues_from_report (json_arch_obj);
 
+          get_runtime_linker_from_report (info, abi, json_arch_obj);
+
           _srt_graphics_get_from_report (json_arch_obj,
                                          l->data,
                                          &abi->cached_graphics_results);
@@ -3833,3 +3926,137 @@ srt_system_info_get_xdg_portal_issues (SrtSystemInfo *self,
 
   return srt_xdg_portal_get_issues (self->xdg_portal_data);
 }
+
+static void
+ensure_runtime_linker (SrtSystemInfo *self,
+                       Abi *abi)
+{
+  g_autofree gchar *tmp = NULL;
+  int fd;
+
+  if (abi->runtime_linker_resolved != NULL)
+    return;
+
+  if (abi->runtime_linker_error != NULL)
+    return;
+
+  if (abi->known_architecture == NULL)
+    return;
+
+  if (abi->known_architecture->interoperable_runtime_linker == NULL)
+    {
+      g_set_error (&abi->runtime_linker_error, SRT_ARCHITECTURE_ERROR,
+                   SRT_ARCHITECTURE_ERROR_NO_INFORMATION,
+                   "Interoperable runtime_linker for \"%s\" not known",
+                   abi->known_architecture->multiarch_tuple);
+      return;
+    }
+
+  if (self->sysroot_fd < 0)
+    {
+      g_set_error (&abi->runtime_linker_error, SRT_ARCHITECTURE_ERROR,
+                   SRT_ARCHITECTURE_ERROR_INTERNAL_ERROR,
+                   "Unable to open sysroot");
+      return;
+    }
+
+  fd = _srt_resolve_in_sysroot (self->sysroot_fd,
+                                abi->known_architecture->interoperable_runtime_linker,
+                                SRT_RESOLVE_FLAGS_READABLE,
+                                &tmp,
+                                &abi->runtime_linker_error);
+
+  if (fd >= 0)
+    {
+      abi->runtime_linker_resolved = g_strconcat ("/", tmp, NULL);
+      close (fd);
+    }
+}
+
+/**
+ * srt_system_info_check_runtime_linker:
+ * @self: The #SrtSystemInfo object
+ * @multiarch_tuple: A multiarch tuple defining an ABI, as printed
+ *  by `gcc -print-multiarch` in the Steam Runtime
+ * @resolved: (out) (type filename) (transfer full) (optional): Used
+ *  to return the path to the runtime linker after resolving all
+ *  symbolic links. Free with g_free(); may be %NULL to ignore.
+ * @error: Used to raise an error on failure.
+ *
+ * Check whether the runtime linker `ld.so(8)` for the ABI described
+ * by @multiarch_tuple, as returned by
+ * srt_architecture_get_expected_runtime_linker(), is available.
+ *
+ * If `ld.so` is unavailable, return %FALSE with @error set.
+ *
+ * If not enough information is available to determine whether `ld.so`
+ * is available, raise %SRT_ARCHITECTURE_ERROR_NO_INFORMATION.
+ *
+ * Returns: %TRUE if `ld.so(8)` is available at the interoperable path
+ *  for the given architecture
+ */
+gboolean
+srt_system_info_check_runtime_linker (SrtSystemInfo *self,
+                                      const char *multiarch_tuple,
+                                      gchar **resolved,
+                                      GError **error)
+{
+  Abi *abi;
+
+  g_return_val_if_fail (SRT_IS_SYSTEM_INFO (self), FALSE);
+  g_return_val_if_fail (multiarch_tuple != NULL, FALSE);
+
+  abi = ensure_abi_unless_immutable (self, multiarch_tuple);
+
+  if (abi == NULL)
+    {
+      g_set_error (error, SRT_ARCHITECTURE_ERROR,
+                   SRT_ARCHITECTURE_ERROR_NO_INFORMATION,
+                   "ABI \"%s\" not included in report",
+                   multiarch_tuple);
+      return FALSE;
+    }
+
+  if (!self->immutable_values)
+    ensure_runtime_linker (self, abi);
+
+  if (abi->known_architecture == NULL)
+    {
+      g_set_error (error, SRT_ARCHITECTURE_ERROR,
+                   SRT_ARCHITECTURE_ERROR_NO_INFORMATION,
+                   "Interoperable runtime linker for \"%s\" not known",
+                   multiarch_tuple);
+      return FALSE;
+    }
+  else if (abi->runtime_linker_resolved != NULL)
+    {
+      if (resolved != NULL)
+        *resolved = g_strdup (abi->runtime_linker_resolved);
+
+      return TRUE;
+    }
+  else if (abi->runtime_linker_error != NULL)
+    {
+      if (error != NULL)
+        *error = g_error_copy (abi->runtime_linker_error);
+
+      return FALSE;
+    }
+  else if (self->immutable_values)
+    {
+      g_set_error (error, SRT_ARCHITECTURE_ERROR,
+                   SRT_ARCHITECTURE_ERROR_NO_INFORMATION,
+                   "Runtime linker for \"%s\" not included in report",
+                   multiarch_tuple);
+      return FALSE;
+    }
+  else
+    {
+      /* We shouldn't be able to get here */
+      g_set_error (error, SRT_ARCHITECTURE_ERROR,
+                   SRT_ARCHITECTURE_ERROR_INTERNAL_ERROR,
+                   "Runtime linker for \"%s\" not checked",
+                   multiarch_tuple);
+      g_return_val_if_reached (FALSE);
+    }
+}
diff --git a/steam-runtime-tools/system-info.h b/steam-runtime-tools/system-info.h
index 3ce01a9b3..63f1b8008 100644
--- a/steam-runtime-tools/system-info.h
+++ b/steam-runtime-tools/system-info.h
@@ -275,6 +275,12 @@ _SRT_PUBLIC
 SrtXdgPortalIssues srt_system_info_get_xdg_portal_issues (SrtSystemInfo *self,
                                                           gchar **messages);
 
+_SRT_PUBLIC
+gboolean srt_system_info_check_runtime_linker (SrtSystemInfo *self,
+                                               const char *multiarch_tuple,
+                                               gchar **real_path,
+                                               GError **error);
+
 #ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (SrtSystemInfo, g_object_unref)
 #endif
diff --git a/tests/architecture.c b/tests/architecture.c
index 595ac8dd2..6aa76b490 100644
--- a/tests/architecture.c
+++ b/tests/architecture.c
@@ -67,6 +67,13 @@ test_architecture (Fixture *f,
 #if defined(__i386__)
   g_assert_true (srt_architecture_can_run_i386 ());
 #endif
+
+  g_assert_cmpstr (srt_architecture_get_expected_runtime_linker (SRT_ABI_X86_64),
+                   ==, "/lib64/ld-linux-x86-64.so.2");
+  g_assert_cmpstr (srt_architecture_get_expected_runtime_linker (SRT_ABI_I386),
+                   ==, "/lib/ld-linux.so.2");
+  g_assert_cmpstr (srt_architecture_get_expected_runtime_linker ("potato-glados"),
+                   ==, NULL);
 }
 
 int
diff --git a/tests/generate-sysroots.py b/tests/generate-sysroots.py
index c6937dcc1..e143ab620 100755
--- a/tests/generate-sysroots.py
+++ b/tests/generate-sysroots.py
@@ -107,6 +107,7 @@ ubuntu16/usr/lib/mock-ubuntu-64-bit/vdpau
     os.makedirs(name, mode=0o755, exist_ok=True)
 
 for name in '''
+debian10/usr/lib/i386-linux-gnu/ld.so
 debian10/usr/lib/i386-linux-gnu/dri/i965_dri.so
 debian10/usr/lib/i386-linux-gnu/dri/r300_dri.so
 debian10/usr/lib/i386-linux-gnu/dri/r600_drv_video.so
@@ -116,6 +117,7 @@ debian10/usr/lib/i386-linux-gnu/libva.so.2
 debian10/usr/lib/i386-linux-gnu/libvdpau.so.1
 debian10/usr/lib/i386-linux-gnu/vdpau/libvdpau_r600.so
 debian10/usr/lib/i386-linux-gnu/vdpau/libvdpau_radeonsi.so.1.0.0
+debian10/usr/lib/x86_64-linux-gnu/ld.so
 debian10/usr/lib/x86_64-linux-gnu/dri/i965_dri.so
 debian10/usr/lib/x86_64-linux-gnu/dri/r600_dri.so
 debian10/usr/lib/x86_64-linux-gnu/dri/r600_drv_video.so
@@ -188,6 +190,7 @@ steamrt/overrides/lib/i386-linux-gnu/libGLX_nvidia.so.0
 steamrt/run/pressure-vessel/.exists
 steamrt-overrides-issues/overrides/bin/.keep
 steamrt-overrides-issues/overrides/lib/i386-linux-gnu/.keep
+ubuntu16/lib64/ld-linux-x86-64.so.2
 ubuntu16/usr/lib/dri/radeonsi_dri.so
 ubuntu16/usr/lib/mock-ubuntu-64-bit/dri/i965_dri.so
 ubuntu16/usr/lib/mock-ubuntu-64-bit/dri/radeon_dri.so
@@ -201,6 +204,10 @@ ubuntu16/usr/lib/mock-ubuntu-64-bit/vdpau/libvdpau_radeonsi.so.1.0.0
     open(name, 'w').close()
 
 for name, target in {
+    'debian10/lib/ld-linux.so.2':
+        '/usr/lib/i386-linux-gnu/ld.so',
+    'debian10/lib64/ld-linux-x86-64.so.2':
+        '../usr/lib/x86_64-linux-gnu/ld.so',
     'debian10/usr/lib/i386-linux-gnu/vdpau/libvdpau_radeonsi.so':
         'libvdpau_radeonsi.so.1.0.0',
     'debian10/usr/lib/i386-linux-gnu/vdpau/libvdpau_radeonsi.so.1':
diff --git a/tests/json-report/full-good-report.json b/tests/json-report/full-good-report.json
index 371ac3c4b..d926f4b07 100644
--- a/tests/json-report/full-good-report.json
+++ b/tests/json-report/full-good-report.json
@@ -53,6 +53,12 @@
   "architectures" : {
     "i386-linux-gnu" : {
       "can-run" : false,
+      "runtime-linker": {
+        "path" : "/lib/ld-linux.so.2",
+        "error-domain" : "g-io-error-quark",
+        "error-code" : 1,
+        "error" : "No such file or directory"
+      },
       "graphics-details" : {
         "x11/vulkan" : {
           "messages" : "ERROR: [Loader Message] Code 0 : /usr/lib/amdvlk64.so: wrong ELF class: ELFCLASS64\nCannot create Vulkan instance.\n",
@@ -120,6 +126,10 @@
     },
     "x86_64-linux-gnu" : {
       "can-run" : true,
+      "runtime-linker": {
+        "path" : "/lib64/ld-linux-x86-64.so.2",
+        "resolved" : "/lib/x86_64-linux-gnu/ld-2.31.so"
+      },
       "library-issues-summary" : [
       ],
       "graphics-details" : {
diff --git a/tests/json-report/partial-report-2.json b/tests/json-report/partial-report-2.json
index b882cd97c..20a77f242 100644
--- a/tests/json-report/partial-report-2.json
+++ b/tests/json-report/partial-report-2.json
@@ -24,6 +24,10 @@
   "architectures" : {
     "x86_64-linux-gnu" : {
       "can-run" : true,
+      "runtime-linker": {
+        "path": "/foobar",
+        "resolved": "/foo/bar"
+      },
       "graphics-details" : {
         "x11/vulkan" : {
         }
diff --git a/tests/json-report/partial-report.json b/tests/json-report/partial-report.json
index e769bee39..d843f8237 100644
--- a/tests/json-report/partial-report.json
+++ b/tests/json-report/partial-report.json
@@ -13,6 +13,9 @@
   "architectures" : {
     "i386-linux-gnu" : {
       "can-run" : false,
+      "runtime-linker": {
+        "error": "We just don't know"
+      },
       "graphics-details" : {
         "x11/vdpau" : {
           "renderer" : "G3DVL VDPAU Driver Shared Library version 1.0\n",
diff --git a/tests/system-info.c b/tests/system-info.c
index 68fc9cf9d..63597d177 100644
--- a/tests/system-info.c
+++ b/tests/system-info.c
@@ -2408,10 +2408,20 @@ typedef struct
   gboolean is_available;
 } GraphicsTest;
 
+typedef struct
+{
+  const char *path;
+  const char *resolved;
+  const char *error_domain;
+  int error_code;
+  const char *error_message;
+} RuntimeLinkerTest;
+
 typedef struct
 {
   gboolean can_run;
   SrtLibraryIssues issues;
+  RuntimeLinkerTest runtime_linker;
   DriverTest dri_drivers[5];
   DriverTest va_api_drivers[5];
   DriverTest vdpau_drivers[5];
@@ -2556,6 +2566,13 @@ static const JsonTest json_test[] =
     {
       {
         .can_run = FALSE,
+        .runtime_linker =
+        {
+          .path = "/lib/ld-linux.so.2",
+          .error_domain = "g-io-error-quark",
+          .error_code = G_IO_ERROR_NOT_FOUND,
+          .error_message = "No such file or directory",
+        },
         .issues = SRT_LIBRARY_ISSUES_UNKNOWN,
         .graphics =
         {
@@ -2641,6 +2658,11 @@ static const JsonTest json_test[] =
 
       {
         .can_run = TRUE,
+        .runtime_linker =
+        {
+          .path = "/lib64/ld-linux-x86-64.so.2",
+          .resolved = "/lib/x86_64-linux-gnu/ld-2.31.so",
+        },
         .graphics =
         {
           {
@@ -2874,6 +2896,15 @@ static const JsonTest json_test[] =
     {
       {
         .can_run = FALSE,
+        .runtime_linker =
+        {
+          .path = "/lib/ld-linux.so.2",
+          /* Error domain and code are missing from the report, so we
+           * make something up */
+          .error_domain = "srt-architecture-error-quark",
+          .error_code = SRT_ARCHITECTURE_ERROR_INTERNAL_ERROR,
+          .error_message = "We just don't know",
+        },
         .issues = SRT_LIBRARY_ISSUES_UNKNOWN,
         .graphics =
         {
@@ -2888,6 +2919,13 @@ static const JsonTest json_test[] =
       {
         .can_run = TRUE,
         .issues = SRT_LIBRARY_ISSUES_UNKNOWN,
+        .runtime_linker =
+        {
+          .path = "/lib64/ld-linux-x86-64.so.2",
+          .error_domain = "srt-architecture-error-quark",
+          .error_code = SRT_ARCHITECTURE_ERROR_NO_INFORMATION,
+          .error_message = "Runtime linker for \"x86_64-linux-gnu\" not included in report",
+        },
       },
     },
     .locale_issues = SRT_LOCALE_ISSUES_UNKNOWN,
@@ -2958,10 +2996,26 @@ static const JsonTest json_test[] =
       {
         /* i386 */
         .issues = SRT_LIBRARY_ISSUES_CANNOT_LOAD,
+        .runtime_linker =
+        {
+          .path = "/lib/ld-linux.so.2",
+          /* i386 is completely missing from this report */
+          .error_domain = "srt-architecture-error-quark",
+          .error_code = SRT_ARCHITECTURE_ERROR_NO_INFORMATION,
+          .error_message = "ABI \"i386-linux-gnu\" not included in report",
+        },
       },
       {
         .can_run = TRUE,
         .issues = SRT_LIBRARY_ISSUES_UNKNOWN,
+        .runtime_linker =
+        {
+          .path = "/lib64/ld-linux-x86-64.so.2",
+          /* We don't have the expected ld.so in the report */
+          .error_domain = "srt-architecture-error-quark",
+          .error_code = SRT_ARCHITECTURE_ERROR_INTERNAL_ERROR,
+          .error_message = "Expected \"/lib64/ld-linux-x86-64.so.2\" in report, but got \"/foobar\"",
+        },
       },
     },
     .locale_issues = SRT_LOCALE_ISSUES_UNKNOWN,
@@ -3001,9 +3055,23 @@ static const JsonTest json_test[] =
     .architecture =
     {
       {
+        .runtime_linker =
+        {
+          .path = "/lib/ld-linux.so.2",
+          .error_domain = "srt-architecture-error-quark",
+          .error_code = SRT_ARCHITECTURE_ERROR_NO_INFORMATION,
+          .error_message = "ABI \"i386-linux-gnu\" not included in report",
+        },
         .issues = SRT_LIBRARY_ISSUES_CANNOT_LOAD,
       },
       {
+        .runtime_linker =
+        {
+          .path = "/lib64/ld-linux-x86-64.so.2",
+          .error_domain = "srt-architecture-error-quark",
+          .error_code = SRT_ARCHITECTURE_ERROR_NO_INFORMATION,
+          .error_message = "ABI \"x86_64-linux-gnu\" not included in report",
+        },
         .issues = SRT_LIBRARY_ISSUES_CANNOT_LOAD,
       },
     },
@@ -3063,6 +3131,49 @@ static const JsonTest json_test[] =
   }, /* End Newer JSON report */
 };
 
+static void
+assert_expected_runtime_linker (SrtSystemInfo *info,
+                                const char *multiarch_tuple,
+                                const RuntimeLinkerTest *rtld)
+{
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *resolved = NULL;
+  const char *expected;
+  gboolean ok;
+
+  /* shorthand notation for having no expectations */
+  if (rtld->path == NULL)
+    return;
+
+  expected = srt_architecture_get_expected_runtime_linker (multiarch_tuple);
+  ok = srt_system_info_check_runtime_linker (info, multiarch_tuple,
+                                             &resolved, &error);
+
+  g_assert_cmpstr (rtld->path, ==, expected);
+
+  if (rtld->resolved != NULL)
+    {
+      g_assert (rtld->error_domain == NULL);
+      g_assert (rtld->error_code == 0);
+      g_assert (rtld->error_message == NULL);
+
+      g_assert_no_error (error);
+      g_assert_true (ok);
+      g_assert_cmpstr (resolved, ==, rtld->resolved);
+    }
+  else
+    {
+      g_assert (rtld->error_domain != NULL);
+      g_assert (rtld->error_message != NULL);
+
+      g_assert_error (error, g_quark_from_string (rtld->error_domain),
+                      rtld->error_code);
+      g_assert_cmpstr (error->message, ==, rtld->error_message);
+      g_assert_false (ok);
+      g_assert_cmpstr (resolved, ==, NULL);
+    }
+}
+
 static void
 json_parsing (Fixture *f,
               gconstpointer context)
@@ -3171,6 +3282,10 @@ json_parsing (Fixture *f,
           g_assert_cmpint (this_arch.issues, ==,
                            srt_system_info_check_libraries (info, multiarch_tuples[j], NULL));
 
+          assert_expected_runtime_linker (info,
+                                          multiarch_tuples[j],
+                                          &this_arch.runtime_linker);
+
           for (jj = 0; this_arch.graphics[jj].is_available; jj++)
             {
               SrtGraphics *graphics = NULL;
@@ -3442,6 +3557,112 @@ json_parsing (Fixture *f,
     }
 }
 
+static void
+architecture_symlinks (Fixture *f,
+                       gconstpointer context)
+{
+  g_autoptr(SrtSystemInfo) info;
+  g_autofree gchar *sysroot = NULL;
+  gboolean ret;
+
+  sysroot = g_build_filename (f->sysroots, "debian10", NULL);
+
+  info = srt_system_info_new (NULL);
+  srt_system_info_set_sysroot (info, sysroot);
+
+  /* In the mock Debian 10 sysroot created by tests/generate-sysroots.py,
+   * the well-known linker paths are symbolic links, much like they are
+   * on a real Debian system. */
+
+    {
+      g_autoptr(GError) error = NULL;
+      g_autofree gchar *resolved = NULL;
+
+      ret = srt_system_info_check_runtime_linker (info, SRT_ABI_X86_64,
+                                                  &resolved, &error);
+      g_assert_no_error (error);
+      g_assert_true (ret);
+      g_assert_cmpstr (resolved, ==, "/usr/lib/x86_64-linux-gnu/ld.so");
+    }
+
+    {
+      g_autoptr(GError) error = NULL;
+      g_autofree gchar *resolved = NULL;
+
+      ret = srt_system_info_check_runtime_linker (info, SRT_ABI_I386,
+                                                  &resolved, &error);
+      g_assert_no_error (error);
+      g_assert_true (ret);
+      g_assert_cmpstr (resolved, ==, "/usr/lib/i386-linux-gnu/ld.so");
+    }
+
+  /* The sysroot doesn't include x32 support. */
+    {
+      g_autoptr(GError) error = NULL;
+      g_autofree gchar *resolved = NULL;
+
+      ret = srt_system_info_check_runtime_linker (info, "x86_64-linux-gnux32",
+                                                  &resolved, &error);
+      g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND);
+      g_assert_false (ret);
+      g_assert_cmpstr (resolved, ==, NULL);
+    }
+
+    {
+      g_autoptr(GError) error = NULL;
+      g_autofree gchar *resolved = NULL;
+
+      ret = srt_system_info_check_runtime_linker (info, "hal9000-netbsd",
+                                                  &resolved, &error);
+      /* We have no idea what the runtime linker would be, so we have
+       * no way to check for it */
+      g_assert_error (error, SRT_ARCHITECTURE_ERROR,
+                      SRT_ARCHITECTURE_ERROR_NO_INFORMATION);
+      g_assert_false (ret);
+      g_assert_cmpstr (resolved, ==, NULL);
+    }
+}
+
+static void
+architecture_notlinks (Fixture *f,
+                       gconstpointer context)
+{
+  g_autoptr(SrtSystemInfo) info;
+  g_autofree gchar *sysroot = NULL;
+  gboolean ret;
+
+  sysroot = g_build_filename (f->sysroots, "ubuntu16", NULL);
+
+  info = srt_system_info_new (NULL);
+  srt_system_info_set_sysroot (info, sysroot);
+
+  /* In the mock Ubuntu 16.04 sysroot created by tests/generate-sysroots.py,
+   * the well-known runtime linker for x86_64 is a real file (unlike
+   * real Ubuntu systems) and the runtime linker for i386 is missing. */
+
+    {
+      g_autoptr(GError) error = NULL;
+      g_autofree gchar *resolved = NULL;
+
+      ret = srt_system_info_check_runtime_linker (info, "x86_64-linux-gnu",
+                                                  &resolved, &error);
+      g_assert_no_error (error);
+      g_assert_true (ret);
+      g_assert_cmpstr (resolved, ==, "/lib64/ld-linux-x86-64.so.2");
+    }
+
+    {
+      g_autoptr(GError) error = NULL;
+      g_autofree gchar *resolved = NULL;
+
+      ret = srt_system_info_check_runtime_linker (info, "i386-linux-gnu",
+                                                  &resolved, &error);
+      g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND);
+      g_assert_false (ret);
+      g_assert_cmpstr (resolved, ==, NULL);
+    }
+}
+
 int
 main (int argc,
       char **argv)
@@ -3531,6 +3752,11 @@ main (int argc,
   g_test_add ("/system-info/json_parsing", Fixture, NULL,
               setup, json_parsing, teardown);
 
+  g_test_add ("/system-info/architecture/symlinks", Fixture, NULL,
+              setup, architecture_symlinks, teardown);
+  g_test_add ("/system-info/architecture/notlinks", Fixture, NULL,
+              setup, architecture_notlinks, teardown);
+
   status = g_test_run ();
 
   if (!_srt_global_teardown_private_xdg_dirs ())
-- 
GitLab