From d73118f55879d81c4ede2b5a69b0d3b079ceb710 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Thu, 14 Dec 2023 12:29:47 +0100 Subject: [PATCH] Record and restore SELinux context for mocked /dev nodes If libselinux is available, record the original node SELinux context into an internal `__DEVCONTEXT` property, and restore it in `umockdev-run`. This property can also be set via the API. Fixes #220 --- README.md | 2 +- meson.build | 32 +++++++++++++++------- src/selinux.vapi | 5 ++++ src/umockdev-record.vala | 14 +++++++++- src/umockdev.vala | 37 +++++++++++++++++++++++-- tests/test-umockdev-record.vala | 14 ++++++++++ tests/test-umockdev-run.vala | 30 +++++++++++++++++++++ tests/test-umockdev-vala.vala | 48 +++++++++++++++++++++++++++++++++ 8 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 src/selinux.vapi diff --git a/README.md b/README.md index 80e7624..bb1b645 100644 --- a/README.md +++ b/README.md @@ -372,7 +372,7 @@ scripts/ioctls, etc.), unless it is a feature request. License ======= - Copyright (C) 2012 - 2014 Canonical Ltd. -- Copyright (C) 2017 - 2021 Martin Pitt +- Copyright (C) 2017 - 2023 Martin Pitt umockdev is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by diff --git a/meson.build b/meson.build index d82eefa..05cab01 100644 --- a/meson.build +++ b/meson.build @@ -66,7 +66,15 @@ meson.add_dist_script(srcdir / 'getversion.sh') # dependencies # +optional_defines = [] + dl = cc.find_library('dl') +selinux = cc.find_library('libselinux', required: false) +if selinux.found() + if cc.check_header('selinux/selinux.h') + optional_defines += ['--define=HAVE_SELINUX'] + endif +endif glib = dependency('glib-2.0', version: '>= 2.32.0') gobject = dependency('gobject-2.0', version: '>= 2.32.0') @@ -87,6 +95,7 @@ vala_libutil = cc.find_library('util') # local VAPIs vapi_config = valac.find_library('config', dirs: srcdir) vapi_ioctl = valac.find_library('ioctl', dirs: srcdir) +vapi_selinux = valac.find_library('selinux', dirs: srcdir) vapi_assertions = valac.find_library('assertions', dirs: testsdir) # @@ -141,7 +150,7 @@ umockdev_lib = shared_library('umockdev', 'src/debug.c'], vala_vapi: 'umockdev-1.0.vapi', vala_gir: 'UMockdev-1.0.gir', - dependencies: [glib, gobject, gio, gio_unix, vapi_posix, vapi_linux, vapi_linux_fixes, vala_libudev, vala_libutil, vapi_ioctl, libpcap], + dependencies: [glib, gobject, gio, gio_unix, vapi_posix, vapi_linux, vapi_linux_fixes, vala_libudev, vala_libutil, vapi_ioctl, vapi_selinux, libpcap, selinux], link_with: [umockdev_utils_lib], link_depends: ['src/umockdev.map'], link_args: [ @@ -151,7 +160,7 @@ umockdev_lib = shared_library('umockdev', ], vala_args: ['--define=INTERNAL_REGISTER_API', '--define=INTERNAL_UNREGISTER_PATH_API', - '--vapidir=@0@/src'.format(meson.current_source_dir())], + '--vapidir=@0@/src'.format(meson.current_source_dir())] + optional_defines, include_directories: include_directories('src'), version: lib_version, install: true, @@ -201,11 +210,11 @@ umockdev_record_exe = executable('umockdev-record', 'src/ioctl_tree.c', 'src/utils.c', 'src/debug.c'], - dependencies: [glib, gobject, gio_unix, vapi_posix, vapi_config, vapi_ioctl, libpcap], + dependencies: [glib, gobject, gio_unix, vapi_posix, vapi_config, vapi_ioctl, vapi_selinux, libpcap, selinux], link_with: [umockdev_utils_lib], vala_args: ['--define=INTERNAL_REGISTER_API', '--define=INTERNAL_UNREGISTER_ALL_API', - '--vapidir=@0@/src'.format(meson.current_source_dir())], + '--vapidir=@0@/src'.format(meson.current_source_dir())] + optional_defines, include_directories: include_directories('src'), install: true) @@ -257,8 +266,9 @@ if gudev.found() test('umockdev-vala', executable('test-umockdev-vala', 'tests/test-umockdev-vala.vala', - dependencies: [glib, gobject, gio, gudev, vapi_posix, vapi_assertions, vapi_ioctl], - link_with: [umockdev_lib, umockdev_utils_lib]), + dependencies: [glib, gobject, gio, gudev, vapi_posix, vapi_assertions, vapi_ioctl, vapi_selinux, selinux], + link_with: [umockdev_lib, umockdev_utils_lib], + vala_args: optional_defines), depends: [preload_lib], suite: 'fails-valgrind') endif @@ -273,14 +283,16 @@ test('ioctl-tree', executable('test-ioctl-tree', test('umockdev-run', executable('test-umockdev-run', 'tests/test-umockdev-run.vala', - dependencies: [glib, gobject, gio, vapi_posix, vapi_assertions, vapi_config], - link_with: [umockdev_lib, umockdev_utils_lib]), + dependencies: [glib, gobject, gio, vapi_posix, vapi_assertions, vapi_config, vapi_selinux, selinux], + link_with: [umockdev_lib, umockdev_utils_lib], + vala_args: optional_defines), depends: [umockdev_run_exe, preload_lib, test_chatter_exe, test_chatter_stream_exe]) test('umockdev-record', executable('test-umockdev-record', 'tests/test-umockdev-record.vala', - dependencies: [glib, gobject, gio, gio_unix, vapi_posix, vapi_linux, vapi_assertions, vapi_config, vala_libutil], - link_with: [umockdev_lib, umockdev_utils_lib]), + dependencies: [glib, gobject, gio, gio_unix, vapi_posix, vapi_linux, vapi_assertions, vapi_config, vala_libutil, vapi_selinux, selinux], + link_with: [umockdev_lib, umockdev_utils_lib], + vala_args: optional_defines), depends: [umockdev_record_exe, preload_lib, test_readbyte_exe, test_chatter_exe, test_chatter_stream_exe], suite: 'fails-valgrind') diff --git a/src/selinux.vapi b/src/selinux.vapi new file mode 100644 index 0000000..11f979d --- /dev/null +++ b/src/selinux.vapi @@ -0,0 +1,5 @@ +[CCode (cprefix = "", lower_case_cprefix = "", cheader_filename = "selinux/selinux.h")] +namespace Selinux { + int lgetfilecon (string path, out string context); + int lsetfilecon (string path, string context); +} diff --git a/src/umockdev-record.vala b/src/umockdev-record.vala index bf0e644..2d49bc8 100644 --- a/src/umockdev-record.vala +++ b/src/umockdev-record.vala @@ -21,6 +21,9 @@ */ using UMockdevUtils; +#if HAVE_SELINUX +using Selinux; +#endif static void devices_from_dir (string dir, ref GenericArray devs) @@ -251,7 +254,16 @@ record_device(string dev) continue; if (line.has_prefix("N: ")) { - line = line + dev_contents("/dev/" + line.substring(3).chomp()); + string devpath = "/dev/" + line.substring(3).chomp(); + line = line + dev_contents(devpath); + + // record SELinux context +#if HAVE_SELINUX + string context; // this is owned by vala, not calling Selinux.freecon() on it + int res = Selinux.lgetfilecon(devpath, out context); + if (res > 0) + properties.append("E: __DEVCONTEXT=" + context); +#endif } stdout.puts(line); stdout.putc('\n'); diff --git a/src/umockdev.vala b/src/umockdev.vala index c5a6b00..37ff9f7 100644 --- a/src/umockdev.vala +++ b/src/umockdev.vala @@ -20,6 +20,10 @@ namespace UMockdev { using UMockdevUtils; +#if HAVE_SELINUX +using Selinux; +#endif + private bool __in_mock_env_initialized = false; private bool __in_mock_env_result = false; @@ -538,6 +542,9 @@ public class Testbed: GLib.Object { * possible to change them later on with umockdev_testbed_set_attribute() and * umockdev_testbed_set_property(). * + * If the pseudo-property "__DEVCONTEXT" is present, the SELinux context of the device's + * DEVNODE will be set to that value. + * * This will synthesize an "add" uevent. * * Returns: The sysfs path for the newly created device. Free with g_free(). @@ -577,6 +584,9 @@ public class Testbed: GLib.Object { * possible to change them later on with umockdev_testbed_set_attribute() and * umockdev_testbed_set_property(). * + * If the pseudo-property "__DEVCONTEXT" is present, the SELinux context of the device's + * DEVNODE will be set to that value. + * * This will synthesize an "add" uevent. * * Example: @@ -1300,6 +1310,7 @@ public class Testbed: GLib.Object { string[] binattrs = {}; /* hex encoded values */ string[] linkattrs = {}; string[] props = {}; + string? selinux_context = null; /* scan until we see an empty line */ while (cur_data.length > 0 && cur_data[0] != '\n') { @@ -1328,6 +1339,14 @@ public class Testbed: GLib.Object { break; case 'E': + if (key == "__DEVCONTEXT") { + if (selinux_context != null) + throw new UMockdev.Error.VALUE("duplicate __DEVCONTEXT property in description of device %s", + devpath); + selinux_context = val; + break; + } + props += key; props += val; if (key == "SUBSYSTEM") { @@ -1378,7 +1397,7 @@ public class Testbed: GLib.Object { /* create fake device node */ if (devnode_path != null) { - this.create_node_for_device(subsystem, devnode_path, devnode_contents, majmin); + this.create_node_for_device(subsystem, devnode_path, devnode_contents, majmin, selinux_context); /* create symlinks */ for (int i = 0; i < devnode_links.length; i++) { @@ -1400,7 +1419,8 @@ public class Testbed: GLib.Object { } private void - create_node_for_device (string subsystem, string node_path, uint8[] node_contents, string? majmin) + create_node_for_device (string subsystem, string node_path, uint8[] node_contents, string? majmin, + string? selinux_context) throws UMockdev.Error { checked_mkdir_with_parents(Path.get_dirname(node_path), 0755); @@ -1418,6 +1438,7 @@ public class Testbed: GLib.Object { error("Cannot create dev node file: %s", e.message); } + set_selinux_context (node_path, selinux_context); return; } @@ -1456,8 +1477,20 @@ public class Testbed: GLib.Object { string devname = node_path.substring (this.root_dir.length); assert (!this.dev_fd.contains (devname)); this.dev_fd.insert (devname, ptym); + + set_selinux_context (node_path, selinux_context); } + private void set_selinux_context (string path, string? context) + { +#if HAVE_SELINUX + if (context != null) { + // this is opportunistic, it needs to work in environments without privilegs or SELinux + if (Selinux.lsetfilecon (path, context) < 0) + debug ("umockdev Testbed.create_node_for_device: setfilecon(%s, %s) failed: %m", path, context); + } +#endif + } /** * umockdev_testbed_record_parse_line: diff --git a/tests/test-umockdev-record.vala b/tests/test-umockdev-record.vala index abef9ec..8672ee1 100644 --- a/tests/test-umockdev-record.vala +++ b/tests/test-umockdev-record.vala @@ -22,6 +22,10 @@ using UMockdevUtils; using Assertions; +#if HAVE_SELINUX +using Selinux; +#endif + string readbyte_path; string tests_dir; @@ -196,6 +200,16 @@ t_system_single () assert_in("E: DEVNAME=/dev/null", sout); assert_in("P: /devices/virtual/mem/null", sout); assert_in("E: DEVNAME=/dev/zero", sout); +#if HAVE_SELINUX + // we may run on a system without SELinux + if (FileUtils.test("/sys/fs/selinux", FileTest.EXISTS)) { + string context; + assert_cmpint (Selinux.lgetfilecon ("/dev/null", out context), CompareOperator.GT, 0); + assert_in("E: __DEVCONTEXT=" + context + "\n", sout); + } else { + assert(!sout.contains("E: __DEVCONTEXT")); + } +#endif } // system /sys: umockdev-record --all works and result loads back diff --git a/tests/test-umockdev-run.vala b/tests/test-umockdev-run.vala index 81fd7b0..cd00a08 100644 --- a/tests/test-umockdev-run.vala +++ b/tests/test-umockdev-run.vala @@ -21,6 +21,10 @@ using UMockdevUtils; using Assertions; +#if HAVE_SELINUX +using Selinux; +#endif + const string umockdev_run_command = "env LC_ALL=C umockdev-run "; const string umockdev_record_command = "env LC_ALL=C umockdev-record "; @@ -180,6 +184,7 @@ t_run_udevadm_block () checked_file_set_contents (umockdev_file, """P: /devices/virtual/block/loop23 N: loop23 E: DEVNAME=/dev/loop23 +E: __DEVCONTEXT=system_u:object_r:fixed_disk_device_t:s0 E: DEVTYPE=disk E: MAJOR=7 E: MINOR=23 @@ -207,6 +212,18 @@ A: size=1048576\n assert (sout.contains ("E: MAJOR=7")); assert (sout.contains ("E: MINOR=23")); +#if HAVE_SELINUX + // we may run on a system without SELinux + if (FileUtils.test("/sys/fs/selinux", FileTest.EXISTS)) { + check_program_out("true", "-d " + umockdev_file + " -- stat -c %C /dev/loop23", + "system_u:object_r:fixed_disk_device_t:s0\n"); + } else { + stdout.printf ("[SKIP selinux context check: SELinux not active] "); + } +#else + stdout.printf ("[SKIP selinux context check: not built with SELinux support] "); +#endif + checked_remove (umockdev_file); } @@ -333,6 +350,19 @@ t_run_record_null () check_program_out("true", "-d " + umockdev_file + " -- stat -c '%n %F %t %T' /dev/null", "/dev/null character special file 1 3\n"); +#if HAVE_SELINUX + // we may run on a system without SELinux + if (FileUtils.test("/sys/fs/selinux", FileTest.EXISTS)) { + string orig_context; + assert_cmpint (Selinux.lgetfilecon ("/dev/null", out orig_context), CompareOperator.GT, 0); + check_program_out("true", "-d " + umockdev_file + " -- stat -c %C /dev/null", orig_context + "\n"); + } else { + stdout.printf ("[SKIP selinux context check: SELinux not active] "); + } +#else + stdout.printf ("[SKIP selinux context check: not built with SELinux support] "); +#endif + checked_remove (umockdev_file); } diff --git a/tests/test-umockdev-vala.vala b/tests/test-umockdev-vala.vala index 2f3f554..bd15e97 100644 --- a/tests/test-umockdev-vala.vala +++ b/tests/test-umockdev-vala.vala @@ -21,6 +21,10 @@ using UMockdevUtils; using Assertions; +#if HAVE_SELINUX +using Selinux; +#endif + string rootdir; /* exception-handling wrappers */ @@ -194,6 +198,47 @@ t_testbed_fs_ops () assert_cmpint (Posix.chdir (orig_cwd), CompareOperator.EQ, 0); } +#if HAVE_SELINUX +void +t_testbed_selinux () +{ + if (!FileUtils.test("/sys/fs/selinux", FileTest.EXISTS)) { + stdout.printf ("[SKIP SELinux not active]\n"); + return; + } + + var tb = new UMockdev.Testbed (); + + // valid context + tb_add_from_string (tb, """P: /devices/myusbhub/cam +N: bus/usb/001/002 +E: SUBSYSTEM=usb +E: DEVTYPE=usb_device +E: DEVNAME=/dev/bus/usb/001/002 +E: __DEVCONTEXT=system_u:object_r:device_t:s0 +"""); + + string context; + assert_cmpint (Selinux.lgetfilecon ("/dev/bus/usb/001/002", out context), CompareOperator.GT, 0); + assert_cmpstr (context, CompareOperator.EQ, "system_u:object_r:device_t:s0"); + + // invalidly context + tb_add_from_string (tb, """P: /devices/invalidcontext +N: invalidcontext +E: SUBSYSTEM=tty +E: DEVNAME=/dev/invalidcontext +E: __DEVCONTEXT=blah +"""); + + assert (FileUtils.test("/dev/invalidcontext", FileTest.EXISTS)); + string root_context; + assert_cmpint (Selinux.lgetfilecon (tb.get_root_dir(), out root_context), CompareOperator.GT, 0); + assert_cmpint (Selinux.lgetfilecon ("/dev/invalidcontext", out context), CompareOperator.GT, 0); + // has default context + assert_cmpstr (context, CompareOperator.EQ, root_context); +} +#endif + void t_usbfs_ioctl_static () { @@ -1076,6 +1121,9 @@ main (string[] args) Test.add_func ("/umockdev-testbed-vala/add_devicev", t_testbed_add_device); Test.add_func ("/umockdev-testbed-vala/gudev-query-list", t_testbed_gudev_query_list); Test.add_func ("/umockdev-testbed-vala/fs_ops", t_testbed_fs_ops); +#if HAVE_SELINUX + Test.add_func ("/umockdev-testbed-vala/selinux", t_testbed_selinux); +#endif /* tests for mocking ioctls */ Test.add_func ("/umockdev-testbed-vala/usbfs_ioctl_static", t_usbfs_ioctl_static);