/*
 * DF-5075 trigger_nullhelper.c -- userspace semantics harness (NOT a kernel PoC).
 *
 * Mirrors, exactly, the C-level shape of the kernel defect chain:
 *
 *   (1) amdgpu kzalloc's struct bios_parser  ->  bp->cmd_helper == NULL
 *       [sys/dev/drm/amd/display/dc/bios/bios_parser.c:100]
 *
 *   (2) bios_parser construct() calls
 *       dal_bios_parser_init_cmd_tbl_helper(&bp->cmd_helper, dce_version)
 *       and IGNORES the returned bool  [bios_parser.c:2935-2936]
 *
 *   (3) for dce_version outside {DCE 8.0,8.1,8.3,10.0,11.0,11.2,11.22}
 *       -- i.e. DCE_VERSION_12_0 / DCN_VERSION_1_0 / DCN_VERSION_1_01 /
 *       DCE_VERSION_UNKNOWN -- the hub switch falls to default, leaves *h
 *       untouched (NULL) and returns false
 *       [sys/dev/drm/amd/display/dc/bios/command_table_helper.c:34-63]
 *
 *   (4) construct() continues and returns true; dc_construct succeeds
 *       (resource pools exist for DCE12/DCN: dc_resource.c:138-146);
 *       the first BIOS command at mode set does
 *       bp->cmd_helper->assign_control_parameter(...)  [command_table.c:204]
 *       -> load of a function pointer from NULL+offset -> page fault.
 *
 * This program reproduces the *C semantics* of that chain (steps 1-4) in an
 * unprivileged user process: expect the child to die with SIGSEGV.
 * Success criterion: exit code 42.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

/* shape of struct command_table_helper (first members, offsets matter) */
struct command_table_helper {
	int (*controller_id_to_atom)(int id, unsigned char *atom_id);   /* +0x00 */
	int (*encoder_action_to_atom)(int action);		      /* +0x08 */
	long (*encoder_mode_bp_to_atom)(int s, int audio);	      /* +0x10 */
	void (*engine_bp_to_atom)(int engine_id, unsigned *atom_id);    /* +0x18 */
	void (*assign_control_parameter)(const struct command_table_helper *h,
		void *control, void *ctrl_param);		      /* +0x20 */
	int (*clock_source_id_to_atom)(int id, unsigned *pll_id);	      /* +0x28 */
};

/* the hub: mirrors dal_bios_parser_init_cmd_tbl_helper() -- on an unhandled
 * version it returns 0 and leaves *h exactly as it was (NULL). */
static int init_cmd_tbl_helper(const struct command_table_helper **h, int dce_version)
{
	switch (dce_version) {
	case 80: case 81: case 83: case 100: case 110: case 112: case 1122:
		/* static const table omitted: handled versions would set *h */
		*h = (const struct command_table_helper *)(uintptr_t)0x1000;
		return 1;
	default:
		/* Unsupported DCE -- BREAK_TO_DEBUGGER() is a no-op in release */
		return 0;	/* *h NOT touched */
	}
}

int main(void)
{
	/* (1) kzalloc semantics: zero-filled storage */
	struct parser_shim {
		const struct command_table_helper *cmd_helper;  /* first member */
	} *bp = calloc(1, sizeof(*bp));
	struct bp_encoder_control { int dummy; } cntl = { 0 };
	struct dig_params { unsigned char raw[16]; } params = { 0 };
	int dce_version = 1200;		/* DCE_VERSION_12_0 shape: unhandled */
	pid_t pid;
	int st;

	if (bp == NULL) {
		perror("calloc");
		return 1;
	}
	printf("[*] kzalloc'd bios_parser: bp->cmd_helper = %p (NULL)\n",
	    (void *)bp->cmd_helper);

	pid = fork();
	if (pid < 0) {
		perror("fork");
		return 1;
	}
	if (pid == 0) {
		/* (2)+(3) the caller-side bug: return value IGNORED, *h left NULL */
		(void)init_cmd_tbl_helper(&bp->cmd_helper, dce_version);
		printf("[*] init_cmd_tbl_helper(dce_version=%d) returned; "
		    "return value ignored, cmd_helper = %p\n",
		    dce_version, (void *)bp->cmd_helper);
		fflush(stdout);
		/* (4) first command-table dispatch: unguarded indirect call --
		 * reads the member function pointer from NULL+0x20 */
		printf("[*] calling bp->cmd_helper->assign_control_parameter() "
		    "(member at +0x20)...\n");
		fflush(stdout);
		bp->cmd_helper->assign_control_parameter(bp->cmd_helper,
		    &cntl, &params);
		/* NOT REACHED if the semantics hold */
		printf("[-] call returned -- semantics NOT reproduced\n");
		exit(1);
	}
	if (waitpid(pid, &st, 0) < 0) {
		perror("waitpid");
		return 1;
	}
	if (WIFSIGNALED(st) && WTERMSIG(st) == SIGSEGV) {
		printf("[+] child died SIGSEGV at the indirect call through "
		    "the NULL vtable -- semantics chain (1)-(4) confirmed\n");
		return 42;
	}
	printf("[-] unexpected child status %#x\n", st);
	return 1;
}
