Skip to content

Commit

Permalink
Record and restore SELinux context for mocked /dev nodes
Browse files Browse the repository at this point in the history
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
  • Loading branch information
martinpitt committed Dec 18, 2023
1 parent 697e1ec commit a08e391
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 14 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 22 additions & 10 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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)

#
Expand Down Expand Up @@ -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: [
Expand All @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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')

Expand Down
5 changes: 5 additions & 0 deletions src/selinux.vapi
Original file line number Diff line number Diff line change
@@ -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);
}
14 changes: 13 additions & 1 deletion src/umockdev-record.vala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
*/

using UMockdevUtils;
#if HAVE_SELINUX
using Selinux;
#endif

static void
devices_from_dir (string dir, ref GenericArray<string> devs)
Expand Down Expand Up @@ -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');
Expand Down
37 changes: 35 additions & 2 deletions src/umockdev.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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().
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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++) {
Expand All @@ -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);
Expand All @@ -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;
}

Expand Down Expand Up @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions tests/test-umockdev-record.vala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
using UMockdevUtils;
using Assertions;

#if HAVE_SELINUX
using Selinux;
#endif

string readbyte_path;
string tests_dir;

Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions tests/test-umockdev-run.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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 ";

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down
48 changes: 48 additions & 0 deletions tests/test-umockdev-vala.vala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
using UMockdevUtils;
using Assertions;

#if HAVE_SELINUX
using Selinux;
#endif

string rootdir;

/* exception-handling wrappers */
Expand Down Expand Up @@ -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 ()
{
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit a08e391

Please sign in to comment.