[PATCH v2] arm64: implement support for static call trampolines

Peter Zijlstra peterz at infradead.org
Thu Oct 29 06:40:07 EDT 2020


On Wed, Oct 28, 2020 at 07:41:14PM +0100, Ard Biesheuvel wrote:
> +/*
> + * The static call trampoline consists of one of the following sequences:
> + *
> + *      (A)           (B)           (C)           (D)           (E)
> + * 00: BTI  C        BTI  C        BTI  C        BTI  C        BTI  C
> + * 04: B    fn       NOP           NOP           NOP           NOP
> + * 08: RET           RET           ADRP X16, fn  ADRP X16, fn  ADRP X16, fn
> + * 0c: NOP           NOP           ADD  X16, fn  ADD  X16, fn  ADD  X16, fn
> + * 10:                             BR   X16      RET           NOP
> + * 14:                                                         ADRP X16, &fn
> + * 18:                                                         LDR  X16, [X16, &fn]
> + * 1c:                                                         BR   X16
> + *
> + * The architecture permits us to patch B instructions into NOPs or vice versa
> + * directly, but patching any other instruction sequence requires careful
> + * synchronization. Since branch targets may be out of range for ordinary
> + * immediate branch instructions, we may have to fall back to ADRP/ADD/BR
> + * sequences in some cases, which complicates things considerably; since any
> + * sleeping tasks may have been preempted right in the middle of any of these
> + * sequences, we have to carefully transform one into the other, and ensure
> + * that it is safe to resume execution at any point in the sequence for tasks
> + * that have already executed part of it.
> + *
> + * So the rules are:
> + * - we start out with (A) or (B)
> + * - a branch within immediate range can always be patched in at offset 0x4;
> + * - sequence (A) can be turned into (B) for NULL branch targets;
> + * - a branch outside of immediate range can be patched using (C), but only if
> + *   . the sequence being updated is (A) or (B), or
> + *   . the branch target address modulo 4k results in the same ADD opcode
> + *     (which could occur when patching the same far target a second time)
> + * - once we have patched in (C) we cannot go back to (A) or (B), so patching
> + *   in a NULL target now requires sequence (D);
> + * - if we cannot patch a far target using (C), we fall back to sequence (E),
> + *   which loads the function pointer from memory.
> + *
> + * If we abide by these rules, then the following must hold for tasks that were
> + * interrupted halfway through execution of the trampoline:
> + * - when resuming at offset 0x8, we can only encounter a RET if (B) or (D)
> + *   was patched in at any point, and therefore a NULL target is valid;
> + * - when resuming at offset 0xc, we are executing the ADD opcode that is only
> + *   reachable via the preceding ADRP, and which is patched in only a single
> + *   time, and is therefore guaranteed to be consistent with the ADRP target;
> + * - when resuming at offset 0x10, X16 must refer to a valid target, since it
> + *   is only reachable via a ADRP/ADD pair that is guaranteed to be consistent.
> + *
> + * Note that sequence (E) is only used when switching between multiple far
> + * targets, and that it is not a terminal degraded state.
> + */

Would it make things easier if your trampoline consisted of two complete
slots, between which you can flip?

Something like:

	0x00	B 0x24 / NOP
	0x04    < slot 1 >
		....
	0x20
	0x24	< slot 2 >
		....
	0x40

Then each (20 byte) slot can contain any of the variants above and you
can write the unused slot without stop-machine. Then, when the unused
slot is populated, flip the initial instruction (like a static-branch),
issue synchronize_rcu_tasks() and flip to using the other slot for next
time.

Alternatively, you can patch the call-sites to point to the alternative
trampoline slot, but that might be pushing things a bit.



More information about the linux-arm-kernel mailing list