root/scripts/gendwarfksyms/kabi.c
// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2024 Google LLC
 */

#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>

#include "gendwarfksyms.h"

#define KABI_RULE_SECTION ".discard.gendwarfksyms.kabi_rules"
#define KABI_RULE_VERSION "1"

/*
 * The rule section consists of four null-terminated strings per
 * entry:
 *
 *   1. version
 *      Entry format version. Must match KABI_RULE_VERSION.
 *
 *   2. type
 *      Type of the kABI rule. Must be one of the tags defined below.
 *
 *   3. target
 *      Rule-dependent target, typically the fully qualified name of
 *      the target DIE.
 *
 *   4. value
 *      Rule-dependent value.
 */
#define KABI_RULE_MIN_ENTRY_SIZE                                  \
        (/* version\0 */ 2 + /* type\0 */ 2 + /* target\0" */ 1 + \
         /* value\0 */ 1)
#define KABI_RULE_EMPTY_VALUE ""

/*
 * Rule: declonly
 * - For the struct/enum/union in the target field, treat it as a
 *   declaration only even if a definition is available.
 */
#define KABI_RULE_TAG_DECLONLY "declonly"

/*
 * Rule: enumerator_ignore
 * - For the enum_field in the target field, ignore the enumerator.
 */
#define KABI_RULE_TAG_ENUMERATOR_IGNORE "enumerator_ignore"

/*
 * Rule: enumerator_value
 * - For the fqn_field in the target field, set the value to the
 *   unsigned integer in the value field.
 */
#define KABI_RULE_TAG_ENUMERATOR_VALUE "enumerator_value"

/*
 * Rule: byte_size
 * - For the fqn_field in the target field, set the byte_size
 *   attribute to the value in the value field.
 */
#define KABI_RULE_TAG_BYTE_SIZE "byte_size"

/*
 * Rule: type_string
 * - For the type reference in the fqn field, use the type string
 *   in the value field.
 */
#define KABI_RULE_TAG_TYPE_STRING "type_string"

enum kabi_rule_type {
        KABI_RULE_TYPE_UNKNOWN,
        KABI_RULE_TYPE_DECLONLY,
        KABI_RULE_TYPE_ENUMERATOR_IGNORE,
        KABI_RULE_TYPE_ENUMERATOR_VALUE,
        KABI_RULE_TYPE_BYTE_SIZE,
        KABI_RULE_TYPE_TYPE_STRING,
};

#define RULE_HASH_BITS 7

struct rule {
        enum kabi_rule_type type;
        const char *target;
        const char *value;
        struct hlist_node hash;
};

/* { type, target } -> struct rule */
static HASHTABLE_DEFINE(rules, 1 << RULE_HASH_BITS);

static inline unsigned int rule_values_hash(enum kabi_rule_type type,
                                            const char *target)
{
        return hash_32(type) ^ hash_str(target);
}

static inline unsigned int rule_hash(const struct rule *rule)
{
        return rule_values_hash(rule->type, rule->target);
}

static inline const char *get_rule_field(const char **pos, ssize_t *left)
{
        const char *start = *pos;
        size_t len;

        if (*left <= 0)
                error("unexpected end of kABI rules");

        len = strnlen(start, *left) + 1;
        *pos += len;
        *left -= len;

        return start;
}

void kabi_read_rules(int fd)
{
        GElf_Shdr shdr_mem;
        GElf_Shdr *shdr;
        Elf_Data *rule_data = NULL;
        Elf_Scn *scn;
        Elf *elf;
        size_t shstrndx;
        const char *rule_str;
        ssize_t left;
        int i;

        const struct {
                enum kabi_rule_type type;
                const char *tag;
        } rule_types[] = {
                {
                        .type = KABI_RULE_TYPE_DECLONLY,
                        .tag = KABI_RULE_TAG_DECLONLY,
                },
                {
                        .type = KABI_RULE_TYPE_ENUMERATOR_IGNORE,
                        .tag = KABI_RULE_TAG_ENUMERATOR_IGNORE,
                },
                {
                        .type = KABI_RULE_TYPE_ENUMERATOR_VALUE,
                        .tag = KABI_RULE_TAG_ENUMERATOR_VALUE,
                },
                {
                        .type = KABI_RULE_TYPE_BYTE_SIZE,
                        .tag = KABI_RULE_TAG_BYTE_SIZE,
                },
                {
                        .type = KABI_RULE_TYPE_TYPE_STRING,
                        .tag = KABI_RULE_TAG_TYPE_STRING,
                },
        };

        if (!stable)
                return;

        if (elf_version(EV_CURRENT) != EV_CURRENT)
                error("elf_version failed: %s", elf_errmsg(-1));

        elf = elf_begin(fd, ELF_C_READ_MMAP, NULL);
        if (!elf)
                error("elf_begin failed: %s", elf_errmsg(-1));

        if (elf_getshdrstrndx(elf, &shstrndx) < 0)
                error("elf_getshdrstrndx failed: %s", elf_errmsg(-1));

        scn = elf_nextscn(elf, NULL);

        while (scn) {
                const char *sname;

                shdr = gelf_getshdr(scn, &shdr_mem);
                if (!shdr)
                        error("gelf_getshdr failed: %s", elf_errmsg(-1));

                sname = elf_strptr(elf, shstrndx, shdr->sh_name);
                if (!sname)
                        error("elf_strptr failed: %s", elf_errmsg(-1));

                if (!strcmp(sname, KABI_RULE_SECTION)) {
                        rule_data = elf_getdata(scn, NULL);
                        if (!rule_data)
                                error("elf_getdata failed: %s", elf_errmsg(-1));
                        break;
                }

                scn = elf_nextscn(elf, scn);
        }

        if (!rule_data) {
                debug("kABI rules not found");
                check(elf_end(elf));
                return;
        }

        rule_str = rule_data->d_buf;
        left = shdr->sh_size;

        if (left < KABI_RULE_MIN_ENTRY_SIZE)
                error("kABI rule section too small: %zd bytes", left);

        if (rule_str[left - 1] != '\0')
                error("kABI rules are not null-terminated");

        while (left > KABI_RULE_MIN_ENTRY_SIZE) {
                enum kabi_rule_type type = KABI_RULE_TYPE_UNKNOWN;
                const char *field;
                struct rule *rule;

                /* version */
                field = get_rule_field(&rule_str, &left);

                if (strcmp(field, KABI_RULE_VERSION))
                        error("unsupported kABI rule version: '%s'", field);

                /* type */
                field = get_rule_field(&rule_str, &left);

                for (i = 0; i < ARRAY_SIZE(rule_types); i++) {
                        if (!strcmp(field, rule_types[i].tag)) {
                                type = rule_types[i].type;
                                break;
                        }
                }

                if (type == KABI_RULE_TYPE_UNKNOWN)
                        error("unsupported kABI rule type: '%s'", field);

                rule = xmalloc(sizeof(*rule));

                rule->type = type;
                rule->target = xstrdup(get_rule_field(&rule_str, &left));
                rule->value = xstrdup(get_rule_field(&rule_str, &left));

                hash_add(rules, &rule->hash, rule_hash(rule));

                debug("kABI rule: type: '%s', target: '%s', value: '%s'", field,
                      rule->target, rule->value);
        }

        if (left > 0)
                warn("unexpected data at the end of the kABI rules section");

        check(elf_end(elf));
}

static char *get_enumerator_target(const char *fqn, const char *field)
{
        char *target = NULL;

        if (asprintf(&target, "%s %s", fqn, field) < 0)
                error("asprintf failed for '%s %s'", fqn, field);

        return target;
}

static struct rule *find_rule(enum kabi_rule_type type, const char *target)
{
        struct rule *rule;

        if (!stable)
                return NULL;
        if (!target || !*target)
                return NULL;

        hash_for_each_possible(rules, rule, hash,
                               rule_values_hash(type, target)) {
                if (rule->type == type && !strcmp(target, rule->target))
                        return rule;
        }

        return NULL;
}

static struct rule *find_enumerator_rule(enum kabi_rule_type type,
                                         const char *fqn, const char *field)
{
        struct rule *rule;
        char *target;

        if (!stable)
                return NULL;
        if (!fqn || !*fqn || !field || !*field)
                return NULL;

        target = get_enumerator_target(fqn, field);
        rule = find_rule(type, target);

        free(target);
        return rule;
}

bool kabi_is_declonly(const char *fqn)
{
        return !!find_rule(KABI_RULE_TYPE_DECLONLY, fqn);
}

static unsigned long get_ulong_value(const char *value)
{
        unsigned long result = 0;
        char *endptr = NULL;

        errno = 0;
        result = strtoul(value, &endptr, 10);

        if (errno || *endptr)
                error("invalid unsigned value '%s'", value);

        return result;
}

bool kabi_is_enumerator_ignored(const char *fqn, const char *field)
{
        return !!find_enumerator_rule(KABI_RULE_TYPE_ENUMERATOR_IGNORE, fqn,
                                      field);
}

bool kabi_get_enumerator_value(const char *fqn, const char *field,
                               unsigned long *value)
{
        struct rule *rule;

        rule = find_enumerator_rule(KABI_RULE_TYPE_ENUMERATOR_VALUE, fqn,
                                    field);
        if (rule) {
                *value = get_ulong_value(rule->value);
                return true;
        }

        return false;
}

bool kabi_get_byte_size(const char *fqn, unsigned long *value)
{
        struct rule *rule;

        rule = find_rule(KABI_RULE_TYPE_BYTE_SIZE, fqn);
        if (rule) {
                *value = get_ulong_value(rule->value);
                return true;
        }

        return false;
}

bool kabi_get_type_string(const char *type, const char **str)
{
        struct rule *rule;

        rule = find_rule(KABI_RULE_TYPE_TYPE_STRING, type);
        if (rule) {
                *str = rule->value;
                return true;
        }

        return false;
}

void kabi_free(void)
{
        struct hlist_node *tmp;
        struct rule *rule;

        hash_for_each_safe(rules, rule, tmp, hash) {
                free((void *)rule->target);
                free((void *)rule->value);
                free(rule);
        }

        hash_init(rules);
}