diff --git a/debian/control b/debian/control
index 4bba83d208fcee7028c7483315419f4497de9b25..8ec09e31f18c7df95060300338f527d40e5c2894 100644
--- a/debian/control
+++ b/debian/control
@@ -6,6 +6,7 @@ Standards-Version: 4.4.0
 Build-Depends:
  debhelper (>= 9),
  g++ (>= 4:4.8) | g++-4.8,
+ libelf-dev,
  libglib2.0-dev,
  libsteam-runtime-tools-0-dev (>= 0.20200331.1~),
  libxau-dev,
diff --git a/meson.build b/meson.build
index e29eb930e14ac0cce9bd53674cbff1aaca85127f..78edcb1e5b1db960329fca5efdaaaff8b3ff4da4 100644
--- a/meson.build
+++ b/meson.build
@@ -150,6 +150,11 @@ glib_tap_support = dependency(
   required : false,
 )
 xau = dependency('xau', required : true)
+libelf = dependency('libelf', required : false)
+
+if not libelf.found()
+  libelf = declare_dependency(link_args : ['-lelf'])
+endif
 
 scripts = [
   'pressure-vessel-locale-gen',
diff --git a/src/elf-utils.c b/src/elf-utils.c
new file mode 100644
index 0000000000000000000000000000000000000000..5fcb790bc29f69b7998fd6283334d7dfabdff4a0
--- /dev/null
+++ b/src/elf-utils.c
@@ -0,0 +1,158 @@
+/*
+ * Copyright © 2017-2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "elf-utils.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+#include <libelf.h>
+
+#include "libglnx/libglnx.h"
+
+#include "glib-backports.h"
+
+#define throw_elf_error(error, format, ...) \
+  glnx_null_throw (error, format ": %s", ##__VA_ARGS__, elf_errmsg (elf_errno ()))
+
+/*
+ * pv_elf_open_fd:
+ * @fd: An open file descriptor
+ *
+ * Returns: (transfer full): A libelf object representing the library
+ */
+Elf *
+pv_elf_open_fd (int fd,
+                GError **error)
+{
+  g_autoptr(Elf) elf = NULL;
+
+  g_return_val_if_fail (fd >= 0, NULL);
+  g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+  if (elf_version (EV_CURRENT) == EV_NONE)
+    return throw_elf_error (error, "elf_version(EV_CURRENT)");
+
+  elf = elf_begin (fd, ELF_C_READ, NULL);
+
+  if (elf == NULL)
+    return throw_elf_error (error, "elf_begin");
+
+  return g_steal_pointer (&elf);
+}
+
+/*
+ * pv_elf_get_soname:
+ * @elf: A libelf object
+ *
+ * Return the `DT_SONAME` header, or %NULL on error
+ */
+gchar *
+pv_elf_get_soname (Elf *elf,
+                   GError **error)
+{
+  GElf_Ehdr ehdr;
+  size_t phdr_count = 0;
+  size_t i;
+  GElf_Phdr phdr_mem;
+  GElf_Phdr *phdr = NULL;
+  GElf_Dyn *dynamic_header = NULL;
+  GElf_Dyn dynamic_header_mem;
+  Elf_Scn *dynamic_section = NULL;
+  GElf_Shdr dynamic_section_header_mem;
+  GElf_Shdr *dynamic_section_header = NULL;
+  Elf_Scn *string_table_section = NULL;
+  GElf_Shdr string_table_header_mem;
+  GElf_Shdr *string_table_header = NULL;
+  Elf_Data *data = NULL;
+  size_t size_per_dyn;
+  const char *soname = NULL;
+
+  g_return_val_if_fail (elf != NULL, NULL);
+  g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+  if (elf_kind (elf) != ELF_K_ELF)
+    return glnx_null_throw (error, "elf_kind %d, expected ELF_K_ELF=%d",
+                            elf_kind (elf), ELF_K_ELF);
+
+  if (gelf_getehdr (elf, &ehdr) == NULL)
+    return throw_elf_error (error, "elf_getehdr");
+
+  if (ehdr.e_type != ET_DYN)
+    return glnx_null_throw (error, "ehdr.e_type %d, expected ET_DYN=%d",
+                            ehdr.e_type, ET_DYN);
+
+  if (elf_getphdrnum (elf, &phdr_count) != 0)
+    return throw_elf_error (error, "elf_getphdrnum");
+
+  for (i = 0; i < phdr_count; i++)
+    {
+      phdr = gelf_getphdr (elf, i, &phdr_mem);
+
+      if (phdr != NULL && phdr->p_type == PT_DYNAMIC)
+        {
+          dynamic_section = gelf_offscn (elf, phdr->p_offset);
+          dynamic_section_header = gelf_getshdr (dynamic_section,
+                                                 &dynamic_section_header_mem);
+          break;
+        }
+    }
+
+  if (dynamic_section == NULL || dynamic_section_header == NULL)
+    return glnx_null_throw (error, "Unable to find dynamic section header");
+
+  string_table_section = elf_getscn (elf, dynamic_section_header->sh_link);
+  string_table_header = gelf_getshdr (string_table_section,
+                                      &string_table_header_mem);
+
+  if (string_table_section == NULL || string_table_header == NULL)
+    return glnx_null_throw (error, "Unable to find linked string table");
+
+  data = elf_getdata (dynamic_section, NULL);
+
+  if (data == NULL)
+    return throw_elf_error (error, "elf_getdata(dynamic_section)");
+
+  size_per_dyn = gelf_fsize (elf, ELF_T_DYN, 1, EV_CURRENT);
+
+  for (i = 0; i < dynamic_section_header->sh_size / size_per_dyn; i++)
+    {
+      dynamic_header = gelf_getdyn (data, i, &dynamic_header_mem);
+
+      if (dynamic_header == NULL)
+        break;
+
+      if (dynamic_header->d_tag == DT_SONAME)
+        {
+          soname = elf_strptr (elf, dynamic_section_header->sh_link,
+                               dynamic_header->d_un.d_val);
+
+          if (soname == NULL)
+            return glnx_null_throw (error, "Unable to read DT_SONAME");
+
+
+        }
+    }
+
+  if (soname == NULL)
+    return glnx_null_throw (error, "Unable to find DT_SONAME");
+
+  return g_strdup (soname);
+}
+
diff --git a/src/elf-utils.h b/src/elf-utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..fa89652e727feb5ca26b5ced048203434fceffb5
--- /dev/null
+++ b/src/elf-utils.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gelf.h>
+#include <glib.h>
+#include <libelf.h>
+
+#include "libglnx/libglnx.h"
+
+#include "glib-backports.h"
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (Elf, elf_end);
+
+Elf *pv_elf_open_fd (int fd,
+                     GError **error);
+
+gchar *pv_elf_get_soname (Elf *elf,
+                          GError **error);
diff --git a/src/meson.build b/src/meson.build
index 789e181cf8c8272ecc98181d793e29df225abeca..220d1d30be08deec869384d4fab3bf2573979338 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -37,6 +37,8 @@ pressure_vessel_utils = static_library(
   sources : [
     'bwrap-lock.c',
     'bwrap-lock.h',
+    'elf-utils.c',
+    'elf-utils.h',
     'flatpak-utils-base.c',
     'flatpak-utils-base-private.h',
     'flatpak-utils.c',
@@ -50,6 +52,7 @@ pressure_vessel_utils = static_library(
   ],
   dependencies : [
     gio_unix,
+    libelf,
     libglnx.get_variable('libglnx_dep'),
   ],
   include_directories : project_include_dirs,
diff --git a/tests/elf-get-soname.c b/tests/elf-get-soname.c
new file mode 100644
index 0000000000000000000000000000000000000000..218623ed8cdd2bf5ab04c2504c9a54041b876120
--- /dev/null
+++ b/tests/elf-get-soname.c
@@ -0,0 +1,114 @@
+/*
+ * Copyright © 2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "subprojects/libglnx/config.h"
+
+#include <locale.h>
+#include <sysexits.h>
+
+#include "libglnx/libglnx.h"
+
+#include "glib-backports.h"
+
+#include "elf-utils.h"
+#include "utils.h"
+
+static GOptionEntry options[] =
+{
+  { NULL }
+};
+
+int
+main (int argc,
+      char *argv[])
+{
+  g_autoptr(GOptionContext) context = NULL;
+  g_autoptr(GError) local_error = NULL;
+  GError **error = &local_error;
+  int ret = EX_USAGE;
+  int i;
+
+  setlocale (LC_ALL, "");
+  pv_avoid_gvfs ();
+
+  context = g_option_context_new ("LIBRARY...");
+  g_option_context_add_main_entries (context, options, NULL);
+
+  if (!g_option_context_parse (context, &argc, &argv, error))
+    goto out;
+
+  if (argc >= 2 && strcmp (argv[1], "--") == 0)
+    {
+      argv++;
+      argc--;
+    }
+
+  if (argc < 2)
+    {
+      g_printerr ("A library to open is required\n");
+      goto out;
+    }
+
+  ret = 0;
+
+  for (i = 1; i < argc; i++)
+    {
+      int fd = open (argv[i], O_RDONLY | O_CLOEXEC);
+      g_autoptr(Elf) elf = NULL;
+      g_autofree gchar *soname = NULL;
+
+      if (fd < 0)
+        {
+          g_printerr ("Cannot open %s: %s\n", argv[i], g_strerror (errno));
+          ret = 1;
+          continue;
+        }
+
+      elf = pv_elf_open_fd (fd, error);
+
+      if (elf == NULL)
+        {
+          g_printerr ("Unable to open %s: %s\n",
+                      argv[i], local_error->message);
+          g_clear_error (error);
+          ret = 1;
+          continue;
+        }
+
+      soname = pv_elf_get_soname (elf, error);
+
+      if (soname == NULL)
+        {
+          g_printerr ("Unable to get SONAME of %s: %s\n",
+                      argv[i], local_error->message);
+          g_clear_error (error);
+          ret = 1;
+          continue;
+        }
+
+      g_print ("%s DT_SONAME: %s\n", argv[i], soname);
+    }
+
+out:
+  if (local_error != NULL)
+    g_warning ("%s", local_error->message);
+
+  return ret;
+}
diff --git a/tests/meson.build b/tests/meson.build
index 64bceaefd0983658ee251789ad4d9aceed75c43f..b122e5e4850b367b3d7c400aa9bbfcd103eb9e03 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -102,20 +102,28 @@ foreach test_name : tests
   endif
 endforeach
 
-executable(
-  'test-cheap-copy',
-  sources : [
-    'cheap-copy.c',
-  ],
-  dependencies : [
-    gio_unix,
-    libglnx.get_variable('libglnx_dep'),
-  ],
-  link_with : [
-    pressure_vessel_utils,
-  ],
-  include_directories : project_include_dirs,
-  install : false,
-)
+# Helper programs and manual tests
+helpers = [
+  'cheap-copy',
+  'elf-get-soname',
+]
+
+foreach helper : helpers
+  executable(
+    'test-' + helper,
+    sources : [
+      helper + '.c',
+    ],
+    dependencies : [
+      gio_unix,
+      libglnx.get_variable('libglnx_dep'),
+    ],
+    link_with : [
+      pressure_vessel_utils,
+    ],
+    include_directories : project_include_dirs,
+    install : false,
+  )
+endforeach
 
 # vim:set sw=2 sts=2 et: