Repository: mesonbuild/meson

Issue:
SELinux context wrong after install
The selinux context is wrong after a ninja install. Found this during the libinput installs  and that's just a normal .so file. For the example here I'm using libratbag because we're overwriting system files and it doesn't matter much if libratbag breaks, your desktop won't go down because of it. The reason I'm picking an existing library is because that has a guaranteed SELinux context after system install as opposed to something new that may fall through the cracks of the SELinux policy.

Reproducer:

* Fedora 25 with SELinux enabled (i.e. standard config)
*  sudo dnf install -y libratbag

The context is:
```
$ ls -lZ /usr/lib64/libratbag.so.4.0.3
-rwxr-xr-x. 1 root root system_u:object_r:lib_t:s0 155944 Jun  6 23:41 /usr/lib64/libratbag.so.4.0.3
```
meson.build file
```meson
project('test', 'c', version : '0')
lib_test = shared_library('ratbag',
	'test.c',
	 version : '4.0.3', # Matches F25 soname
	 install: true)
```

```
$ touch test.c
$ meson builddir --prefix=/usr
$ ninja -C builddir
$ sudo ninja -C builddir install

$ ls -lZ /usr/lib64/libratbag.so.4.0.3
-rwxrwxr-x. 1 root root unconfined_u:object_r:user_tmp_t:s0 7904 Jun 20 11:58 /usr/lib64/libratbag.so.4.0.3

# must run restorecon to fix it
$ sudo restorecon /usr/lib64/libratbag.*
$  ls -lZ /usr/lib64/libratbag.so.4.0.3
-rwxrwxr-x. 1 root root unconfined_u:object_r:lib_t:s0 7904 Jun 20 11:58 /usr/lib64/libratbag.so.4.0.3
```
Note how it's still ```unconfined_u``` instead of ```system_u``` after restorecon but at least this doesn't trigger SELinux denials anymore. In the case of libinput a denial prevents gdm from starting up, which is a tad annoying to some users :)


Autotools uses ```/usr/bin/install``` instead and that works (even when run manually):
````
$ sudo /usr/bin/install builddir/libratbag.so.4.0.3 /usr/lib64/libratbag.so.4.0.3 
$ ls -lZ /usr/lib64/libratbag.so.4.0.3 
-rwxr-xr-x. 1 root root system_u:object_r:lib_t:s0 7904 Jun 20 12:05 /usr/lib64/libratbag.so.4.0.3
````



Discussion:
Do we want to set the `system_u` selinux context on all installs or only on installs outside the home directory?
I'm curious: is this something we decide or something that should be handled by system policy?
I think we need someone who understands selinux here, like @ignatenkobrain ;)
@whot can you send link to project which I could experiment on?
@ignatenkobrain libinput reproduces this, at least on a stock Fedora 25 and when built with meson

http://cgit.freedesktop.org/wayland/libinput/
FWIW, if this bug is blocking something, you could place a temporary workaround using [`meson.add_install_script()`](http://mesonbuild.com/Reference-manual.html#meson-object).
Patch on the list, thanks for the idea and the review: https://lists.freedesktop.org/archives/wayland-devel/2017-June/034300.html
I think this is because we do
```python
    shutil.copyfile(from_file, to_file)
    shutil.copystat(from_file, to_file)
```

looks like that copystat copies selinux attributes..
🤦‍

So `copystat` blindly copies all extended attributes, which also means selinux attributes. Maybe we should do `copymode` instead which only copies permissions?
@nirbheek I think we definitely want to copy atime/mtime.. Not sure about extended attributes.
Also owner and group since we provide API for that. We don't support any ACLs yet, so I think we should just ignore extattrs until someone complains.
Ok, I finally had some time to look at this, and came up with this diff as a WIP

````
diff --git a/mesonbuild/scripts/meson_install.py b/mesonbuild/scripts/meson_install.py
index d9490901..d680e4a0 100644
--- a/mesonbuild/scripts/meson_install.py
+++ b/mesonbuild/scripts/meson_install.py
@@ -53,6 +53,18 @@ def set_mode(path, mode):
             msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...'
             print(msg.format(path, mode.perms_s, e.strerror))
 
+def set_selinux_context(to_file):
+    try:
+        import selinux
+        if selinux.is_selinux_enabled():
+            con = selinux.matchpathcon(to_file, 0)[1]
+            selinux.lsetfilecon(to_file, con)
+    except OSError:
+        # Should probably have some error message here
+        pass
+    except ModuleNotFoundError:
+        pass # can quietly ignore this one
+
 def append_to_log(line):
     install_log_file.write(line)
     if not line.endswith('\n'):
@@ -73,6 +85,7 @@ def do_copyfile(from_file, to_file):
         os.unlink(to_file)
     shutil.copyfile(from_file, to_file)
     shutil.copystat(from_file, to_file)
+    set_selinux_context(to_file)
     append_to_log(to_file)
 
 def do_copydir(src_prefix, src_dir, dst_dir):
````
Nothing in `shutil` really helps us here because we need to restore the context after installing based on the system setup. Copying permissions/attributes isn't going to work because while the built file sits in $HOME/path/to/foo it'll have a different context than after install into the target path.

There's one immediate problem: afaict `import selinux` is a nonstarter because it's an external module. What's the procedure here to get this into meson?

 

Resolving patch:
diff --git a/mesonbuild/scripts/meson_install.py b/mesonbuild/scripts/meson_install.py
--- a/mesonbuild/scripts/meson_install.py
+++ b/mesonbuild/scripts/meson_install.py
@@ -19,6 +19,7 @@
 from ..mesonlib import is_windows, Popen_safe
 
 install_log_file = None
+use_selinux = True
 
 def set_mode(path, mode):
     if mode is None:
@@ -53,6 +54,28 @@ def set_mode(path, mode):
             msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...'
             print(msg.format(path, mode.perms_s, e.strerror))
 
+def restore_selinux_context(to_file):
+    '''
+    Restores the SELinux context for @to_file
+    '''
+    global use_selinux
+
+    if not use_selinux:
+        return
+
+    try:
+        subprocess.check_call(['selinuxenabled'])
+        try:
+            subprocess.check_call(['restorecon', '-F', to_file], stderr=subprocess.DEVNULL)
+        except subprocess.CalledProcessError as e:
+            use_selinux = False
+            msg = "{!r}: Failed to restore SELinux context, ignoring SELinux context for all remaining files..."
+            print(msg.format(to_file, e.returncode))
+    except (FileNotFoundError, subprocess.CalledProcessError) as e:
+        # If we don't have selinux or selinuxenabled returned 1, failure
+        # is ignored quietly.
+        use_selinux = False
+
 def append_to_log(line):
     install_log_file.write(line)
     if not line.endswith('\n'):
@@ -73,6 +96,7 @@ def do_copyfile(from_file, to_file):
         os.unlink(to_file)
     shutil.copyfile(from_file, to_file)
     shutil.copystat(from_file, to_file)
+    restore_selinux_context(to_file)
     append_to_log(to_file)
 
 def do_copydir(src_prefix, src_dir, dst_dir):


Test patch: