lib/sku: split vpd implementation into two versions

This splits the _get_vpd_value implementation into two different
versions, one for systems which support sysfs VPD, and one for older
devices which don't export VPD in CBMEM.

This allows mosys to stop shelling out to vpd_get_value on most
non-AUE devices (see the bug below for the context on why this is
necessary from a security perspective).

BUG=b:77594752
TEST=provided unit tests

Change-Id: Iec41860f2295d921dd81c1d2bfcfc8c1cb069ed7
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/mosys/+/2487440
Tested-by: Jack Rosenthal <jrosenth@chromium.org>
Reviewed-by: Paul Fagerburg <pfagerburg@chromium.org>
Commit-Queue: Jack Rosenthal <jrosenth@chromium.org>
diff --git a/include/lib/vpd.h b/include/lib/vpd.h
new file mode 100644
index 0000000..c05fcd4
--- /dev/null
+++ b/include/lib/vpd.h
@@ -0,0 +1,22 @@
+/* Copyright 2020 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+/**
+ * An arbitrary maximum value length which is more than sufficient for
+ * what mosys needs (whitelabel_tag, customization_id, and
+ * rlz_brand_code).
+ */
+#define VPD_MAX_VALUE_SIZE 256
+
+/**
+ * vpd_get_value() - Get a RO Vital Product Data (VPD) value.
+ *
+ * @value:	The name of the value to read (e.g., "whitelabel_tag").
+ *
+ * Returns: a newly allocated and null terminated buffer containing
+ * the value upon success, or NULL upon failure.  The caller is
+ * expected to free() the returned buffer.
+ */
+extern char *vpd_get_value(const char *value);
diff --git a/lib/meson.build b/lib/meson.build
index c39adad..4f1da89 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -11,3 +11,4 @@
 subdir('smbios')
 subdir('spd')
 subdir('string')
+subdir('vpd')
diff --git a/lib/misc/sku.c b/lib/misc/sku.c
index f8928f9..4f7ded1 100644
--- a/lib/misc/sku.c
+++ b/lib/misc/sku.c
@@ -4,9 +4,8 @@
  * found in the LICENSE file.
  */
 
-#include <ctype.h>
+#include <stdlib.h>
 #include <string.h>
-#include <unistd.h>
 
 #include "mosys/alloc.h"
 #include "mosys/globals.h"
@@ -16,57 +15,7 @@
 #include "lib/file.h"
 #include "lib/sku.h"
 #include "lib/string.h"
-
-/*
- * Strips the "end of line" character (\n) in string.
- */
-static void _strip_eol(char *str)
-{
-	char *newline = strchr(str, '\n');
-	if (newline)
-		*newline = '\0';
-}
-
-/*
- * Reads one stripped line from fp and close file.
- *
- * This is a helper utility for functions reading identifier files.
- */
-static char *_read_close_stripped_line(FILE *fp)
-{
-	char buffer[256];
-
-	if (!fp)
-		return NULL;
-
-	if (!fgets(buffer, sizeof(buffer), fp)) {
-		buffer[0] = '\0';
-	} else {
-		_strip_eol(buffer);
-	}
-	fclose(fp);
-
-	if (!*buffer)
-		return NULL;
-	return mosys_strdup(buffer);
-}
-
-/*
- * Reads and returns a VPD value.
- */
-static char *_get_vpd_value(const char *key_name)
-{
-	char command[PATH_MAX];
-	FILE *fp = NULL;
-	char *value;
-
-	snprintf(command, sizeof(command), "vpd_get_value %s", key_name);
-	command[sizeof(command) - 1] = '\0';
-
-	fp = popen(command, "r");
-	value = _read_close_stripped_line(fp);
-	return value;
-}
+#include "lib/vpd.h"
 
 /*
  * Extracts the SERIES part from VPD "customization_id".
@@ -79,7 +28,7 @@
 	char *customization_id;
 	char *series = NULL, *dash;
 
-	customization_id = _get_vpd_value("customization_id");
+	customization_id = vpd_get_value("customization_id");
 	if (!customization_id)
 		return NULL;
 
@@ -112,7 +61,7 @@
 	if (info && info->brand)
 		return mosys_strdup(info->brand);
 
-	return _get_vpd_value("rlz_brand_code");
+	return vpd_get_value("rlz_brand_code");
 }
 
 char *sku_get_chassis(struct platform_intf *intf)
@@ -159,7 +108,7 @@
 	char *customization_id;
 
 	/* Look for VPD first before looking into model */
-	customization_id = _get_vpd_value("customization_id");
+	customization_id = vpd_get_value("customization_id");
 	if (customization_id)
 		return customization_id;
 
@@ -178,7 +127,7 @@
 	if (info && info->customization)
 		return mosys_strdup(info->customization);
 
-	customization_id = _get_vpd_value("customization_id");
+	customization_id = vpd_get_value("customization_id");
 	if (customization_id)
 		return customization_id;
 
@@ -193,7 +142,7 @@
 {
 	const char *value;
 
-	value = _get_vpd_value("whitelabel_tag");
+	value = vpd_get_value("whitelabel_tag");
 	if (!value)
 		value = "";
 	return mosys_strdup(value);
diff --git a/lib/vpd/meson.build b/lib/vpd/meson.build
new file mode 100644
index 0000000..86289a2
--- /dev/null
+++ b/lib/vpd/meson.build
@@ -0,0 +1,7 @@
+if use_vpd_file_cache
+  libmosys_src += files('vpd_file_cache.c')
+else
+  libmosys_src += files('vpd_sysfs.c')
+endif
+
+unittest_src += files('vpd_unittest.c')
diff --git a/lib/vpd/vpd_file_cache.c b/lib/vpd/vpd_file_cache.c
new file mode 100644
index 0000000..fb9c7d0
--- /dev/null
+++ b/lib/vpd/vpd_file_cache.c
@@ -0,0 +1,113 @@
+/* Copyright 2020 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+/*
+ * VPD implementations for legacy platforms which don't support the
+ * sysfs VPD driver (VPD not cached in CBMEM by firmware).
+ *
+ * Note: as of October 2020, the list of non-AUE platforms which
+ * requires this is:
+ * - Baytrail devices (Rambi)
+ * - Braswell devices (Strago)
+ * - Broadwell devices (Auron, Jecht)
+ * - Veyron devices
+ *
+ * By today's published AUE dates, that means this code can be deleted
+ * in late 2022.
+ */
+
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "lib/vpd.h"
+#include "mosys/alloc.h"
+#include "mosys/log.h"
+
+char *vpd_get_value(const char *name)
+{
+	char result[VPD_MAX_VALUE_SIZE] = { 0 };
+	size_t result_len;
+	size_t result_offset = 0;
+	size_t bytes_to_read = sizeof(result) - 1;
+	int wstatus;
+	int pipefd[2];
+	pid_t pid;
+	const char *const argv[] = { "/usr/sbin/vpd_get_value", name, NULL };
+	char *const env[] = { NULL };
+
+	if (pipe(pipefd) < 0) {
+		lprintf(LOG_ERR, "%s: pipe() failed: %s\n", __func__,
+			strerror(errno));
+		return NULL;
+	}
+
+	pid = fork();
+	if (pid < 0) {
+		lprintf(LOG_ERR, "%s: fork() failed: %s\n", __func__,
+			strerror(errno));
+		close(pipefd[1]);
+		goto exit;
+	}
+
+	if (pid == 0) {
+		close(pipefd[0]);
+		if (dup2(pipefd[1], STDOUT_FILENO) < 0) {
+			lprintf(LOG_ERR, "%s: dup2() failed: %s\n", __func__,
+				strerror(errno));
+			exit(1);
+		}
+
+		execve(argv[0], (char * const *)argv, env);
+		lprintf(LOG_ERR, "%s: cannot exec %s: %s\n", __func__, argv[0],
+			strerror(errno));
+		exit(1);
+	}
+
+	close(pipefd[1]);
+	if (waitpid(pid, &wstatus, 0) < 0) {
+		lprintf(LOG_ERR, "%s: waitpid() failed: %s\n", __func__,
+			strerror(errno));
+		goto exit;
+	}
+
+	if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) {
+		lprintf(LOG_ERR, "%s: %s %s returned non-zero (wstatus=%d)\n",
+			__func__, argv[0], argv[1], wstatus);
+		goto exit;
+	}
+
+	while (bytes_to_read) {
+		ssize_t read_rv = read(pipefd[0], result + result_offset,
+				       bytes_to_read);
+
+		if (read_rv < 0) {
+			lprintf(LOG_ERR, "%s: read() failed: %s\n",
+				__func__, strerror(errno));
+			goto exit;
+		} else if (read_rv == 0) {
+			break;
+		} else {
+			result_offset += read_rv;
+			bytes_to_read -= read_rv;
+		}
+	}
+
+exit:
+	close(pipefd[0]);
+	result_len = strlen(result);
+
+	if (result_len) {
+		if (result[result_len - 1] == '\n')
+			result[result_len - 1] = '\0';
+		return mosys_strdup(result);
+	}
+
+	return NULL;
+}
diff --git a/lib/vpd/vpd_sysfs.c b/lib/vpd/vpd_sysfs.c
new file mode 100644
index 0000000..9ff06fc
--- /dev/null
+++ b/lib/vpd/vpd_sysfs.c
@@ -0,0 +1,26 @@
+/* Copyright 2020 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include <limits.h>
+#include <stdlib.h>
+
+#include "lib/file.h"
+#include "lib/vpd.h"
+#include "mosys/alloc.h"
+#include "mosys/log.h"
+
+#define VPD_SYSFS_BASE "/sys/firmware/vpd/ro"
+
+char *vpd_get_value(const char *name)
+{
+	char path[PATH_MAX];
+	char result[VPD_MAX_VALUE_SIZE];
+
+	snprintf(path, sizeof(path), VPD_SYSFS_BASE "/%s", name);
+	if (read_file(path, result, sizeof(result), LOG_DEBUG) < 0)
+		return NULL;
+
+	return mosys_strdup(result);
+}
diff --git a/lib/vpd/vpd_unittest.c b/lib/vpd/vpd_unittest.c
new file mode 100644
index 0000000..a60479f
--- /dev/null
+++ b/lib/vpd/vpd_unittest.c
@@ -0,0 +1,100 @@
+/* Copyright 2020 The Chromium OS Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <cmocka.h>
+
+#include "lib/file.h"
+#include "lib/math.h"
+#include "lib/vpd.h"
+#include "mosys/log.h"
+
+typeof(execve) __real_execve;
+typeof(execve) __wrap_execve;
+typeof(read_file) __real_read_file;
+typeof(read_file) __wrap_read_file;
+
+static struct {
+	const char *name;
+	const char *value;
+} fake_vpd_values[] = {
+	{ "customization_id", "fake_cid" },
+	{ "whitelabel_tag", "fake_wl" },
+	{ "rlz_brand_code", "ZZCR" },
+};
+
+int __wrap_execve(const char *prog, char *const argv[], char *const envp[])
+{
+	if (strcmp(prog, "/usr/sbin/vpd_get_value")) {
+		return __real_execve(prog, argv, envp);
+	}
+
+	if (!argv[1])
+		exit(1);
+
+	for (size_t i = 0; i < ARRAY_SIZE(fake_vpd_values); i++) {
+		if (!strcmp(fake_vpd_values[i].name, argv[1])) {
+			printf("%s\n", fake_vpd_values[i].value);
+			exit(0);
+		}
+	}
+
+	/* vpd_get_value exits zero even when the value does not exist */
+	exit(0);
+	__builtin_unreachable();
+}
+
+#define SYSFS_PREFIX "/sys/firmware/vpd/ro/"
+ssize_t __wrap_read_file(const char *path, char *buf, size_t buf_sz,
+			 enum log_levels lvl)
+{
+	if (strncmp(path, SYSFS_PREFIX, strlen(SYSFS_PREFIX))) {
+		return __real_read_file(path, buf, buf_sz, lvl);
+	}
+
+	const char *name = path + strlen(SYSFS_PREFIX);
+
+	for (size_t i = 0; i < ARRAY_SIZE(fake_vpd_values); i++) {
+		if (!strcmp(fake_vpd_values[i].name, name)) {
+			size_t out_size = strlen(fake_vpd_values[i].value) + 1;
+
+			if (out_size > buf_sz)
+				return -1;
+
+			memcpy(buf, fake_vpd_values[i].value, out_size);
+			return out_size;
+		}
+	}
+
+	return -1;
+}
+
+static void vpd_value_exists_test(void **state)
+{
+	assert_string_equal(vpd_get_value("customization_id"), "fake_cid");
+	assert_string_equal(vpd_get_value("whitelabel_tag"), "fake_wl");
+	assert_string_equal(vpd_get_value("rlz_brand_code"), "ZZCR");
+}
+
+static void vpd_value_not_exists_test(void **state)
+{
+	assert_null(vpd_get_value("does_not_exist"));
+}
+
+int main(void)
+{
+	const struct CMUnitTest tests[] = {
+		cmocka_unit_test(vpd_value_exists_test),
+		cmocka_unit_test(vpd_value_not_exists_test),
+	};
+
+	return cmocka_run_group_tests(tests, NULL, NULL);
+}