Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20251113200633.833127-2-bill.roberts@arm.com>
Date: Thu, 13 Nov 2025 13:44:26 -0600
From: Bill Roberts <bill.roberts@....com>
To: musl@...ts.openwall.com
Cc: Bill Roberts <bill.roberts@....com>
Subject: [PATCH 1/3] dso/aarch64: add PROT_BTI support for mem maps

This change adds support for setting PROT_BTI on executable mappings
when the corresponding ELF object includes the GNU_PROPERTY_AARCH64_FEATURE_1_BTI
note. This ensures correct Branch Target Identification (BTI) enforcement
as defined in the Arm Architecture Reference Manual (Arm ARM) and the
ELF for the Arm 64-bit Architecture (AArch64) ABI supplement.

When loading shared objects or executables, the dynamic loader now checks
for BTI-related GNU notes and applies the appropriate protection flags
via `mprotect` and `mmap` during mapping. This aligns musl’s behavior with
other modern loaders, improving security and correctness on systems with BTI
enabled hardware.

References:
 - Arm Architecture Reference Manual for A-profile architecture (Arm ARM):
   https://developer.arm.com/documentation/ddi0487/latest
 - ELF for the Arm® 64-bit Architecture (AArch64) ABI supplement:
   https://github.com/ARM-software/abi-aa/blob/main/aaelf64/aaelf64.rst
 - Arm Community Blog – *Enabling Pointer Authentication and Branch Target Identification*:
   1. https://community.arm.com/arm-community-blogs/b/architectures-and-processors-blog/posts/enabling-pac-and-bti-part1
   2. https://community.arm.com/arm-community-blogs/b/architectures-and-processors-blog/posts/enabling-pac-and-bti-part2
   3. https://community.arm.com/arm-community-blogs/b/architectures-and-processors-blog/posts/enabling-pac-and-bti-part3

Signed-off-by: Bill Roberts <bill.roberts@....com>
---
 Makefile                          |  2 +-
 configure                         |  1 +
 include/elf.h                     |  2 +
 ldso/dynlink.c                    | 50 ++++++++++++++---
 src/ldso/aarch64/arch_ldso_hook.c | 90 +++++++++++++++++++++++++++++++
 src/ldso/aarch64/arch_ldso_hook.h | 21 ++++++++
 6 files changed, 158 insertions(+), 8 deletions(-)
 create mode 100644 src/ldso/aarch64/arch_ldso_hook.c
 create mode 100644 src/ldso/aarch64/arch_ldso_hook.h

diff --git a/Makefile b/Makefile
index 3ad88b35..a20bafaf 100644
--- a/Makefile
+++ b/Makefile
@@ -47,7 +47,7 @@ CFLAGS_AUTO = -Os -pipe
 CFLAGS_C99FSE = -std=c99 -ffreestanding -nostdinc 
 
 CFLAGS_ALL = $(CFLAGS_C99FSE)
-CFLAGS_ALL += -D_XOPEN_SOURCE=700 -I$(srcdir)/arch/$(ARCH) -I$(srcdir)/arch/generic -Iobj/src/internal -I$(srcdir)/src/include -I$(srcdir)/src/internal -Iobj/include -I$(srcdir)/include
+CFLAGS_ALL += -D_XOPEN_SOURCE=700 -I$(srcdir)/arch/$(ARCH) -I$(srcdir)/arch/generic -Iobj/src/internal -I$(srcdir)/src/include -I$(srcdir)/src/internal -Iobj/include -I$(srcdir)/include -I$(srcdir)/src/ldso/$(ARCH)
 CFLAGS_ALL += $(CPPFLAGS) $(CFLAGS_AUTO) $(CFLAGS)
 
 LDFLAGS_ALL = $(LDFLAGS_AUTO) $(LDFLAGS)
diff --git a/configure b/configure
index bc9fbe48..3809f24c 100755
--- a/configure
+++ b/configure
@@ -671,6 +671,7 @@ fi
 
 if test "$ARCH" = "aarch64" ; then
 trycppif __AARCH64EB__ "$t" && SUBARCH=${SUBARCH}_be
+CFLAGS_AUTO="${CFLAGS_AUTO} -DARCH_SUPPORTS_DL_ADD_PROTECTIONS"
 fi
 
 if test "$ARCH" = "loongarch64" ; then
diff --git a/include/elf.h b/include/elf.h
index d6ae539a..e40b815d 100644
--- a/include/elf.h
+++ b/include/elf.h
@@ -1110,7 +1110,9 @@ typedef struct {
 #define NT_GNU_GOLD_VERSION	4
 #define NT_GNU_PROPERTY_TYPE_0	5
 
+#define GNU_PROPERTY_AARCH64_FEATURE_1_AND 0xc0000000
 
+#define GNU_PROPERTY_AARCH64_FEATURE_1_BTI (1U << 0)
 
 typedef struct {
   Elf32_Xword m_value;
diff --git a/ldso/dynlink.c b/ldso/dynlink.c
index 715948f4..d204f2d5 100644
--- a/ldso/dynlink.c
+++ b/ldso/dynlink.c
@@ -682,6 +682,21 @@ static void unmap_library(struct dso *dso)
 	}
 }
 
+#ifdef ARCH_SUPPORTS_DL_ADD_PROTECTIONS
+#include <arch_ldso_hook.h>
+#else
+static inline unsigned dl_add_protections(const Elf64_Phdr *ph,
+	const Elf64_Ehdr *eh, int fd, unsigned *prot, unsigned *mask)
+{
+	(void)ph;
+	(void)eh;
+	(void)fd;
+	(void)prot;
+	(void)mask;
+	return 0;
+}
+#endif
+
 static void *map_library(int fd, struct dso *dso)
 {
 	Ehdr buf[(896+sizeof(Ehdr))/sizeof(Ehdr)];
@@ -693,7 +708,7 @@ static void *map_library(int fd, struct dso *dso)
 	off_t off_start;
 	Ehdr *eh;
 	Phdr *ph, *ph0;
-	unsigned prot;
+	unsigned prot, arch_prot, arch_mask;
 	unsigned char *map=MAP_FAILED, *base;
 	size_t dyn=0;
 	size_t tls_image=0;
@@ -720,6 +735,16 @@ static void *map_library(int fd, struct dso *dso)
 	} else {
 		ph = ph0 = (void *)((char *)buf + eh->e_phoff);
 	}
+
+	/*
+	 * some architectures may have additional protection flags embedded in the
+	 * ELF section somewhere. Start with those and OR in the extras. Do this
+	 * before we need to map any sections.
+	 */
+	if(dl_add_protections(ph, eh, fd, &arch_prot, &arch_mask))
+		goto error;
+
+
 	for (i=eh->e_phnum; i; i--, ph=(void *)((char *)ph+eh->e_phentsize)) {
 		if (ph->p_type == PT_DYNAMIC) {
 			dyn = ph->p_vaddr;
@@ -746,6 +771,7 @@ static void *map_library(int fd, struct dso *dso)
 			prot = (((ph->p_flags&PF_R) ? PROT_READ : 0) |
 				((ph->p_flags&PF_W) ? PROT_WRITE: 0) |
 				((ph->p_flags&PF_X) ? PROT_EXEC : 0));
+			prot |= prot&arch_mask ? arch_prot : 0;
 		}
 		if (ph->p_vaddr+ph->p_memsz > addr_max) {
 			addr_max = ph->p_vaddr+ph->p_memsz;
@@ -762,6 +788,7 @@ static void *map_library(int fd, struct dso *dso)
 			prot = (((ph->p_flags&PF_R) ? PROT_READ : 0) |
 				((ph->p_flags&PF_W) ? PROT_WRITE: 0) |
 				((ph->p_flags&PF_X) ? PROT_EXEC : 0));
+			prot |= prot&arch_mask ? arch_prot : 0;
 			map = mmap(0, ph->p_memsz + (ph->p_vaddr & PAGE_SIZE-1),
 				prot, MAP_PRIVATE,
 				fd, ph->p_offset & -PAGE_SIZE);
@@ -780,6 +807,7 @@ static void *map_library(int fd, struct dso *dso)
 				size_t pgbrk = brk + PAGE_SIZE-1 & -PAGE_SIZE;
 				size_t pgend = brk + ph->p_memsz - ph->p_filesz
 					+ PAGE_SIZE-1 & -PAGE_SIZE;
+				/* arch_prot has already been added */
 				if (pgend > pgbrk && mmap_fixed(map+pgbrk,
 					pgend-pgbrk, prot,
 					MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS,
@@ -801,11 +829,16 @@ static void *map_library(int fd, struct dso *dso)
 	 * the length of the file. This is okay because we will not
 	 * use the invalid part; we just need to reserve the right
 	 * amount of virtual address space to map over later. */
-	map = DL_NOMMU_SUPPORT
-		? mmap((void *)addr_min, map_len, PROT_READ|PROT_WRITE|PROT_EXEC,
-			MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
-		: mmap((void *)addr_min, map_len, prot,
-			MAP_PRIVATE, fd, off_start);
+	if (map == DL_NOMMU_SUPPORT) {
+		prot = PROT_READ|PROT_WRITE|PROT_EXEC;
+		prot |= prot&arch_mask ? arch_prot : 0;
+		map = mmap((void *)addr_min, map_len, prot,
+				MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
+	} else {
+		prot = prot & arch_mask ? prot | arch_prot : prot;
+		map = mmap((void *)addr_min, map_len, prot,
+				MAP_PRIVATE, fd, off_start);
+	}
 	if (map==MAP_FAILED) goto error;
 	dso->map = map;
 	dso->map_len = map_len;
@@ -835,6 +868,7 @@ static void *map_library(int fd, struct dso *dso)
 		prot = (((ph->p_flags&PF_R) ? PROT_READ : 0) |
 			((ph->p_flags&PF_W) ? PROT_WRITE: 0) |
 			((ph->p_flags&PF_X) ? PROT_EXEC : 0));
+		prot |= prot&arch_mask ? arch_prot : 0;
 		/* Reuse the existing mapping for the lowest-address LOAD */
 		if ((ph->p_vaddr & -PAGE_SIZE) != addr_min || DL_NOMMU_SUPPORT)
 			if (mmap_fixed(base+this_min, this_max-this_min, prot, MAP_PRIVATE|MAP_FIXED, fd, off_start) == MAP_FAILED)
@@ -849,7 +883,9 @@ static void *map_library(int fd, struct dso *dso)
 	}
 	for (i=0; ((size_t *)(base+dyn))[i]; i+=2)
 		if (((size_t *)(base+dyn))[i]==DT_TEXTREL) {
-			if (mprotect(map, map_len, PROT_READ|PROT_WRITE|PROT_EXEC)
+			prot = PROT_READ|PROT_WRITE|PROT_EXEC;
+			prot |= prot&arch_mask ? arch_prot : 0;
+			if (mprotect(map, map_len, prot)
 			    && errno != ENOSYS)
 				goto error;
 			break;
diff --git a/src/ldso/aarch64/arch_ldso_hook.c b/src/ldso/aarch64/arch_ldso_hook.c
new file mode 100644
index 00000000..9a755707
--- /dev/null
+++ b/src/ldso/aarch64/arch_ldso_hook.c
@@ -0,0 +1,90 @@
+#include <elf.h>
+#include <sys/mman.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+
+unsigned dl_add_protections(const Elf64_Phdr *ph, const Elf64_Ehdr *eh, int fd,
+		unsigned *prot, unsigned *mask) {
+	size_t i;
+	char *buf = NULL;
+	unsigned rc = 1;
+
+	/*
+	 * Unfortunately, we need to loop through the ELF header, as we need
+	 * to get the PROT flags before we map anything in and they could be
+	 * anywhere.
+	 */
+	for (i=eh->e_phnum; i; i--, ph=(void *)((char *)ph+eh->e_phentsize)) {
+
+		/* skip non note sections */
+		if (ph->p_type != PT_NOTE)
+			continue;
+
+		buf = malloc(ph->p_filesz);
+		if (!buf)
+			return rc;
+
+		if (pread(fd, buf, ph->p_filesz, ph->p_offset) != ph->p_filesz)
+			goto error;
+
+		const char *p = buf;
+		const char *end = buf + ph->p_filesz;
+
+		/* for each ELF note */
+		while (p + sizeof(Elf64_Nhdr) <= end) {
+
+			const Elf64_Nhdr *nh = (const Elf64_Nhdr*) p;
+			p += sizeof(Elf64_Nhdr);
+
+			const char *name = (const char*) p;
+			p += (nh->n_namesz + 3) & ~3;  // 4-byte align
+
+			const char *desc = p;
+			p += (nh->n_descsz + 3) & ~3;
+
+			if (p > end)
+				break;
+
+			/* We're only interested in GNU notes and property type 0*/
+			if (nh->n_namesz != 4|| memcmp(name, "GNU", 4)
+				|| nh->n_type != NT_GNU_PROPERTY_TYPE_0) {
+				goto out;
+			}
+
+			const char *dp = desc;
+			const char *dend = desc + nh->n_descsz;
+
+			/* for each property in the property list, kind of like TLV record in series */
+			while (dp + 2 * sizeof(uint32_t) <= dend) {
+				uint32_t pr_type = *(const uint32_t*) dp;
+				dp += sizeof(uint32_t);
+				uint32_t pr_datasz = *(const uint32_t*) dp;
+				dp += sizeof(uint32_t);
+
+				if (dp + pr_datasz > dend)
+					break;
+
+				if (pr_type == GNU_PROPERTY_AARCH64_FEATURE_1_AND
+						&& pr_datasz >= 4) {
+					uint32_t features = *(const uint32_t*) dp;
+
+					if (features & GNU_PROPERTY_AARCH64_FEATURE_1_BTI) {
+						(*prot) |= PROT_BTI;
+						goto out;
+					}
+				}
+
+				dp += (pr_datasz + 3) & ~3;  // align to 4 bytes
+			}
+		}
+	}
+out:
+	/* Only add flags if the mapping will be executable */
+	*mask = PROT_EXEC;
+	rc = 0;
+error:
+	free(buf);
+	return rc;
+}
diff --git a/src/ldso/aarch64/arch_ldso_hook.h b/src/ldso/aarch64/arch_ldso_hook.h
new file mode 100644
index 00000000..11581712
--- /dev/null
+++ b/src/ldso/aarch64/arch_ldso_hook.h
@@ -0,0 +1,21 @@
+#ifndef LDSO_AARCH64_ARCH_LDSO_HOOK_H_
+#define LDSO_AARCH64_ARCH_LDSO_HOOK_H_
+
+/* Optional arch hook for the dynamic loader.
+ * If ARCH_SUPPORTS_DL_ADD_PROTECTIONS is defined, this function may
+ * examine ELF headers and file sections to determine whether to add
+ * arch-specific mmap/mprotect flags.
+ *
+ * Arguments:
+ *   ph, eh (in)  – program and ELF headers for the object being loaded.
+ *   fd     (in)  – file descriptor for the mapped object.
+ *   prot   (out) – protection flags to OR into existing flags.
+ *   mask   (out) – bitmask; new flags in *prot are ORed into existing_flags
+ *                  only if (existing_flags & *mask).
+ *
+ * Returns 0 on sucess.
+ */
+unsigned dl_add_protections(const Elf64_Phdr *ph,
+	const Elf64_Ehdr *eh, int fd, unsigned *prot, unsigned *mask);
+
+#endif
-- 
2.51.0

Powered by blists - more mailing lists

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.