// Copyright (c) Meta Platforms, Inc. and affiliates.
// SPDX-License-Identifier: LGPL-2.1-or-later

static struct drgn_error *parse_vmcoreinfo_u64(const char *value,
					       const char *newline, int base,
					       uint64_t *ret)
{
	errno = 0;
	char *end;
	*ret = strtoull(value, &end, base);
	if (errno == ERANGE) {
		return drgn_error_create(DRGN_ERROR_OVERFLOW,
					 "number in VMCOREINFO is too large");
	} else if (errno || end == value || end != newline) {
		return drgn_error_create(DRGN_ERROR_OTHER,
					 "number in VMCOREINFO is invalid");
	}
	return NULL;
}

// Linux kernel commit 905415ff3ffb ("lib/buildid: harden build ID parsing
// logic") (in v6.12) contains a bug that results in a garbage build ID in
// VMCOREINFO. It was fixed in the same patch series in commits de3ec364c3c3
// ("lib/buildid: add single folio-based file reader abstraction") and
// d4deb8242341 ("lib/buildid: take into account e_phoff when fetching program
// headers"). However, the broken commit was backported to several stable
// kernels. Some branches were fixed by "lib/buildid: Fix build ID parsing
// logic", but a couple reached their end-of-life while broken. See
// https://lore.kernel.org/all/20241104175256.2327164-1-jolsa@kernel.org/.
//
// The very sad workaround is to ignore the build ID based on a version check.
static void ignore_broken_vmcoreinfo_build_id(struct drgn_program *prog)
{
	char *p = (char *)prog->vmcoreinfo.osrelease;
	long major = strtol(p, &p, 10), minor = 0, patch = 0;
	if (*p == '.') {
		minor = strtol(p + 1, &p, 10);
		if (*p == '.')
			patch = strtol(p + 1, NULL, 10);
	}
	if ((major == 6 && minor == 11 && patch >= 3 && patch < 10)
	    || (major == 6 && minor == 10 && patch >= 14)
	    || (major == 6 && minor == 6 && patch >= 55 && patch < 63)
	    || (major == 6 && minor == 1 && patch >= 113 && patch < 119)
	    || (major == 5 && minor == 15 && patch >= 168))
		prog->vmcoreinfo.build_id_len = 0;
}

struct drgn_error *drgn_program_parse_vmcoreinfo(struct drgn_program *prog,
						 const char *desc,
						 size_t descsz)
{
	struct drgn_error *err;

	prog->vmcoreinfo.raw_size = descsz;
	prog->vmcoreinfo.raw = memdup(desc, descsz);
	if (!prog->vmcoreinfo.raw)
		return &drgn_enomem;
	for (const char *line = desc, *end = &desc[descsz], *newline;
	     (newline = memchr(line, '\n', end - line));
	     line = newline + 1) {
		const char *equals = memchr(line, '=', newline - line);
		if (!equals)
			continue;

		const char *value = equals + 1;
		@memswitch (line, equals - line)@
		@case "BUILD-ID"@
		{
			size_t build_id_len = (newline - value) / 2;
			if (build_id_len > sizeof(prog->vmcoreinfo.build_id)) {
				return drgn_error_create(DRGN_ERROR_OTHER,
							 "BUILD-ID in VMCOREINFO is too long");
			}
			if (!unhexlify(value, newline - value,
				       &prog->vmcoreinfo.build_id)) {
				return drgn_error_create(DRGN_ERROR_OTHER,
							 "couldn't parse BUILD-ID in VMCOREINFO");
			}
			prog->vmcoreinfo.build_id_len = build_id_len;
			break;
		}
		@case "CRASHTIME"@
			prog->vmcoreinfo.have_crashtime = true;
			break;
		@case "OSRELEASE"@
			if ((size_t)(newline - value) >=
			    sizeof(prog->vmcoreinfo.osrelease)) {
				return drgn_error_create(DRGN_ERROR_OTHER,
							 "OSRELEASE in VMCOREINFO is too long");
			}
			memcpy(prog->vmcoreinfo.osrelease, value,
			       newline - value);
			prog->vmcoreinfo.osrelease[newline - value] = '\0';
			break;
		@case "PAGESIZE"@
			err = parse_vmcoreinfo_u64(value, newline, 0,
						   &prog->vmcoreinfo.page_size);
			if (err)
				return err;
			break;
		@case "KERNELOFFSET"@
			err = parse_vmcoreinfo_u64(value, newline, 16,
						   &prog->vmcoreinfo.kaslr_offset);
			if (err)
				return err;
			break;
		@case "SYMBOL(swapper_pg_dir)"@
			err = parse_vmcoreinfo_u64(value, newline, 16,
						   &prog->vmcoreinfo.swapper_pg_dir);
			if (err)
				return err;
			break;
		@case "LENGTH(mem_section)"@
			err = parse_vmcoreinfo_u64(value, newline, 0,
						   &prog->vmcoreinfo.mem_section_length);
			if (err)
				return err;
			break;
		@case "NUMBER(pgtable_l5_enabled)"@
		{
			uint64_t tmp;
			err = parse_vmcoreinfo_u64(value, newline, 0, &tmp);
			if (err)
				return err;
			prog->vmcoreinfo.pgtable_l5_enabled = tmp;
			break;
		}
		@case "NUMBER(phys_base)"@
		{
			err = parse_vmcoreinfo_u64(value, newline, 0,
						   &prog->vmcoreinfo.phys_base);
			if (err)
				return err;
			prog->vmcoreinfo.have_phys_base = true;
			break;
		}
		@case "NUMBER(KERNELPACMASK)"@
			err = parse_vmcoreinfo_u64(value, newline, 16,
						   &prog->aarch64_insn_pac_mask);
			if (err)
				return err;
			break;
		@case "NUMBER(VA_BITS)"@
			err = parse_vmcoreinfo_u64(value, newline, 0,
						   &prog->vmcoreinfo.va_bits);
			if (err)
				return err;
			break;
		@case "NUMBER(TCR_EL1_T1SZ)"@
			err = parse_vmcoreinfo_u64(value, newline, 0,
						   &prog->vmcoreinfo.tcr_el1_t1sz);
			if (err)
				return err;
			break;
		@case "CONFIG_ARM_LPAE"@
			prog->vmcoreinfo.arm_lpae = value[0] == 'y';
			break;
		@endswitch@
	}
	if (!prog->vmcoreinfo.osrelease[0]) {
		return drgn_error_create(DRGN_ERROR_OTHER,
					 "VMCOREINFO does not contain valid OSRELEASE");
	}
	ignore_broken_vmcoreinfo_build_id(prog);
	if (!is_power_of_two(prog->vmcoreinfo.page_size)) {
		return drgn_error_create(DRGN_ERROR_OTHER,
					 "VMCOREINFO does not contain valid PAGESIZE");
	}
	prog->vmcoreinfo.page_shift = ctz(prog->vmcoreinfo.page_size);
	if (!prog->vmcoreinfo.swapper_pg_dir) {
		return drgn_error_create(DRGN_ERROR_OTHER,
					 "VMCOREINFO does not contain valid swapper_pg_dir");
	}
	// Everything else is optional.
	return NULL;
}
