From c98bf4d902821cfeb83cd6037a36a0bf208f842c Mon Sep 17 00:00:00 2001
From: Nathan Lynch <nathanl@linux.ibm.com>
Date: Mon, 25 Sep 2023 11:42:26 -0500
Subject: [PATCH 6/6] librtas/sysparm: prefer /dev/papr-sysparm when available

Change rtas_get_sysparm() and rtas_set_sysparm() to prefer the
/dev/papr-sysparm character device as proposed for the kernel here:

[[[ FIXME ]]]

On the first invocation of either function, probe for the new ABI and
initialize internal function pointers according to the result. Use
pthread_once() to ensure that this initialization step is performed
atomically and only once.

When /dev/papr-sysparm is available, use its PAPR_SYSPARM_IOC_GET and
PAPR_SYSPARM_IOC_SET ioctl commands to retrieve and update system
parameters. Most of the complexity of calling RTAS is handled
internally to the kernel and the ioctl follows Linux conventions,
returning 0 on success or -1 w/errno on failure. On failure,
back-convert the errno to an RTAS status value that callers will
recognize.

For now, carry a copy of the kernel uapi header.

Signed-off-by: Nathan Lynch <nathanl@linux.ibm.com>
---
 Makefile.am                |   1 +
 librtas_src/papr-sysparm.h |  58 +++++++++++++++
 librtas_src/sysparm.c      | 144 ++++++++++++++++++++++++++++++++++++-
 3 files changed, 201 insertions(+), 2 deletions(-)
 create mode 100644 librtas_src/papr-sysparm.h

diff --git a/Makefile.am b/Makefile.am
index d3513e9..29b3b76 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -38,6 +38,7 @@ library_include_HEADERS += librtas_src/librtas.h
 noinst_HEADERS += \
 	librtas_src/internal.h \
 	librtas_src/papr-miscdev.h \
+	librtas_src/papr-sysparm.h \
 	librtas_src/papr-vpd.h
 
 # See "Updating library version information" in the libtool manual for
diff --git a/librtas_src/papr-sysparm.h b/librtas_src/papr-sysparm.h
new file mode 100644
index 0000000..e0c0ebb
--- /dev/null
+++ b/librtas_src/papr-sysparm.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+#ifndef _UAPI_PAPR_SYSPARM_H_
+#define _UAPI_PAPR_SYSPARM_H_
+
+#include <linux/types.h>
+#include <asm/ioctl.h>
+#include "papr-miscdev.h"
+
+enum {
+	PAPR_SYSPARM_MAX_INPUT  = 1024,
+	PAPR_SYSPARM_MAX_OUTPUT = 4000,
+};
+
+struct papr_sysparm_io_block {
+	__u32 parameter;
+	__u16 length;
+	char data[PAPR_SYSPARM_MAX_OUTPUT];
+};
+
+/**
+ * PAPR_SYSPARM_IOC_GET - Retrieve the value of a PAPR system parameter.
+ *
+ * Uses _IOWR because of one corner case: Retrieving the value of the
+ * "OS Service Entitlement Status" parameter (60) requires the caller
+ * to supply input data (a date string) in the buffer passed to
+ * firmware. So the @length and @data of the incoming
+ * papr_sysparm_io_block are always used to initialize the work area
+ * supplied to ibm,get-system-parameter. No other parameters are known
+ * to parameterize the result this way, and callers are encouraged
+ * (but not required) to zero-initialize @length and @data in the
+ * common case.
+ *
+ * On error the contents of the ioblock are indeterminate.
+ *
+ * Return:
+ * 0: Success; @length is the length of valid data in @data, not to exceed @PAPR_SYSPARM_MAX_OUTPUT.
+ * -EIO: Platform error. (-1)
+ * -EINVAL: Incorrect data length or format. (-9999)
+ * -EPERM: The calling partition is not allowed to access this parameter. (-9002)
+ * -EOPNOTSUPP: Parameter not supported on this platform (-3)
+ */
+#define PAPR_SYSPARM_IOC_GET _IOWR(PAPR_MISCDEV_IOC_ID, 1, struct papr_sysparm_io_block)
+
+/**
+ * PAPR_SYSPARM_IOC_SET - Update the value of a PAPR system parameter.
+ *
+ * The contents of the ioblock are unchanged regardless of success.
+ *
+ * Return:
+ * 0: Success; the parameter has been updated.
+ * -EIO: Platform error. (-1)
+ * -EINVAL: Incorrect data length or format. (-9999)
+ * -EPERM: The calling partition is not allowed to access this parameter. (-9002)
+ * -EOPNOTSUPP: Parameter not supported on this platform (-3)
+ */
+#define PAPR_SYSPARM_IOC_SET _IOW(PAPR_MISCDEV_IOC_ID, 2, struct papr_sysparm_io_block)
+
+#endif /* _UAPI_PAPR_SYSPARM_H_ */
diff --git a/librtas_src/sysparm.c b/librtas_src/sysparm.c
index 40af55e..b4d2054 100644
--- a/librtas_src/sysparm.c
+++ b/librtas_src/sysparm.c
@@ -1,7 +1,16 @@
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdbool.h>
 #include <stdint.h>
 #include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
 #include "internal.h"
 #include "librtas.h"
+#include "papr-sysparm.h"
+
+static const char sysparm_devpath[] = "/dev/papr-sysparm";
 
 /**
  * rtas_get_sysparm
@@ -15,7 +24,8 @@
  * @param data reference to buffer to return parameter in
  * @return 0 on success, !0 otherwise
  */
-int rtas_get_sysparm(unsigned int parameter, unsigned int length, char *data)
+static int
+get_sysparm_syscall_fallback(unsigned int parameter, unsigned int length, char *data)
 {
 	uint32_t kernbuf_pa;
 	void *kernbuf;
@@ -49,7 +59,8 @@ int rtas_get_sysparm(unsigned int parameter, unsigned int length, char *data)
  * @param data
  * @return 0 on success, !0 otherwise
  */
-int rtas_set_sysparm(unsigned int parameter, char *data)
+static int
+set_sysparm_syscall_fallback(unsigned int parameter, char *data)
 {
 	uint32_t kernbuf_pa;
 	void *kernbuf;
@@ -86,3 +97,132 @@ int rtas_set_sysparm(unsigned int parameter, char *data)
 	dbg("(%u, %p) = %d\n", parameter, data, rc ? rc : status);
 	return rc ? rc : status;
 }
+
+static bool sysparm_can_use_chardev(void)
+{
+	struct stat statbuf;
+
+	if (stat(sysparm_devpath, &statbuf))
+		return false;
+
+	if (!S_ISCHR(statbuf.st_mode))
+		return false;
+
+	if (close(open(sysparm_devpath, O_RDONLY)))
+		return false;
+
+	return true;
+}
+
+/*
+ * Only to be used when converting an actual error from a syscall.
+ */
+static int chardev_backconvert_errno(int saved_errno)
+{
+	const struct {
+		int linux_errno;
+		int rtas_status;
+	} map[] = {
+#define errno_to_status(e, s) { .linux_errno = (e), .rtas_status = (s), }
+		errno_to_status(EINVAL,     -9999),
+		errno_to_status(EPERM,      -9002),
+		errno_to_status(EOPNOTSUPP,    -3),
+		errno_to_status(EIO,           -1),
+		errno_to_status(EFAULT,        -1),
+#undef errno_to_status
+	};
+
+	for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); ++i)
+		if (map[i].linux_errno == saved_errno)
+			return map[i].rtas_status;
+	return -1;
+}
+
+static int get_sysparm_chardev(unsigned int parameter, unsigned int length, char *data)
+{
+	const int fd = open(sysparm_devpath, O_RDWR);
+	struct papr_sysparm_io_block buf = {
+		.parameter = parameter,
+	};
+
+	if (fd < 0) {
+		/*
+		 * Really shouldn't get here without misconfiguration,
+		 * e.g. removal of /dev/papr-sysparm. Synthesize a
+		 * hardware/platform error.
+		 */
+		return -1;
+	}
+
+	/*
+	 * It might make sense to have special handling for parameter
+	 * 60 (OS Service Entitlement Status), which takes input data,
+	 * but librtas has never handled that one correctly. So ignore
+	 * it for now and don't copy incoming data into the block we
+	 * pass to PAPR_SYSPARM_IOC_GET.
+	 */
+
+	const int res = ioctl(fd, PAPR_SYSPARM_IOC_GET, &buf);
+	const int saved_errno = errno;
+	(void)close(fd);
+
+	if (res != 0)
+		return chardev_backconvert_errno(saved_errno);
+
+	const uint16_t result_size_msb = htobe16(buf.length);
+	memcpy(data, &result_size_msb, sizeof(result_size_msb));
+	length -= sizeof(result_size_msb);
+	data += sizeof(result_size_msb);
+
+	/*
+	 * Copy no more than min(@length, sizeof(buf.data)).
+	 */
+	const size_t copy_size = sizeof(buf.data) < length ?
+		sizeof(buf.data) : length;
+	memcpy(data, buf.data, copy_size);
+	return 0;
+}
+
+static int set_sysparm_chardev(unsigned int parameter, char *data)
+{
+	const int fd = open(sysparm_devpath, O_RDWR);
+	struct papr_sysparm_io_block buf = {
+		.parameter = parameter,
+		.length = (((unsigned char)data[0] << 8) | (unsigned char)data[1]),
+	};
+
+	memcpy(buf.data, data + 2, buf.length);
+
+	const int res = ioctl(fd, PAPR_SYSPARM_IOC_SET, &buf);
+	const int saved_errno = errno;
+	(void)close(fd);
+
+	return res == 0 ? 0 : chardev_backconvert_errno(saved_errno);
+}
+
+static int (*get_sysparm_fn)(unsigned int parameter, unsigned int length, char *data);
+static int (*set_sysparm_fn)(unsigned int parameter, char *data);
+
+static void sysparm_fn_setup(void)
+{
+	const bool use_chardev = sysparm_can_use_chardev();
+
+	get_sysparm_fn = use_chardev ?
+		get_sysparm_chardev : get_sysparm_syscall_fallback;
+	set_sysparm_fn = use_chardev ?
+		set_sysparm_chardev : set_sysparm_syscall_fallback;
+}
+
+static pthread_once_t sysparm_fn_setup_once = PTHREAD_ONCE_INIT;
+
+int rtas_get_sysparm(unsigned int parameter, unsigned int length, char *data)
+{
+	pthread_once(&sysparm_fn_setup_once, sysparm_fn_setup);
+	return get_sysparm_fn(parameter, length, data);
+}
+
+int rtas_set_sysparm(unsigned int parameter, char *data)
+{
+	pthread_once(&sysparm_fn_setup_once, sysparm_fn_setup);
+	return set_sysparm_fn(parameter, data);
+}
-- 
2.42.0

