September 30th, 2011

Fedora 16 New SELinux Feature part IV - Shrinking policy

Back in July the systemd team was trying to decrease the boot time on early versions of Fedora 16.  They found that with a Solid State disk, SELinux policy load and relabel was quickly becoming the biggest pig as far as boot time.  So they added some log messages that showed how long it was taking to just read the selinux policy off of disk and load it into the kernel.

Lennart Poettering announced systemd 32 with the following message.

Primarily bugfixes, and one really cool improvement: we can now load the SELinux policy without having to reexecute ourselves. This is much
prettier and saves up to 70ms or so. I also added some basic profiling output for SELinux which unfortunately shows that SELinux costs around
5s on every boot on f16 (and that on my really fast machine!). Sad. 

Look for output like this:

[   10.727004] systemd[1]: Successfully loaded SELinux policy in 3s 270ms 896us.
[   10.769204] systemd[1]: Successfully loaded SELinux database in 41ms 700us, size on heap is 460K.
[   11.943903] systemd[1]: Relabelled /dev and /run in 1s 125ms 738us.

He even added these lines to every boot, so everyone would know how much time SELinux was costing them on boot. 

Nothing like public embarrassment to make you take action. 

Shame is a great motivator. :^(

I decided to take a look at the policy using the sesearch tools.  I wanted to figure out where all the rules were coming from, and whether we had some duplicates we could remove.  The first thing I noticed was there were thousands of rules related to network ports.  To me there seemed to be way to many.  I began to investigate and found that M4 macro expansion was the problem.

SELinux policy is written using m4.  Over the years we have written lots of macros which policy writers take advantage.   We call these macros interfaces.  Another feature of SELinux policy is the use of attributes.  Attrinbutes are a way of grouping lots of types (init_t, httpd_t) together.  You can create a new user type say staff_t and add an attribute say usertype.  Now you write rules regarding the usertype that affect all users.

allow usertype etc_t:file read;

SELinux also defines network port attributes like port_type and reserved_port_type.  All network ports get the attribute port_type and all ports < 1024 get the attribute reserved port type.  Well M4 has a cool feature "negation".   SELinux policy was using negation in many places including defineing unreserved_ports.  For example in Fedora 15 we have an interface that says.

        attribute port_type, reserved_port_type;

    allow $1 { port_type -reserved_port_type }:tcp_socket name_bind;

All types that need to bind to ports > 1023 would then using this interface.

/usr/bin/ssh (ssh_t) needs to be able to setup alternate ports to allow a tunnel connection between a remote sshd service and the local machine, so we allow it to bind to any port > 1023 using the following line:


Seems like a simple rule to add, until you understand how m4 works with negation.  M4 expands out all the attributes into their types and then writes a rule for each type that matches.  A rule like this could end up adding 100s of allow rules.  For every type that is a port_type and not a reserved_port_type, a rule would be written allowing ssh_t to bind to the port.

allow ssh_t amqp_port_t:tcp_socket name_bind;
allow ssh_t asterisk_port_t:tcp_socket name_bind;

I found that if I defined a new attribute "unreserved_port_type", and rewrote the interface to something like.

        attribute port_type, reserved_port_type;

    allow $1 unreserved_port_type:tcp_socket name_bind;

I ended up with only one rule generated by


allow ssh_t unreserved_port_type:tcp_socket name_bind;

Turns out we had lots and lots of interfaces where we used the negation.

    dontaudit $1 { port_type -port_t }:dccp_socket name_bind;
    files_read_all_dirs_except($1, $2 -shadow_t)

I went through the entire policy and switched to using only attributes like unreserved_port_type attributes and shrunk the size of policy by about 80 %.

What is really nice, you can check the size of policy using seinfo.
As time went on F15 machine:
$ seinfo
Statistics for policy file: /etc/selinux/targeted/policy/policy.24
Policy Version & Type: v.24 (binary, mls)
Allow:          282444
Dontaudit:      184516

and on F16 machine:

$ seinfo
Statistics for policy file: /etc/selinux/targeted/policy/policy.26
Policy Version & Type: v.26 (binary, mls)
Allow:           88242
Dontaudit:       11302

Tools used to load the policy run about 3 times as fast.


Tom London looked at the change on his machine and found

And comparing 'old vs. new' boot times, first the old:

Jul 28 06:39:29 tlondon systemd[1]: Startup finished in 3s 336ms 755us (kernel) + 11s 625ms 240us (initrd) + 28s 189ms 914us (userspace) = 43s 151ms 909us.

And now the 'new':

Jul 29 06:00:41 tlondon systemd[1]: Startup finished in 1s 844ms 542us (kernel) + 4s 999ms 977us (initrd) + 29s 239ms 766us (userspace) = 36s 84ms 285us.

6.5 seconds less in initrd.

A second feature of this change is we are now taking up probably 80% less kernel memory...

# du -s /etc/selinux/targeted/policy/policy.24
6004    /etc/selinux/targeted/policy/policy.24

Fedora 16:

# du -s /etc/selinux/targeted/policy/policy.26
2156    /etc/selinux/targeted/policy/policy.26

And Fedora 16 has more domains, types and rules...

At some point I should probably back port these changes to RHEL6.