2025-01-06 - https://www.youtube.com/watch?v=pJ8EyNFg9Dk

Context

I was working on building a structure to represent every function a traceable function calls in the Linux kernel (this does not cover functions that make indirect calls by function pointers, etc), by processing the kernels ELF. This was being done to compare what they call to a trace obtained by the ftrace function_graph tracer, as I wanted to find functions that occur only with hooks.

The problem was that there was functions appearing in the trace that my code was missing, mostly from kmem related functions and calling things like __memcg_slab_free_hook . It turns out this was due to a feature in the kernel called static keys.

What?

This is a feature in the Linux kernel to allow features to be turned on / off in hot paths. The core files for implementation are:

Basically, two struct we care about which are __jump_table which consists of a bunch of struct jump_entry's, which point to a struct static_key, where the patch should be applied and the address to apply. They get defined in the kernel via a few macros, and areas which can get patched are setup via the static_branch_likely()/static_branch_unlikely() macros.

There is some logic to decide if it should be NOP’d out, or a branch instruction based on if its enabled (defined in the static_key struct) and if its a likely/unlikely branch (as part of the lower bits of jump_entry.key )

Applying it to an ELF on disk

On the Ubuntu 24.04 kernel i checked, the section __jump_table section didn’t exist (before and after processing with vmlinux-to-elf), so I had to use the symbol table to find the start and end.

This python function used radare2 via r2pipe to apply every static_key that should be enabled (this assumes you have ran .is* beforehand):

def chunks(lst, n):
    """Yield successive n-sized chunks from lst."""
    for i in range(0, len(lst), n):
        yield lst[i:i + n]

def fix_jump_table(r2):
    table = r2.cmdj(
        'pxj sym.__stop___jump_table - sym.__start___jump_table'
        ' @ sym.__start___jump_table'
    )

    base_addr = int(r2.cmd('?v sym.__start___jump_table'), 16)

    # I found i had to enable these on my ubuntu machine to make sure the paths
    # in the binary are correct.
    # though we have a problem as we kinda just need to guess these by trial
    # and error / seeing what errors we are getting on a clean trace.
    r2.cmd('wx 0x01 @ sym.memcg_kmem_online_key+8')
    r2.cmd('wx 0x01 @ sym.cgroup_bpf_enabled_key + 4 * 16 + 8')

    for idx, chunk in enumerate(chunks(table, 16)):
        code, target, key = struct.unpack('<iiq', bytes(chunk))
        real_code = base_addr + idx * 16 + code
        real_target = base_addr + idx * 16 + 4 + target
        offset = key & ~3
        real_key = base_addr + idx * 16 + 8 + offset

        static_key = r2.cmdj(f'pxj 16 @ {real_key}')
        is_enabled = static_key[8] & 1
        is_branch = key & 1 == 1
        # is_init = key & 2 == 2

        # there is a logic table on how to decide what we do here.
        encoding = -128 <= (real_target - real_code) <= 127
        ins = {True: 'wx 6690', False: 'wx 0f1f440000'}[encoding]

        if is_branch:
            if is_enabled:
                r2.cmd(f'{ins} @ {real_code}')
            else:
                r2.cmd(f'wa jmp {real_target} @ {real_code}')
        else:
            if is_enabled:
                r2.cmd(f'wa jmp {real_target} @ {real_code}')
            else:
                r2.cmd(f'{ins} @ {real_code}')

Problem remains determining which static keys are actually enabled on a box. I’d probably just write a kernel module to expose it.