Skip to content

Commit 6e14e47

Browse files
arch/x86: Support for automatic shadow stacks
- No more need for special IRQ shadow stacks - just reuse the one created for z_interrupt_stacks; - Add the linker sections for the pairs of stack/shadow stack; - Support shadow stack arrays. Last item was a bit challenging: shadow stacks need to be initialised before use, and this is done statically for normal shadow stacks. To initialise the shadow stacks in the array, one needs how many entries it has. While a simple approach would use `LISTIFY` to them do the initialization on all entries, that is not possible as many stack arrays are created using expressions instead of literals, such as `CONFIG_MP_MAX_NUM_CPUS - 1`, which won't work with `LISTIFY`. Instead, this patch uses a script, `gen_static_shstk_array.py` that gathers all needed information and patches the ELF to initialize the stack arrays. Note that this needs to be done before any other operation on the ELF file that creates new representations, such as the .bin output. Signed-off-by: Ederson de Souza <ederson.desouza@intel.com>
1 parent d1b68c7 commit 6e14e47

File tree

10 files changed

+318
-27
lines changed

10 files changed

+318
-27
lines changed

arch/x86/CMakeLists.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,32 @@ endif()
7575

7676
if(CONFIG_X86_CET)
7777
zephyr_compile_options(-fcf-protection=full)
78+
79+
if(CONFIG_HW_SHADOW_STACK)
80+
set(GEN_SHSTK_ARRAY ${ZEPHYR_BASE}/arch/x86/gen_static_shstk_array.py)
81+
82+
if(CONFIG_X86_64)
83+
set(gen_shstk_array_arch_param --arch x86_64)
84+
85+
execute_process(
86+
COMMAND
87+
${PYTHON_EXECUTABLE}
88+
${GEN_SHSTK_ARRAY}
89+
--header-output ${PROJECT_BINARY_DIR}/include/generated/zephyr/x86_shstk.h
90+
--config ${DOTCONFIG}
91+
${gen_shstk_array_arch_param}
92+
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
93+
)
94+
else()
95+
set(gen_shstk_array_arch_param --arch x86)
96+
endif()
97+
98+
set_property(GLOBAL APPEND PROPERTY post_build_patch_elf_commands
99+
COMMAND
100+
${PYTHON_EXECUTABLE}
101+
${GEN_SHSTK_ARRAY}
102+
--kernel $<TARGET_FILE:${ZEPHYR_FINAL_EXECUTABLE}>
103+
${gen_shstk_array_arch_param}
104+
)
105+
endif()
78106
endif()

arch/x86/Kconfig

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -515,14 +515,6 @@ config X86_CET_SHADOW_STACK_ALIGNMENT
515515
help
516516
This option sets the alignment of the Shadow Stack.
517517

518-
config X86_CET_IRQ_SHADOW_STACK_SIZE
519-
int "IRQ Shadow Stack size"
520-
default 4096
521-
depends on HW_SHADOW_STACK
522-
help
523-
This option sets the size of the shadow stack used for interrupt
524-
and exception handling.
525-
526518
config X86_CET_VERIFY_KERNEL_SHADOW_STACK
527519
bool "Verify kernel shadow stack"
528520
depends on HW_SHADOW_STACK

arch/x86/core/cet.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <zephyr/logging/log_ctrl.h>
1717
#include <zephyr/logging/log.h>
1818

19+
#include <kernel_arch_data.h>
1920
LOG_MODULE_DECLARE(os, CONFIG_KERNEL_LOG_LEVEL);
2021

2122
#ifdef CONFIG_X86_64

arch/x86/core/ia32/irq_manage.c

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,6 @@
2424
#include <zephyr/arch/x86/ia32/segmentation.h>
2525
#include <kernel_arch_data.h>
2626

27-
#ifdef CONFIG_HW_SHADOW_STACK
28-
X86_IRQ_SHADOW_STACK_DEFINE(z_x86_irq_shadow_stack,
29-
CONFIG_X86_CET_IRQ_SHADOW_STACK_SIZE);
30-
#endif
31-
3227
#define TOKEN_OFFSET 4
3328

3429
extern void z_SpuriousIntHandler(void *handler);
@@ -54,8 +49,10 @@ void arch_isr_direct_footer_swap(unsigned int key)
5449
#ifdef CONFIG_HW_SHADOW_STACK
5550
void z_x86_set_irq_shadow_stack(void)
5651
{
57-
size_t stack_size = CONFIG_X86_CET_IRQ_SHADOW_STACK_SIZE;
58-
arch_thread_hw_shadow_stack_t *stack = z_x86_irq_shadow_stack;
52+
size_t stack_size = sizeof(__z_interrupt_stacks_shstk_arr);
53+
arch_thread_hw_shadow_stack_t *stack;
54+
55+
stack = (arch_thread_hw_shadow_stack_t *)__z_interrupt_stacks_shstk_arr;
5956

6057
_kernel.cpus[0].arch.shstk_addr = stack +
6158
(stack_size - TOKEN_OFFSET * sizeof(*stack)) / sizeof(*stack);

arch/x86/core/intel64/locore.S

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
#include <zephyr/drivers/interrupt_controller/loapic.h>
1515
#include <zephyr/arch/cpu.h>
1616
#include <zephyr/kernel/mm.h>
17+
#ifdef CONFIG_HW_SHADOW_STACK
18+
#include <x86_shstk.h>
19+
#endif
1720

1821
/*
1922
* Definitions/macros for enabling paging
@@ -823,7 +826,7 @@ irq:
823826
#ifdef CONFIG_HW_SHADOW_STACK
824827
pushq %rsi
825828
movq %gs:(__x86_tss64_t_shstk_addr_OFFSET), %rsi
826-
subq $CONFIG_X86_CET_IRQ_SHADOW_STACK_SIZE, (%rsi)
829+
subq $X86_CET_IRQ_SHADOW_SUBSTACK_SIZE, (%rsi)
827830
popq %rsi
828831
#endif
829832
cmpl $CONFIG_ISR_DEPTH, ___cpu_t_nested_OFFSET(%rsi)
@@ -981,7 +984,7 @@ irq_dispatch:
981984
addq $CONFIG_ISR_SUBSTACK_SIZE, %gs:__x86_tss64_t_ist1_OFFSET
982985
#ifdef CONFIG_HW_SHADOW_STACK
983986
movq %gs:(__x86_tss64_t_shstk_addr_OFFSET), %rax
984-
addq $CONFIG_X86_CET_IRQ_SHADOW_STACK_SIZE, (%rax)
987+
addq $X86_CET_IRQ_SHADOW_SUBSTACK_SIZE, (%rax)
985988
#endif
986989
decl ___cpu_t_nested_OFFSET(%rsi)
987990
jnz irq_exit_nested

arch/x86/gen_static_shstk_array.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2025 Intel Corporation
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
import argparse
8+
import struct
9+
10+
from elftools.elf.elffile import ELFFile
11+
from elftools.elf.sections import SymbolTableSection
12+
13+
14+
def parse_args():
15+
global args
16+
parser = argparse.ArgumentParser(
17+
description=__doc__,
18+
formatter_class=argparse.RawDescriptionHelpFormatter,
19+
allow_abbrev=False,
20+
)
21+
22+
parser.add_argument("-k", "--kernel", required=False, help="Zephyr kernel image")
23+
parser.add_argument("-o", "--header-output", required=False, help="Header output file")
24+
parser.add_argument("-c", "--config", required=False, help="Configuration file (.config)")
25+
parser.add_argument(
26+
"-a",
27+
"--arch",
28+
required=False,
29+
help="Architecture to generate shadow stack for",
30+
choices=["x86", "x86_64"],
31+
default="x86",
32+
)
33+
args = parser.parse_args()
34+
35+
36+
def get_symbols(obj):
37+
for section in obj.iter_sections():
38+
if isinstance(section, SymbolTableSection):
39+
return {sym.name: sym for sym in section.iter_symbols()}
40+
41+
raise LookupError("Could not find symbol table")
42+
43+
44+
shstk_irq_top_fmt = "<Q"
45+
46+
47+
def generate_initialized_irq_array64(sym, section_data, section_addr, isr_depth):
48+
offset = sym["st_value"] - section_addr
49+
# First four bytes have the number of members of the array
50+
nmemb = int.from_bytes(section_data[offset : offset + 4], "little") * isr_depth
51+
stack_size = (int)(sym["st_size"] / nmemb)
52+
53+
# Top of shadow stack is on the form:
54+
# [-1] = shadow stack supervisor token
55+
56+
output = bytearray(sym["st_size"])
57+
for i in range(nmemb):
58+
token = sym["st_value"] + stack_size * (i + 1) - 8
59+
60+
struct.pack_into(shstk_irq_top_fmt, output, stack_size * (i + 1) - 8, token)
61+
62+
return output
63+
64+
65+
shstk_top_fmt = "<QQQQ"
66+
67+
68+
def generate_initialized_array64(sym, section_data, section_addr, thread_entry):
69+
offset = sym["st_value"] - section_addr
70+
# First four bytes have the number of members of the array
71+
nmemb = int.from_bytes(section_data[offset : offset + 4], "little")
72+
if nmemb == 0:
73+
# CONFIG_DYNAMIC_THREAD_POOL_SIZE can be zero - in which case isn't
74+
# used. To allow the static initialization, we make the corresponding shadow stack
75+
# have one item. Here, we recognize it by having 0 members. But we already
76+
# wasted the space for this. Sigh...
77+
return bytearray(sym["st_size"])
78+
stack_size = (int)(sym["st_size"] / nmemb)
79+
80+
# Top of shadow stack is on the form:
81+
# [-5] = shadow stack token, pointing to [-4]
82+
# [-4] = previous SSP, pointing to [-1]
83+
# [-3] = z_thread_entry
84+
# [-2] = X86_KERNEL_CS
85+
86+
output = bytearray(sym["st_size"])
87+
for i in range(nmemb):
88+
end = sym["st_value"] + stack_size * (i + 1)
89+
token = end - 8 * 4 + 1
90+
prev_ssp = end - 8
91+
cs = 0x18 # X86_KERNEL_CS
92+
93+
struct.pack_into(
94+
shstk_top_fmt,
95+
output,
96+
stack_size * (i + 1) - 8 * 5,
97+
token,
98+
prev_ssp,
99+
thread_entry["st_value"],
100+
cs,
101+
)
102+
103+
return output
104+
105+
106+
shstk_top_fmt32 = "<QI"
107+
108+
109+
def generate_initialized_array32(sym, section_data, section_addr, thread_entry):
110+
offset = sym["st_value"] - section_addr
111+
# First four bytes have the number of members of the array
112+
nmemb = int.from_bytes(section_data[offset : offset + 4], "little")
113+
if nmemb == 0:
114+
# See comment on generate_initialized_array64
115+
return bytearray(sym["st_size"])
116+
stack_size = (int)(sym["st_size"] / nmemb)
117+
118+
# Top of shadow stack is on the form:
119+
# [-4] = shadow stack token, pointing to [-2]
120+
# [-3] = 0 - high order bits of token
121+
# [-2] = z_thread_entry/z_x86_thread_entry_wrapper
122+
123+
output = bytearray(sym["st_size"])
124+
for i in range(nmemb):
125+
end = sym["st_value"] + stack_size * (i + 1)
126+
token = end - 8
127+
128+
struct.pack_into(
129+
shstk_top_fmt32,
130+
output,
131+
stack_size * (i + 1) - 4 * 4,
132+
token,
133+
thread_entry["st_value"],
134+
)
135+
136+
return output
137+
138+
139+
def generate_initialized_array(sym, section_data, section_addr, thread_entry):
140+
if args.arch == "x86":
141+
return generate_initialized_array32(sym, section_data, section_addr, thread_entry)
142+
143+
return generate_initialized_array64(sym, section_data, section_addr, thread_entry)
144+
145+
146+
def patch_elf():
147+
with open(args.kernel, "r+b") as elf_fp:
148+
kernel = ELFFile(elf_fp)
149+
section = kernel.get_section_by_name(".x86shadowstack.arr")
150+
syms = get_symbols(kernel)
151+
thread_entry = syms["z_thread_entry"]
152+
if args.arch == "x86" and syms["CONFIG_X86_DEBUG_INFO"].entry.st_value:
153+
thread_entry = syms["z_x86_thread_entry_wrapper"]
154+
155+
updated_section = bytearray()
156+
shstk_arr_syms = [
157+
sym[1]
158+
for sym in syms.items()
159+
if sym[0].startswith("__") and sym[0].endswith("_shstk_arr")
160+
]
161+
shstk_arr_syms.sort(key=lambda x: x["st_value"])
162+
section_data = section.data()
163+
164+
for sym in shstk_arr_syms:
165+
if sym.name == "__z_interrupt_stacks_shstk_arr" and args.arch == "x86_64":
166+
isr_depth = syms["CONFIG_ISR_DEPTH"].entry.st_value
167+
out = generate_initialized_irq_array64(
168+
sym, section_data, section["sh_addr"], isr_depth
169+
)
170+
else:
171+
out = generate_initialized_array(
172+
sym, section_data, section["sh_addr"], thread_entry
173+
)
174+
175+
updated_section += out
176+
177+
elf_fp.seek(section["sh_offset"])
178+
elf_fp.write(updated_section)
179+
180+
181+
def generate_header():
182+
if args.config is None:
183+
raise ValueError("Configuration file is required to generate header")
184+
185+
isr_depth = stack_size = alignment = hw_stack_percentage = hw_stack_min_size = None
186+
187+
with open(args.config) as config_fp:
188+
config_lines = config_fp.readlines()
189+
190+
for line in config_lines:
191+
if line.startswith("CONFIG_ISR_DEPTH="):
192+
isr_depth = int(line.split("=")[1].strip())
193+
if line.startswith("CONFIG_ISR_STACK_SIZE="):
194+
stack_size = int(line.split("=")[1].strip())
195+
if line.startswith("CONFIG_X86_CET_SHADOW_STACK_ALIGNMENT="):
196+
alignment = int(line.split("=")[1].strip())
197+
if line.startswith("CONFIG_HW_SHADOW_STACK_PERCENTAGE_SIZE="):
198+
hw_stack_percentage = int(line.split("=")[1].strip())
199+
if line.startswith("CONFIG_HW_SHADOW_STACK_MIN_SIZE="):
200+
hw_stack_min_size = int(line.split("=")[1].strip())
201+
202+
if isr_depth is None:
203+
raise ValueError("Missing CONFIG_ISR_DEPTH in configuration file")
204+
if stack_size is None:
205+
raise ValueError("Missing CONFIG_ISR_STACK_SIZE in configuration file")
206+
if alignment is None:
207+
raise ValueError("Missing CONFIG_X86_CET_SHADOW_STACK_ALIGNMENT in configuration file")
208+
if hw_stack_percentage is None:
209+
raise ValueError("Missing CONFIG_HW_SHADOW_STACK_PERCENTAGE_SIZE in configuration file")
210+
if hw_stack_min_size is None:
211+
raise ValueError("Missing CONFIG_HW_SHADOW_STACK_MIN_SIZE in configuration file")
212+
213+
stack_size = int(stack_size * (hw_stack_percentage / 100))
214+
stack_size = int((stack_size + alignment - 1) / alignment) * alignment
215+
stack_size = max(stack_size, hw_stack_min_size)
216+
stack_size = int(stack_size / isr_depth)
217+
218+
with open(args.header_output, "w") as header_fp:
219+
header_fp.write(f"#define X86_CET_IRQ_SHADOW_SUBSTACK_SIZE {stack_size}\n")
220+
221+
222+
def main():
223+
parse_args()
224+
225+
if args.kernel is not None:
226+
patch_elf()
227+
228+
if args.header_output is not None:
229+
generate_header()
230+
231+
232+
if __name__ == "__main__":
233+
main()

arch/x86/include/intel64/kernel_arch_data.h

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ struct x86_interrupt_ssp_table {
4444
};
4545

4646
extern uint8_t x86_cpu_loapics[]; /* CPU logical ID -> local APIC ID */
47-
4847
#endif /* _ASMLANGUAGE */
4948

5049
#ifdef CONFIG_X86_KPTI
@@ -91,6 +90,9 @@ extern uint8_t x86_cpu_loapics[]; /* CPU logical ID -> local APIC ID */
9190
(uintptr_t)name + size * (i + 1) - 1 * \
9291
sizeof(arch_thread_hw_shadow_stack_t)
9392

93+
#define X86_EXCEPTION_SHADOW_STACK_SIZE \
94+
K_THREAD_HW_SHADOW_STACK_SIZE(CONFIG_X86_EXCEPTION_STACK_SIZE)
95+
9496
#define X86_IRQ_SHADOW_STACK_DEFINE(name, size) \
9597
arch_thread_hw_shadow_stack_t Z_GENERIC_SECTION(.x86shadowstack) \
9698
__aligned(CONFIG_X86_CET_SHADOW_STACK_ALIGNMENT) \
@@ -100,24 +102,24 @@ extern uint8_t x86_cpu_loapics[]; /* CPU logical ID -> local APIC ID */
100102
}
101103

102104
#define X86_INTERRUPT_SHADOW_STACK_DEFINE(n) \
103-
X86_IRQ_SHADOW_STACK_DEFINE(z_x86_irq_shadow_stack##n, \
104-
CONFIG_X86_CET_IRQ_SHADOW_STACK_SIZE); \
105105
X86_IRQ_SHADOW_STACK_DEFINE(z_x86_nmi_shadow_stack##n, \
106-
CONFIG_X86_CET_IRQ_SHADOW_STACK_SIZE); \
106+
X86_EXCEPTION_SHADOW_STACK_SIZE); \
107107
X86_IRQ_SHADOW_STACK_DEFINE(z_x86_exception_shadow_stack##n, \
108-
CONFIG_X86_CET_IRQ_SHADOW_STACK_SIZE)
108+
X86_EXCEPTION_SHADOW_STACK_SIZE)
109109

110-
#define SHSTK_LAST_ENTRY (CONFIG_X86_CET_IRQ_SHADOW_STACK_SIZE * \
110+
#define SHSTK_LAST_ENTRY (X86_EXCEPTION_SHADOW_STACK_SIZE * \
111111
CONFIG_ISR_DEPTH / sizeof(arch_thread_hw_shadow_stack_t) - 1)
112112

113+
#define IRQ_SHSTK_LAST_ENTRY sizeof(__z_interrupt_stacks_shstk_arr[0]) / \
114+
sizeof(arch_thread_hw_shadow_stack_t) - 1
115+
113116
#define X86_INTERRUPT_SSP_TABLE_INIT(n, _) \
114117
{ \
115-
.ist1 = (uintptr_t)&z_x86_irq_shadow_stack##n[SHSTK_LAST_ENTRY], \
118+
.ist1 = (uintptr_t)&__z_interrupt_stacks_shstk_arr[n][IRQ_SHSTK_LAST_ENTRY], \
116119
.ist6 = (uintptr_t)&z_x86_nmi_shadow_stack##n[SHSTK_LAST_ENTRY], \
117120
.ist7 = (uintptr_t)&z_x86_exception_shadow_stack##n[SHSTK_LAST_ENTRY], \
118121
}
119122

120-
121123
#define STACK_ARRAY_IDX(n, _) n
122124

123125
#define DEFINE_STACK_ARRAY_IDX\

0 commit comments

Comments
 (0)