diff --git a/tests/Makefile b/tests/Makefile
index d20ef23..95a9990 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -16,7 +16,7 @@ CFLAGS += $(BIN_CFLAGS) -Wno-unused-parameter $(W_MISSING_INITIALIZERS)
 LIBDEPS += -L. -L$(mpathcmddir) -lmultipath -lmpathcmd -lcmocka
 
 TESTS := uevent parser util dmevents hwtable blacklist unaligned vpd pgpolicy \
-	 alias directio valid devt mpathvalid strbuf
+	 alias directio valid devt mpathvalid strbuf sysfs
 HELPERS := test-lib.o test-log.o
 
 .SILENT: $(TESTS:%=%.o)
@@ -70,6 +70,9 @@ ifneq ($(DIO_TEST_DEV),)
 directio-test_LIBDEPS := -laio
 endif
 strbuf-test_OBJDEPS := ../libmultipath/strbuf.o
+sysfs-test_TESTDEPS := test-log.o
+sysfs-test_OBJDEPS := ../libmultipath/sysfs.o ../libmultipath/util.o
+sysfs-test_LIBDEPS := -ludev -lpthread -ldl
 
 %.o: %.c
 	$(CC) $(CPPFLAGS) $(CFLAGS) $($*-test_FLAGS) -c -o $@ $<
diff --git a/tests/sysfs.c b/tests/sysfs.c
new file mode 100644
index 0000000..0ec135b
--- /dev/null
+++ b/tests/sysfs.c
@@ -0,0 +1,494 @@
+/*
+ * Copyright (c) 2021 SUSE LLC
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#define _GNU_SOURCE
+#include <stdbool.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <stdlib.h>
+#include <cmocka.h>
+#include <libudev.h>
+#include <fcntl.h>
+#include <errno.h>
+#include "debug.h"
+#include "globals.c"
+#include "test-log.h"
+#include "sysfs.h"
+#include "util.h"
+
+#define TEST_FD 123
+
+char *__wrap_udev_device_get_syspath(struct udev_device *ud)
+{
+	char *val  = mock_ptr_type(char *);
+
+	return val;
+}
+
+int __wrap_open(const char *pathname, int flags)
+{
+	int ret;
+
+	check_expected(pathname);
+	check_expected(flags);
+	ret = mock_type(int);
+	return ret;
+}
+
+ssize_t __wrap_read(int fd, void *buf, size_t count)
+{
+	ssize_t ret;
+	char *val;
+
+	check_expected(fd);
+	check_expected(count);
+	ret = mock_type(int);
+	val = mock_ptr_type(char *);
+	if (ret >= (ssize_t)count)
+		ret = count;
+	if (ret >= 0 && val) {
+		fprintf(stderr, "%s: '%s' -> %zd\n", __func__, val, ret);
+		memcpy(buf, val, ret);
+	}
+	return ret;
+}
+
+ssize_t __wrap_write(int fd, void *buf, size_t count)
+{
+	ssize_t ret;
+
+	check_expected(fd);
+	check_expected(count);
+	ret = mock_type(int);
+	if (ret >= (ssize_t)count)
+		ret = count;
+	return ret;
+}
+
+int __real_close(int fd);
+int __wrap_close(int fd) {
+	if (fd != TEST_FD)
+		return __real_close(fd);
+	return mock_type(int);
+}
+
+static int setup(void **state)
+{
+	udev = udev_new();
+	return 0;
+}
+
+static int teardown(void **state)
+{
+	udev_unref(udev);
+	return 0;
+}
+
+static void expect_sagv_invalid(void)
+{
+	expect_condlog(1, "__sysfs_attr_get_value: invalid parameters");
+}
+
+static void test_sagv_invalid(void **state)
+{
+	expect_sagv_invalid();
+	assert_int_equal(sysfs_attr_get_value(NULL, NULL, NULL, 0), -EINVAL);
+	expect_sagv_invalid();
+	assert_int_equal(sysfs_bin_attr_get_value(NULL, NULL, NULL, 0), -EINVAL);
+
+	expect_sagv_invalid();
+	assert_int_equal(sysfs_attr_get_value(NULL, (void *)state, (void *)state, 1),
+			 -EINVAL);
+	expect_sagv_invalid();
+	assert_int_equal(sysfs_bin_attr_get_value(NULL, (void *)state, (void *)state, 1),
+			 -EINVAL);
+
+	expect_sagv_invalid();
+	assert_int_equal(sysfs_attr_get_value((void *)state, NULL, (void *)state, 1),
+			 -EINVAL);
+	expect_sagv_invalid();
+	assert_int_equal(sysfs_bin_attr_get_value((void *)state, NULL, (void *)state, 1),
+			 -EINVAL);
+
+	expect_sagv_invalid();
+	assert_int_equal(sysfs_attr_get_value((void *)state, (void *)state, NULL, 1),
+			 -EINVAL);
+	expect_sagv_invalid();
+	assert_int_equal(sysfs_bin_attr_get_value((void *)state, (void *)state, NULL, 1),
+			 -EINVAL);
+
+	expect_sagv_invalid();
+	assert_int_equal(sysfs_attr_get_value((void *)state, (void *)state,
+					      (void *)state, 0), -EINVAL);
+	expect_sagv_invalid();
+	assert_int_equal(sysfs_bin_attr_get_value((void *)state, (void *)state,
+						  (void *)state, 0), -EINVAL);
+}
+
+static void test_sagv_bad_udev(void **state)
+{
+	will_return(__wrap_udev_device_get_syspath, NULL);
+	expect_condlog(3, "__sysfs_attr_get_value: invalid udevice");
+	assert_int_equal(sysfs_attr_get_value((void *)state, (void *)state,
+					      (void *)state, 1), -EINVAL);
+
+	will_return(__wrap_udev_device_get_syspath, NULL);
+	expect_condlog(3, "__sysfs_attr_get_value: invalid udevice");
+	assert_int_equal(sysfs_bin_attr_get_value((void *)state, (void *)state,
+						  (void *)state, 1), -EINVAL);
+}
+
+static void test_sagv_bad_snprintf(void **state)
+{
+	char longstr[PATH_MAX + 1];
+	char buf[1];
+
+	memset(longstr, 'a', sizeof(longstr) - 1);
+	longstr[sizeof(longstr) - 1] = '\0';
+
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(3, "__sysfs_attr_get_value: devpath overflow");
+	assert_int_equal(sysfs_attr_get_value((void *)state, longstr,
+					      buf, sizeof(buf)), -EOVERFLOW);
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(3, "__sysfs_attr_get_value: devpath overflow");
+	assert_int_equal(sysfs_bin_attr_get_value((void *)state, longstr,
+						  (unsigned char *)buf, sizeof(buf)),
+			 -EOVERFLOW);
+}
+
+static void test_sagv_open_fail(void **state)
+{
+	char buf[1];
+
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(4, "open '/foo/bar'");
+	expect_string(__wrap_open, pathname, "/foo/bar");
+	expect_value(__wrap_open, flags, O_RDONLY);
+	errno = ENOENT;
+	will_return(__wrap_open, -1);
+	expect_condlog(3, "__sysfs_attr_get_value: attribute '/foo/bar' can not be opened");
+	assert_int_equal(sysfs_attr_get_value((void *)state, "bar",
+					      buf, sizeof(buf)), -ENOENT);
+}
+
+static void test_sagv_read_fail(void **state)
+{
+	char buf[1];
+
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(4, "open '/foo/bar'");
+	expect_string(__wrap_open, pathname, "/foo/bar");
+	expect_value(__wrap_open, flags, O_RDONLY);
+	will_return(__wrap_open, TEST_FD);
+	expect_value(__wrap_read, fd, TEST_FD);
+	expect_value(__wrap_read, count, sizeof(buf));
+	errno = EISDIR;
+	will_return(__wrap_read, -1);
+	will_return(__wrap_read, NULL);
+	expect_condlog(3, "__sysfs_attr_get_value: read from /foo/bar failed:");
+	will_return(__wrap_close, 0);
+	assert_int_equal(sysfs_attr_get_value((void *)state, "bar",
+					      buf, sizeof(buf)), -EISDIR);
+
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(4, "open '/foo/baz'");
+	expect_string(__wrap_open, pathname, "/foo/baz");
+	expect_value(__wrap_open, flags, O_RDONLY);
+	will_return(__wrap_open, TEST_FD);
+	expect_value(__wrap_read, fd, TEST_FD);
+	expect_value(__wrap_read, count, sizeof(buf));
+	errno = EPERM;
+	will_return(__wrap_read, -1);
+	will_return(__wrap_read, NULL);
+	expect_condlog(3, "__sysfs_attr_get_value: read from /foo/baz failed:");
+	will_return(__wrap_close, 0);
+	assert_int_equal(sysfs_bin_attr_get_value((void *)state, "baz",
+						  (unsigned char *)buf, sizeof(buf)),
+			 -EPERM);
+
+}
+
+static void _test_sagv_read(void **state, unsigned int bufsz)
+{
+	char buf[16];
+	char input[] = "01234567";
+	unsigned int n, trunc;
+
+	assert_in_range(bufsz, 1, sizeof(buf));
+	memset(buf, '.', sizeof(buf));
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(4, "open '/foo/bar'");
+	expect_string(__wrap_open, pathname, "/foo/bar");
+	expect_value(__wrap_open, flags, O_RDONLY);
+	will_return(__wrap_open, TEST_FD);
+	expect_value(__wrap_read, fd, TEST_FD);
+	expect_value(__wrap_read, count, bufsz);
+	will_return(__wrap_read, sizeof(input) - 1);
+	will_return(__wrap_read, input);
+
+	/* If the buffer is too small, input will be truncated by a 0 byte */
+	if (bufsz <= sizeof(input) - 1) {
+		n = bufsz;
+		trunc = 1;
+		expect_condlog(3, "__sysfs_attr_get_value: overflow reading from /foo/bar");
+	} else {
+		n = sizeof(input) - 1;
+		trunc = 0;
+	}
+	will_return(__wrap_close, 0);
+	assert_int_equal(sysfs_attr_get_value((void *)state, "bar",
+					      buf, bufsz), n);
+	assert_memory_equal(buf, input, n - trunc);
+	assert_int_equal(buf[n - trunc], '\0');
+
+	/* Binary input is not truncated */
+	memset(buf, '.', sizeof(buf));
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(4, "open '/foo/baz'");
+	expect_string(__wrap_open, pathname, "/foo/baz");
+	expect_value(__wrap_open, flags, O_RDONLY);
+	will_return(__wrap_open, TEST_FD);
+	expect_value(__wrap_read, fd, TEST_FD);
+	expect_value(__wrap_read, count, bufsz);
+	will_return(__wrap_read, sizeof(input) - 1);
+	will_return(__wrap_read, input);
+	will_return(__wrap_close, 0);
+	n = bufsz < sizeof(input) - 1 ? bufsz : sizeof(input) - 1;
+	assert_int_equal(sysfs_bin_attr_get_value((void *)state, "baz",
+						  (unsigned char *)buf,
+						  bufsz),
+			 n);
+	assert_memory_equal(buf, input, n);
+}
+
+static void test_sagv_read_overflow_8(void **state)
+{
+	_test_sagv_read(state, 8);
+}
+
+static void test_sagv_read_overflow_4(void **state)
+{
+	_test_sagv_read(state, 4);
+}
+
+static void test_sagv_read_overflow_1(void **state)
+{
+	_test_sagv_read(state, 1);
+}
+
+static void test_sagv_read_good_9(void **state)
+{
+	_test_sagv_read(state, 9);
+}
+
+static void test_sagv_read_good_15(void **state)
+{
+	_test_sagv_read(state, 15);
+}
+
+static void _test_sagv_read_zeroes(void **state, unsigned int bufsz)
+{
+	char buf[16];
+	char input[] = { '\0','\0','\0','\0','\0','\0','\0','\0' };
+	unsigned int n;
+
+	assert_in_range(bufsz, 1, sizeof(buf));
+	memset(buf, '.', sizeof(buf));
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(4, "open '/foo/bar'");
+	expect_string(__wrap_open, pathname, "/foo/bar");
+	expect_value(__wrap_open, flags, O_RDONLY);
+	will_return(__wrap_open, TEST_FD);
+	expect_value(__wrap_read, fd, TEST_FD);
+	expect_value(__wrap_read, count, bufsz);
+	will_return(__wrap_read, sizeof(input) - 1);
+	will_return(__wrap_read, input);
+
+	if (bufsz <= sizeof(input) - 1) {
+		n = bufsz;
+		expect_condlog(3, "__sysfs_attr_get_value: overflow reading from /foo/bar");
+	} else
+		n = 0;
+
+	will_return(__wrap_close, 0);
+	assert_int_equal(sysfs_attr_get_value((void *)state, "bar",
+					      buf, bufsz), n);
+
+	/*
+	 * The return value of sysfs_attr_get_value ignores zero bytes,
+	 * but the read data should have been copied to the buffer
+	 */
+	assert_memory_equal(buf, input, n == 0 ? bufsz : n);
+}
+
+static void test_sagv_read_zeroes_4(void **state)
+{
+	_test_sagv_read_zeroes(state, 4);
+}
+
+static void expect_sasv_invalid(void)
+{
+	expect_condlog(1, "sysfs_attr_set_value: invalid parameters");
+}
+
+static void test_sasv_invalid(void **state)
+{
+	expect_sasv_invalid();
+	assert_int_equal(sysfs_attr_set_value(NULL, NULL, NULL, 0), -EINVAL);
+
+	expect_sasv_invalid();
+	assert_int_equal(sysfs_attr_set_value(NULL, (void *)state, (void *)state, 1),
+			 -EINVAL);
+
+	expect_sasv_invalid();
+	assert_int_equal(sysfs_attr_set_value((void *)state, NULL, (void *)state, 1),
+			 -EINVAL);
+
+	expect_sasv_invalid();
+	assert_int_equal(sysfs_attr_set_value((void *)state, (void *)state, NULL, 1),
+			 -EINVAL);
+
+	expect_sasv_invalid();
+	assert_int_equal(sysfs_attr_set_value((void *)state, (void *)state,
+					      (void *)state, 0), -EINVAL);
+}
+
+static void test_sasv_bad_udev(void **state)
+{
+	will_return(__wrap_udev_device_get_syspath, NULL);
+	expect_condlog(3, "sysfs_attr_set_value: invalid udevice");
+	assert_int_equal(sysfs_attr_set_value((void *)state, (void *)state,
+					      (void *)state, 1), -EINVAL);
+}
+
+static void test_sasv_bad_snprintf(void **state)
+{
+	char longstr[PATH_MAX + 1];
+	char buf[1];
+
+	memset(longstr, 'a', sizeof(longstr) - 1);
+	longstr[sizeof(longstr) - 1] = '\0';
+
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(3, "sysfs_attr_set_value: devpath overflow");
+	assert_int_equal(sysfs_attr_set_value((void *)state, longstr,
+					      buf, sizeof(buf)), -EOVERFLOW);
+}
+
+static void test_sasv_open_fail(void **state)
+{
+	char buf[1];
+
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(4, "open '/foo/bar'");
+	expect_string(__wrap_open, pathname, "/foo/bar");
+	expect_value(__wrap_open, flags, O_WRONLY);
+	errno = EPERM;
+	will_return(__wrap_open, -1);
+	expect_condlog(3, "sysfs_attr_set_value: attribute '/foo/bar' can not be opened");
+	assert_int_equal(sysfs_attr_set_value((void *)state, "bar",
+					      buf, sizeof(buf)), -EPERM);
+}
+
+static void test_sasv_write_fail(void **state)
+{
+	char buf[1];
+
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(4, "open '/foo/bar'");
+	expect_string(__wrap_open, pathname, "/foo/bar");
+	expect_value(__wrap_open, flags, O_WRONLY);
+	will_return(__wrap_open, TEST_FD);
+	expect_value(__wrap_write, fd, TEST_FD);
+	expect_value(__wrap_write, count, sizeof(buf));
+	errno = EISDIR;
+	will_return(__wrap_write, -1);
+	expect_condlog(3, "sysfs_attr_set_value: write to /foo/bar failed:");
+	will_return(__wrap_close, 0);
+	assert_int_equal(sysfs_attr_set_value((void *)state, "bar",
+					      buf, sizeof(buf)), -EISDIR);
+
+}
+
+static void _test_sasv_write(void **state, unsigned int n_written)
+{
+	char buf[8];
+
+	assert_in_range(n_written, 0, sizeof(buf));
+	will_return(__wrap_udev_device_get_syspath, "/foo");
+	expect_condlog(4, "open '/foo/bar'");
+	expect_string(__wrap_open, pathname, "/foo/bar");
+	expect_value(__wrap_open, flags, O_WRONLY);
+	will_return(__wrap_open, TEST_FD);
+	expect_value(__wrap_write, fd, TEST_FD);
+	expect_value(__wrap_write, count, sizeof(buf));
+	will_return(__wrap_write, n_written);
+
+	if (n_written < sizeof(buf))
+		expect_condlog(3, "sysfs_attr_set_value: underflow writing");
+	will_return(__wrap_close, 0);
+	assert_int_equal(sysfs_attr_set_value((void *)state, "bar",
+					      buf, sizeof(buf)),
+			 n_written);
+}
+
+static void test_sasv_write_0(void **state)
+{
+	_test_sasv_write(state, 0);
+}
+
+static void test_sasv_write_4(void **state)
+{
+	_test_sasv_write(state, 4);
+}
+
+static void test_sasv_write_7(void **state)
+{
+	_test_sasv_write(state, 7);
+}
+
+static void test_sasv_write_8(void **state)
+{
+	_test_sasv_write(state, 8);
+}
+
+static int test_sysfs(void)
+{
+	const struct CMUnitTest tests[] = {
+		cmocka_unit_test(test_sagv_invalid),
+		cmocka_unit_test(test_sagv_bad_udev),
+		cmocka_unit_test(test_sagv_bad_snprintf),
+		cmocka_unit_test(test_sagv_open_fail),
+		cmocka_unit_test(test_sagv_read_fail),
+		cmocka_unit_test(test_sagv_read_overflow_1),
+		cmocka_unit_test(test_sagv_read_overflow_4),
+		cmocka_unit_test(test_sagv_read_overflow_8),
+		cmocka_unit_test(test_sagv_read_good_9),
+		cmocka_unit_test(test_sagv_read_good_15),
+		cmocka_unit_test(test_sagv_read_zeroes_4),
+		cmocka_unit_test(test_sasv_invalid),
+		cmocka_unit_test(test_sasv_bad_udev),
+		cmocka_unit_test(test_sasv_bad_snprintf),
+		cmocka_unit_test(test_sasv_open_fail),
+		cmocka_unit_test(test_sasv_write_fail),
+		cmocka_unit_test(test_sasv_write_0),
+		cmocka_unit_test(test_sasv_write_4),
+		cmocka_unit_test(test_sasv_write_7),
+		cmocka_unit_test(test_sasv_write_8),
+	};
+
+	return cmocka_run_group_tests(tests, setup, teardown);
+}
+
+int main(void)
+{
+	int ret = 0;
+
+	init_test_verbosity(4);
+	ret += test_sysfs();
+	return ret;
+}
