livepatchでftraceのハンドラでセットしたInstruction Pointerがどう使われるのか調べた

Linuxのlivepatchはftraceを使って古い関数へのアクセスをフックして新しい関数を呼ぶようにしているというのは以前の記事で調べたんですが、 じゃあ、セットしたIP(Instruction Pointer)をどのように使って新しい関数に飛ばしているのか?というのが知りたかったことです。

ftraceにセットするハンドラの関数はkernel/livepatch/core.cにあるklp_ftrace_handler()で325行目から332行目までで呼び出したい関数のアドレスを取得します。

317 static void notrace klp_ftrace_handler(unsigned long ip,
318                                        unsigned long parent_ip,
319                                        struct ftrace_ops *fops,
320                                        struct pt_regs *regs)
321 {
322         struct klp_ops *ops;
323         struct klp_func *func;
324 
325         ops = container_of(fops, struct klp_ops, fops);
326 
327         rcu_read_lock();
328         func = list_first_or_null_rcu(&ops->func_stack, struct klp_func,
329                                       stack_node);
330         if (WARN_ON_ONCE(!func))
331                 goto unlock;
332 
333         klp_arch_set_pc(regs, (unsigned long)func->new_func);
334 unlock:
335         rcu_read_unlock();
336 }

そして、333行目で呼んでいるklp_arch_set_pc()にてstruct pt_regsのipをfunc->new_funcに設定します。

 38 static inline void klp_arch_set_pc(struct pt_regs *regs, unsigned long ip)
 39 {
 40         regs->ip = ip;
 41 }

ここまでは特に問題なくて、じゃあ、regs->ipをどのように使ってnew_funcを呼んでいるのかというのが今回のポイントです。 そして、調べたところ答えは平松さんがylugの第110回で発表したkpatchのスライドにありました。 リンク先のスライドを見ていただければ分かるように、ハンドラの呼び出しから戻った後にスタックフレームにある戻りアドレスをregs->ipに変えているようです。

そこでklp_ftrace_handler()にWARN_ON_ONCE()を入れてスタックトレースを取ったのがこちらです。使ったのはlivepatchのサンプルコードそのままです。

[  171.642145] Call Trace:
[  171.642149]  [<ffffffff81588791>] dump_stack+0x4c/0x6e
[  171.642151]  [<ffffffff810789fa>] warn_slowpath_common+0x8a/0xc0
[  171.642164]  [<ffffffff8124ff10>] ? cmdline_proc_open+0x20/0x20
[  171.642167]  [<ffffffff812037a5>] ? seq_read+0xf5/0x3d0
[  171.642169]  [<ffffffff81078b2a>] warn_slowpath_null+0x1a/0x20
[  171.642171]  [<ffffffff810dfb01>] klp_ftrace_handler+0xb1/0xc0
[  171.642173]  [<ffffffff81124810>] ftrace_ops_list_func+0xb0/0x180
[  171.642177]  [<ffffffff81590425>] ftrace_regs_call+0x5/0x72
[  171.642178]  [<ffffffff8124ff15>] ? cmdline_proc_show+0x5/0x30
[  171.642180]  [<ffffffff8124ff15>] cmdline_proc_show+0x5/0x30
[  171.642181]  [<ffffffff812037a5>] seq_read+0xf5/0x3d0
[  171.642183]  [<ffffffff8124ff15>] ? cmdline_proc_show+0x5/0x30
[  171.642184]  [<ffffffff812037a5>] ? seq_read+0xf5/0x3d0
[  171.642187]  [<ffffffff81247fe8>] proc_reg_read+0x48/0x70
[  171.642190]  [<ffffffff811de617>] __vfs_read+0x37/0x100
[  171.642193]  [<ffffffff8128204a>] ? security_file_permission+0x8a/0xa0
[  171.642195]  [<ffffffff811def0a>] vfs_read+0x8a/0x140
[  171.642197]  [<ffffffff811dfd69>] SyS_read+0x59/0xd0
[  171.642199]  [<ffffffff81066f97>] ? trace_do_page_fault+0x37/0xf0
[  171.642201]  [<ffffffff8158df2e>] system_call_fastpath+0x12/0x71
[  171.642202] ---[ end trace 19cacbab0add2e74 ]---

patch対象のcmdline_proc_show()が呼ばれた後にftrace_regs_call()が呼ばれてますね。 ftrace_regs_call()はアセンブラの関数でarch/x86/kernel/mcount_64.Sにあります。

222 GLOBAL(ftrace_regs_call)
223         call ftrace_stub
224 
225         /* Copy flags back to SS, to restore them */
226         movq EFLAGS(%rsp), %rax
227         movq %rax, MCOUNT_REG_SIZE(%rsp)
228 
229         /* Handlers can change the RIP */
230         movq RIP(%rsp), %rax
231         movq %rax, MCOUNT_REG_SIZE+8(%rsp)
232 
233         /* restore the rest of pt_regs */
234         movq R15(%rsp), %r15
235         movq R14(%rsp), %r14
236         movq R13(%rsp), %r13
237         movq R12(%rsp), %r12
238         movq R10(%rsp), %r10
239         movq RBX(%rsp), %rbx
240 
241         restore_mcount_regs
242 
243         /* Restore flags */
244         popfq
245 
246         /*
247          * As this jmp to ftrace_return can be a short jump
248          * it must not be copied into the trampoline.
249          * The trampoline will add the code to jump
250          * to the return.
251          */
252 GLOBAL(ftrace_regs_caller_end)
253 
254         jmp ftrace_return

スライドのソースから多少変わっていますが230、231行目がリターンアドレスを書き換えているところでしょうか。 それプラス、その他のレジスタを元に戻してあげるとリターンアドレスが新しい関数のアドレスになっているで、そちらに遷移しかつレジスタはその関数を呼ぶのに適切な設定に戻っているのでなんら問題なく新しい関数にたどり着いて処理が続行できるということですね。