diff --git a/include/libcamera/internal/utils.h b/include/libcamera/internal/utils.h
index 742657bebb28..98d5094eb88d 100644
--- a/include/libcamera/internal/utils.h
+++ b/include/libcamera/internal/utils.h
@@ -8,9 +8,12 @@
 #pragma once
 
 #include <functional>
+#include <stdint.h>
 #include <string>
 #include <vector>
 
+#include <libcamera/base/span.h>
+
 namespace libcamera {
 
 namespace utils {
@@ -21,6 +24,9 @@ void parseDir(const char *libDir, unsigned int maxDepth,
 unsigned int addDir(const char *libDir, unsigned int maxDepth,
 		    std::function<int(const std::string &)> func);
 
+int elfVerifyIdent(Span<const uint8_t> elf);
+Span<const uint8_t> elfLoadSymbol(Span<const uint8_t> elf, const char *symbol);
+
 } /* namespace utils */
 
 } /* namespace libcamera */
diff --git a/src/libcamera/ipa_module.cpp b/src/libcamera/ipa_module.cpp
index e6ea61e44829..c8559ec8b74c 100644
--- a/src/libcamera/ipa_module.cpp
+++ b/src/libcamera/ipa_module.cpp
@@ -25,6 +25,7 @@
 #include <libcamera/base/utils.h>
 
 #include "libcamera/internal/pipeline_handler.h"
+#include "libcamera/internal/utils.h"
 
 /**
  * \file ipa_module.h
@@ -40,153 +41,6 @@ namespace libcamera {
 
 LOG_DEFINE_CATEGORY(IPAModule)
 
-namespace {
-
-template<typename T>
-typename std::remove_extent_t<T> *elfPointer(Span<const uint8_t> elf,
-					     off_t offset, size_t objSize)
-{
-	size_t size = offset + objSize;
-	if (size > elf.size() || size < objSize)
-		return nullptr;
-
-	return reinterpret_cast<typename std::remove_extent_t<T> *>(
-		reinterpret_cast<const char *>(elf.data()) + offset);
-}
-
-template<typename T>
-typename std::remove_extent_t<T> *elfPointer(Span<const uint8_t> elf,
-					     off_t offset)
-{
-	return elfPointer<T>(elf, offset, sizeof(T));
-}
-
-int elfVerifyIdent(Span<const uint8_t> elf)
-{
-	const char *e_ident = elfPointer<const char[EI_NIDENT]>(elf, 0);
-	if (!e_ident)
-		return -ENOEXEC;
-
-	if (e_ident[EI_MAG0] != ELFMAG0 ||
-	    e_ident[EI_MAG1] != ELFMAG1 ||
-	    e_ident[EI_MAG2] != ELFMAG2 ||
-	    e_ident[EI_MAG3] != ELFMAG3 ||
-	    e_ident[EI_VERSION] != EV_CURRENT)
-		return -ENOEXEC;
-
-	int bitClass = sizeof(unsigned long) == 4 ? ELFCLASS32 : ELFCLASS64;
-	if (e_ident[EI_CLASS] != bitClass)
-		return -ENOEXEC;
-
-	int a = 1;
-	unsigned char endianness = *reinterpret_cast<char *>(&a) == 1
-				 ? ELFDATA2LSB : ELFDATA2MSB;
-	if (e_ident[EI_DATA] != endianness)
-		return -ENOEXEC;
-
-	return 0;
-}
-
-const ElfW(Shdr) *elfSection(Span<const uint8_t> elf, const ElfW(Ehdr) *eHdr,
-			     ElfW(Half) idx)
-{
-	if (idx >= eHdr->e_shnum)
-		return nullptr;
-
-	off_t offset = eHdr->e_shoff + idx *
-				       static_cast<uint32_t>(eHdr->e_shentsize);
-	return elfPointer<const ElfW(Shdr)>(elf, offset);
-}
-
-/**
- * \brief Retrieve address and size of a symbol from an mmap'ed ELF file
- * \param[in] elf Address and size of mmap'ed ELF file
- * \param[in] symbol Symbol name
- *
- * \return The memory region storing the symbol on success, or an empty span
- * otherwise
- */
-Span<const uint8_t> elfLoadSymbol(Span<const uint8_t> elf, const char *symbol)
-{
-	const ElfW(Ehdr) *eHdr = elfPointer<const ElfW(Ehdr)>(elf, 0);
-	if (!eHdr)
-		return {};
-
-	const ElfW(Shdr) *sHdr = elfSection(elf, eHdr, eHdr->e_shstrndx);
-	if (!sHdr)
-		return {};
-	off_t shnameoff = sHdr->sh_offset;
-
-	/* Locate .dynsym section header. */
-	const ElfW(Shdr) *dynsym = nullptr;
-	for (unsigned int i = 0; i < eHdr->e_shnum; i++) {
-		sHdr = elfSection(elf, eHdr, i);
-		if (!sHdr)
-			return {};
-
-		off_t offset = shnameoff + sHdr->sh_name;
-		const char *name = elfPointer<const char[8]>(elf, offset);
-		if (!name)
-			return {};
-
-		if (sHdr->sh_type == SHT_DYNSYM && !strcmp(name, ".dynsym")) {
-			dynsym = sHdr;
-			break;
-		}
-	}
-
-	if (dynsym == nullptr) {
-		LOG(IPAModule, Error) << "ELF has no .dynsym section";
-		return {};
-	}
-
-	sHdr = elfSection(elf, eHdr, dynsym->sh_link);
-	if (!sHdr)
-		return {};
-	off_t dynsym_nameoff = sHdr->sh_offset;
-
-	/* Locate symbol in the .dynsym section. */
-	const ElfW(Sym) *targetSymbol = nullptr;
-	unsigned int dynsym_num = dynsym->sh_size / dynsym->sh_entsize;
-	for (unsigned int i = 0; i < dynsym_num; i++) {
-		off_t offset = dynsym->sh_offset + dynsym->sh_entsize * i;
-		const ElfW(Sym) *sym = elfPointer<const ElfW(Sym)>(elf, offset);
-		if (!sym)
-			return {};
-
-		offset = dynsym_nameoff + sym->st_name;
-		const char *name = elfPointer<const char>(elf, offset,
-							  strlen(symbol) + 1);
-		if (!name)
-			return {};
-
-		if (!strcmp(name, symbol) &&
-		    sym->st_info & STB_GLOBAL) {
-			targetSymbol = sym;
-			break;
-		}
-	}
-
-	if (targetSymbol == nullptr) {
-		LOG(IPAModule, Error) << "Symbol " << symbol << " not found";
-		return {};
-	}
-
-	/* Locate and return data of symbol. */
-	sHdr = elfSection(elf, eHdr, targetSymbol->st_shndx);
-	if (!sHdr)
-		return {};
-	off_t offset = sHdr->sh_offset + (targetSymbol->st_value - sHdr->sh_addr);
-	const uint8_t *data = elfPointer<const uint8_t>(elf, offset,
-							targetSymbol->st_size);
-	if (!data)
-		return {};
-
-	return { data, targetSymbol->st_size };
-}
-
-} /* namespace */
-
 /**
  * \def IPA_MODULE_API_VERSION
  * \brief The IPA module API version
@@ -280,13 +134,13 @@ int IPAModule::loadIPAModuleInfo()
 	}
 
 	Span<const uint8_t> data = file.map();
-	int ret = elfVerifyIdent(data);
+	int ret = utils::elfVerifyIdent(data);
 	if (ret) {
 		LOG(IPAModule, Error) << "IPA module is not an ELF file";
 		return ret;
 	}
 
-	Span<const uint8_t> info = elfLoadSymbol(data, "ipaModuleInfo");
+	Span<const uint8_t> info = utils::elfLoadSymbol(data, "ipaModuleInfo");
 	if (info.size() < sizeof(info_)) {
 		LOG(IPAModule, Error) << "IPA module has no valid info";
 		return -EINVAL;
diff --git a/src/libcamera/utils.cpp b/src/libcamera/utils.cpp
index ef046ac3134e..f7bc5c54ff57 100644
--- a/src/libcamera/utils.cpp
+++ b/src/libcamera/utils.cpp
@@ -10,20 +10,63 @@
 #include <algorithm>
 #include <dirent.h>
 #include <functional>
+#include <link.h>
+#include <stdint.h>
 #include <string.h>
 #include <string>
 #include <sys/types.h>
 #include <vector>
 
+#include <libcamera/base/log.h>
+
 /**
  * \file internal/utils.h
  * \brief Miscellaneous utility functions (internal)
  */
 
+/* musl doesn't declare _DYNAMIC in link.h, declare it manually. */
+extern ElfW(Dyn) _DYNAMIC[];
+
 namespace libcamera {
 
+namespace {
+
+template<typename T>
+typename std::remove_extent_t<T> *elfPointer(Span<const uint8_t> elf,
+					     off_t offset, size_t objSize)
+{
+	size_t size = offset + objSize;
+	if (size > elf.size() || size < objSize)
+		return nullptr;
+
+	return reinterpret_cast<typename std::remove_extent_t<T> *>(
+		reinterpret_cast<const char *>(elf.data()) + offset);
+}
+
+template<typename T>
+typename std::remove_extent_t<T> *elfPointer(Span<const uint8_t> elf,
+					     off_t offset)
+{
+	return elfPointer<T>(elf, offset, sizeof(T));
+}
+
+const ElfW(Shdr) *elfSection(Span<const uint8_t> elf, const ElfW(Ehdr) *eHdr,
+			     ElfW(Half) idx)
+{
+	if (idx >= eHdr->e_shnum)
+		return nullptr;
+
+	off_t offset = eHdr->e_shoff + idx *
+				       static_cast<uint32_t>(eHdr->e_shentsize);
+	return elfPointer<const ElfW(Shdr)>(elf, offset);
+}
+
+} /* namespace */
+
 namespace utils {
 
+LOG_DEFINE_CATEGORY(Utils)
+
 /**
  * \brief Identify shared library objects within a directory
  * \param[in] libDir The directory to search for shared objects
@@ -102,6 +145,120 @@ unsigned int addDir(const char *libDir, unsigned int maxDepth,
 	return count;
 }
 
+int elfVerifyIdent(Span<const uint8_t> elf)
+{
+	const char *e_ident = elfPointer<const char[EI_NIDENT]>(elf, 0);
+	if (!e_ident)
+		return -ENOEXEC;
+
+	if (e_ident[EI_MAG0] != ELFMAG0 ||
+	    e_ident[EI_MAG1] != ELFMAG1 ||
+	    e_ident[EI_MAG2] != ELFMAG2 ||
+	    e_ident[EI_MAG3] != ELFMAG3 ||
+	    e_ident[EI_VERSION] != EV_CURRENT)
+		return -ENOEXEC;
+
+	int bitClass = sizeof(unsigned long) == 4 ? ELFCLASS32 : ELFCLASS64;
+	if (e_ident[EI_CLASS] != bitClass)
+		return -ENOEXEC;
+
+	int a = 1;
+	unsigned char endianness = *reinterpret_cast<char *>(&a) == 1
+				 ? ELFDATA2LSB : ELFDATA2MSB;
+	if (e_ident[EI_DATA] != endianness)
+		return -ENOEXEC;
+
+	return 0;
+}
+
+/**
+ * \brief Retrieve address and size of a symbol from an mmap'ed ELF file
+ * \param[in] elf Address and size of mmap'ed ELF file
+ * \param[in] symbol Symbol name
+ *
+ * \return The memory region storing the symbol on success, or an empty span
+ * otherwise
+ */
+Span<const uint8_t> elfLoadSymbol(Span<const uint8_t> elf, const char *symbol)
+{
+	const ElfW(Ehdr) *eHdr = elfPointer<const ElfW(Ehdr)>(elf, 0);
+	if (!eHdr)
+		return {};
+
+	const ElfW(Shdr) *sHdr = elfSection(elf, eHdr, eHdr->e_shstrndx);
+	if (!sHdr)
+		return {};
+	off_t shnameoff = sHdr->sh_offset;
+
+	/* Locate .dynsym section header. */
+	const ElfW(Shdr) *dynsym = nullptr;
+	for (unsigned int i = 0; i < eHdr->e_shnum; i++) {
+		sHdr = elfSection(elf, eHdr, i);
+		if (!sHdr)
+			return {};
+
+		off_t offset = shnameoff + sHdr->sh_name;
+		const char *name = elfPointer<const char[8]>(elf, offset);
+		if (!name)
+			return {};
+
+		if (sHdr->sh_type == SHT_DYNSYM && !strcmp(name, ".dynsym")) {
+			dynsym = sHdr;
+			break;
+		}
+	}
+
+	if (dynsym == nullptr) {
+		LOG(Utils, Error) << "ELF has no .dynsym section";
+		return {};
+	}
+
+	sHdr = elfSection(elf, eHdr, dynsym->sh_link);
+	if (!sHdr)
+		return {};
+	off_t dynsym_nameoff = sHdr->sh_offset;
+
+	/* Locate symbol in the .dynsym section. */
+	const ElfW(Sym) *targetSymbol = nullptr;
+	unsigned int dynsym_num = dynsym->sh_size / dynsym->sh_entsize;
+	for (unsigned int i = 0; i < dynsym_num; i++) {
+		off_t offset = dynsym->sh_offset + dynsym->sh_entsize * i;
+		const ElfW(Sym) *sym = elfPointer<const ElfW(Sym)>(elf, offset);
+		if (!sym)
+			return {};
+
+		offset = dynsym_nameoff + sym->st_name;
+		const char *name = elfPointer<const char>(elf, offset,
+							  strlen(symbol) + 1);
+		if (!name)
+			return {};
+
+		if (!strcmp(name, symbol) &&
+		    sym->st_info & STB_GLOBAL) {
+			targetSymbol = sym;
+			break;
+		}
+	}
+
+	if (targetSymbol == nullptr) {
+		LOG(Utils, Error) << "Symbol " << symbol << " not found";
+		return {};
+	}
+
+	/* Locate and return data of symbol. */
+	sHdr = elfSection(elf, eHdr, targetSymbol->st_shndx);
+	if (!sHdr)
+		return {};
+	off_t offset = sHdr->sh_offset + (targetSymbol->st_value - sHdr->sh_addr);
+	const uint8_t *data = elfPointer<const uint8_t>(elf, offset,
+							targetSymbol->st_size);
+	if (!data)
+		return {};
+
+	return { data, targetSymbol->st_size };
+}
+
+
 } /* namespace utils */
 
 } /* namespace libcamera */
