From 1de4e51610b570d1f417f3bc65bca2040f0b329a Mon Sep 17 00:00:00 2001
From: Kirk Webb <kwebb@cs.utah.edu>
Date: Tue, 4 Mar 2014 08:46:02 -0700
Subject: [PATCH] Add taint state tracking for OSes and Nodes.

Emulab can now propagate OS taint traits on to nodes that load these OSes.
The primary reason for doing this is for loading images which
require special treatment of the node.  For example, an OS that has
proprietary software, and which will be used as an appliance (blackbox)
can be marked (tainted) as such.  Code that manages user accounts on such
OSes, along with other side channel providers (console, node admin, image
creation) can key off of these taint states to prevent or alter access.

Taint states are defined as SQL sets in the 'os_info' and 'nodes' tables,
kept in the 'taint_states' column in both.  Currently these sets are comprised
of the following entries:

* usermode: OS/node should only allow user level access (not root)
* blackbox: OS/node should allow no direct interaction via shell, console, etc.
* dangerous: OS image may contain malicious software.

Taint states are inherited by a node from OSes it loads during the OS load
process.  Similarly, they are cleared from nodes as these OSes are removed.
Any taint state applied to a node will currently enforce disk zeroing.

No other tools/subsystems consider the taint states currently, but that will
change soon.

Setting taint states for an OS has to be done via SQL presently.
---
 db/EmulabConstants.pm.in    |  11 +++
 db/Node.pm.in               | 139 ++++++++++++++++++++++++++++++++++++
 db/OSinfo.pm.in             | 110 ++++++++++++++++++++++++++++
 sql/database-create.sql     |   2 +
 sql/database-fill.sql       |   1 +
 sql/updates/4/385           |  33 +++++++++
 tbsetup/libosload.pm.in     |  53 +++++++++++++-
 tbsetup/libosload_new.pm.in |  47 ++++++++++++
 8 files changed, 393 insertions(+), 3 deletions(-)
 create mode 100644 sql/updates/4/385

diff --git a/db/EmulabConstants.pm.in b/db/EmulabConstants.pm.in
index f72ca7862a..d2b99912b9 100644
--- a/db/EmulabConstants.pm.in
+++ b/db/EmulabConstants.pm.in
@@ -76,6 +76,9 @@ use vars qw(@ISA @EXPORT);
 	 TB_OSID_DESTROY TB_OSID_MIN TB_OSID_MAX
 	 TB_OSID_OSIDLEN TB_OSID_OSNAMELEN TB_OSID_VERSLEN
 
+         TB_TAINTSTATE_USERONLY TB_TAINTSTATE_BLACKBOX TB_TAINTSTATE_DANGEROUS 
+         TB_TAINTSTATE_ALL
+
 	 TB_IMAGEID_READINFO TB_IMAGEID_MODIFYINFO TB_IMAGEID_EXPORT
 	 TB_IMAGEID_CREATE TB_IMAGEID_DESTROY
 	 TB_IMAGEID_ACCESS TB_IMAGEID_MIN TB_IMAGEID_MAX
@@ -375,6 +378,14 @@ sub TB_OSID_MBKERNEL()          { "_KERNEL_"; } # multiboot kernel OSID
 sub TB_OSID_FREEBSD_MFS()	{ "FREEBSD-MFS" };
 sub TB_OSID_FRISBEE_MFS()	{ "FRISBEE-MFS" };
 
+# OS/Node taint states
+sub TB_TAINTSTATE_USERONLY()    { "useronly"; };
+sub TB_TAINTSTATE_BLACKBOX()    { "blackbox"; };
+sub TB_TAINTSTATE_DANGEROUS()   { "dangerous"; };
+sub TB_TAINTSTATE_ALL()         { (TB_TAINTSTATE_USERONLY(),
+				   TB_TAINTSTATE_BLACKBOX(),
+				   TB_TAINTSTATE_DANGEROUS()); };
+
 # ImageIDs
 #
 # Clarification:
diff --git a/db/Node.pm.in b/db/Node.pm.in
index ae72810624..b1d9053f70 100755
--- a/db/Node.pm.in
+++ b/db/Node.pm.in
@@ -3742,5 +3742,144 @@ sub ClrTipAclUrl($)
 		"where node_id='$node_id'");
 }
 
+#
+# Check to see if the node is tainted, or tainted in a
+# particular way.
+#
+sub IsTainted($;$)
+{
+    my ($self, $taint) = @_;
+
+    my $taint_states = $self->taint_states();
+    return 0
+	if (!defined($taint_states) || $taint_states eq "");
+
+    # Just looking to see if any taint is applied?
+    return 1
+	if (!defined($taint));
+
+    # Looking for a specific taint.
+    return grep {$_ eq $taint} split(',', $taint_states);
+}
+
+#
+# Get the current set of taint states for the Node
+#
+sub GetTaintStates($) {
+    my ($self) = @_;
+
+    my $taint_states = $self->taint_states();
+    return ()
+	if (!defined($taint_states) || $taint_states eq "");
+
+    return split(',', $taint_states);
+}
+
+#
+# Explicitly set the taint states based on an input array of states.
+# Squash any duplicates or empty/undefined entries.
+#
+sub SetTaintStates($@) {
+    my ($self, @taint_states) = @_;
+
+    my @newtstates = ();
+    my @validtstates = TB_TAINTSTATE_ALL();
+
+    foreach my $tstate (@taint_states) {
+	next if (!$tstate);
+	if (!grep {$_ eq $tstate} @validtstates) {
+	    warn "Invalid taint state: $tstate\n";
+	    return -1;
+	}
+	if (!grep {$_ eq $tstate} @newtstates) {
+	    push @newtstates, $tstate;
+	}
+    }
+
+    return 0
+	if (!@newtstates);
+
+    return $self->Update({"taint_states" => join(',', @newtstates)});
+}
+
+#
+# Add a taint state to the node.
+#
+sub AddTaint($$)
+{
+    my ($self, $taint) = @_;
+
+    if (!grep {$_ eq $taint} TB_TAINTSTATE_ALL()) {
+	warn "Invalid taint state: $taint\n";
+	return -1;
+    }
+
+    return 0
+	if ($self->IsTainted($taint));
+
+    my $taint_states = $self->taint_states();
+    if (!defined($taint_states) || $taint_states eq "") {
+	$taint_states = $taint;
+    }
+    else {
+	$taint_states .= ",$taint";
+    }
+
+    return $self->Update({"taint_states" => $taint_states});
+}
+
+#
+# Inherit the taint states from an OS.  Take the union with whatever
+# taint states are already set for the node.
+#
+sub InheritTaintStates($$) {
+    my ($self, $osinfo) = @_;
+    require OSinfo;
+
+    if (!ref($osinfo)) {
+	my $tmp = OSinfo->Lookup($osinfo);
+	if (!defined($tmp)) {
+	    warn "Cannot lookup osinfo for $osinfo\n";
+	    return -1;
+	}
+	$osinfo = $tmp;
+    }
+
+    my $os_taint_states   = $osinfo->taint_states();
+    return 0
+	if (!defined($os_taint_states) || $os_taint_states eq "");
+    my @taint_states      = split(',', $os_taint_states);
+    my $node_taint_states = $self->taint_states();
+    if ($node_taint_states) {
+	push @taint_states, split(',', $node_taint_states);
+    }
+
+    return $self->SetTaintStates(@taint_states);
+}
+
+#
+# Remove a taint state (or all taint states) from the node.
+#
+sub RemoveTaint($;$)
+{
+    my ($self, $taint) = @_;
+
+    return 0
+	if (!$self->IsTainted($taint));
+
+    my $taint_states = $self->taint_states();
+    return 0
+	if (!defined($taint_states) || $taint_states eq "");
+
+    if (defined($taint)) {
+	$taint_states = join(',', 
+			     grep {$_ ne $taint} split(',', $taint_states));
+    } else {
+	$taint_states = "";
+    }
+
+    return $self->Update({"taint_states" => $taint_states});
+}
+
 # _Always_ make sure that this 1 is at the end of the file...
 1;
diff --git a/db/OSinfo.pm.in b/db/OSinfo.pm.in
index 35377f35d9..4128dd610e 100644
--- a/db/OSinfo.pm.in
+++ b/db/OSinfo.pm.in
@@ -730,5 +730,115 @@ sub MapToImage($$)
     return Image->Lookup($imageid);
 }
 
+#
+# Check if the OS is tainted, or tainted in a
+# particular way.
+#
+sub IsTainted($;$)
+{
+    my ($self, $taint) = @_;
+
+    my $taint_states = $self->taint_states();
+    return 0
+	if (!defined($taint_states) || $taint_states eq "");
+
+    # Just looking to see if any taint is applied?
+    return 1
+	if (!defined($taint));
+
+    # Looking for a specific taint.
+    return grep {$_ eq $taint} split(',', $taint_states);
+}
+
+#
+# Get the current set of taint states for the OS
+#
+sub GetTaintStates() {
+    my ($self) = @_;
+
+    my $taint_states = $self->taint_states();
+    return ()
+	if (!defined($taint_states) || $taint_states eq "");
+
+    return split(',', $taint_states);
+}
+
+#
+# Explicitly set the taint states based on an input array of states.
+# Squash any duplicates or empty/undefined entries.
+#
+sub SetTaintStates($@) {
+    my ($self, @taint_states) = @_;
+
+    my @newtstates = ();
+    my @validtstates = TB_TAINTSTATE_ALL();
+
+    foreach my $tstate (@taint_states) {
+	next if (!$tstate);
+	if (!grep {$_ eq $tstate} @validtstates) {
+	    warn "Invalid taint state: $tstate\n";
+	    return -1;
+	}
+	if (!grep {$_ eq $tstate} @newtstates) {
+	    push @newtstates, $tstate;
+	}
+    }
+
+    return 0
+	if (!@newtstates);
+
+    return $self->Update({"taint_states" => join(',', @newtstates)});
+}
+
+#
+# Add a taint state to the OS.
+#
+sub AddTaint($$)
+{
+    my ($self, $taint) = @_;
+
+    if (!grep {$_ eq $taint} TB_TAINTSTATE_ALL()) {
+	warn "Invalid taint state: $taint\n";
+	return -1;
+    }
+
+    return 0
+	if ($self->IsTainted($taint));
+
+    my $taint_states = $self->taint_states();
+    if (!defined($taint_states) || $taint_states eq "") {
+	$taint_states = $taint;
+    }
+    else {
+	$taint_states .= ",$taint";
+    }
+
+    return $self->Update({"taint_states" => $taint_states});
+}
+
+#
+# Remove a taint state (or all taint states) from the OS.
+#
+sub RemoveTaint($;$)
+{
+    my ($self, $taint) = @_;
+
+    return 0
+	if (!$self->IsTainted($taint));
+
+    my $taint_states = $self->taint_states();
+    return 0
+	if (!defined($taint_states) || $taint_states eq "");
+
+    if (defined($taint)) {
+	$taint_states = join(',', 
+			     grep {$_ ne $taint} split(',', $taint_states));
+    } else {
+	$taint_states = "";
+    }
+
+    return $self->Update({"taint_states" => $taint_states});
+}
+
 # _Always_ make sure that this 1 is at the end of the file...
 1;
diff --git a/sql/database-create.sql b/sql/database-create.sql
index 863acb84fe..9e21a1b7ae 100644
--- a/sql/database-create.sql
+++ b/sql/database-create.sql
@@ -2916,6 +2916,7 @@ CREATE TABLE `nodes` (
   `uuid` varchar(40) NOT NULL default '',
   `reserved_memory` int(10) unsigned default '0',
   `nonfsmounts` tinyint(1) NOT NULL default '0',
+  `taint_states` set('useronly','blackbox','dangerous') default NULL,
   PRIMARY KEY  (`node_id`),
   KEY `phys_nodeid` (`phys_nodeid`),
   KEY `node_id` (`node_id`,`phys_nodeid`),
@@ -3151,6 +3152,7 @@ CREATE TABLE `os_info` (
   `mfs` tinyint(4) NOT NULL default '0',
   `reboot_waittime` int(10) unsigned default NULL,
   `protogeni_export` tinyint(1) NOT NULL default '0',
+  `taint_states` set('useronly','blackbox','dangerous') default NULL,
   PRIMARY KEY  (`osid`),
   UNIQUE KEY `pid` (`pid`,`osname`),
   KEY `OS` (`OS`),
diff --git a/sql/database-fill.sql b/sql/database-fill.sql
index e67552a3eb..0c682e3602 100644
--- a/sql/database-fill.sql
+++ b/sql/database-fill.sql
@@ -1160,6 +1160,7 @@ REPLACE INTO table_regex VALUES ('os_info','op_mode','text','regex','^[-\\w]*$',
 REPLACE INTO table_regex VALUES ('os_info','nextosid','text','redirect','os_info:osid',0,0,NULL);
 REPLACE INTO table_regex VALUES ('os_info','def_parentosid','text','redirect','os_info:osid',0,0,NULL);
 REPLACE INTO table_regex VALUES ('os_info','reboot_waittime','int','redirect','default:int',0,2000,NULL);
+REPLACE INTO table_regex VALUES ('os_info','taint_states','text','regex','^[-\\w,]*$',1,128,NULL);
 
 REPLACE INTO table_regex VALUES ('sitevariables','name','text','regex','^[\\w\\/]+$',1,255,NULL);
 REPLACE INTO table_regex VALUES ('sitevariables','value','text','redirect','default:text',0,0,NULL);
diff --git a/sql/updates/4/385 b/sql/updates/4/385
new file mode 100644
index 0000000000..19eaae3f78
--- /dev/null
+++ b/sql/updates/4/385
@@ -0,0 +1,33 @@
+#
+# Add noexport flag to images.
+#
+use strict;
+use libdb;
+
+my $impotent = 0;
+
+sub DoUpdate($$$)
+{
+    my ($dbhandle, $dbname, $version) = @_;
+
+    if (!DBSlotExists("os_info", "taint_states")) {
+	DBQueryFatal("alter table os_info add ".
+		     " `taint_states` set('useronly','blackbox','dangerous') ".
+		     " default NULL");
+    }
+    DBQueryFatal("REPLACE INTO table_regex VALUES ".
+		 "('os_info','taint_states','text','regex',".
+		 "'^[-\\\\w,]*\$',1,128,NULL)");
+
+    if (!DBSlotExists("nodes", "taint_states")) {
+	DBQueryFatal("alter table nodes add ".
+		     " `taint_states` set('useronly','blackbox','dangerous') ".
+		     " default NULL");
+    }
+
+    return 0;
+}
+
+# Local Variables:
+# mode:perl
+# End:
diff --git a/tbsetup/libosload.pm.in b/tbsetup/libosload.pm.in
index e16fbb77a9..c83eb57c3f 100644
--- a/tbsetup/libosload.pm.in
+++ b/tbsetup/libosload.pm.in
@@ -252,6 +252,17 @@ sub osload ($$) {
 	    tberror "$node: Could not map to object!";
 	    goto failednode;
 	}
+
+	# Check to see if the node is tainted.  If so, then the disk
+	# needs to be cleaned up (zeroed).  If there was an explicit request
+	# to zero all node disks, then capture that here too.
+	my $zeronode = 0;
+	if ($nodeobject->IsTainted()) {
+	    $zeronode = 2;  # use maximum firepower.
+	}
+	elsif ($zerofree) {
+	    $zeronode = $zerofree;
+	}
 	
 	# Get default imageid for this node.
 	# NOTE that virtnodes don't have default imageids -- they are only 
@@ -300,6 +311,7 @@ sub osload ($$) {
 	my $defosid;
 	my $maxwait = 0;
 	my @access_keys;
+	my @tstates = ();
 
 	#
 	# Most of the DB work related to images is determining what
@@ -449,6 +461,26 @@ sub osload ($$) {
 
 		my $osid = $rowref->{$partname};
 		if (defined($osid)) {
+		    # Have the node inherit taint states from each OS
+		    # to be loaded on it (or that is already loaded on
+		    # it).  This action is additive, i.e. the node
+		    # will end up with the union of taint states
+		    # across all partition OSes.  We also retain any
+		    # taint states the node had previously; it's
+		    # important not to clear these existing states
+		    # until OS loading and disk zeroing have been
+		    # performed.
+		    my $osinfo = OSinfo->Lookup($osid);
+		    if (defined($osinfo)) {
+			if ($osinfo->IsTainted()) {
+			    # Save new/incoming taint states for later...
+			    push @tstates, $osinfo->GetTaintStates();
+			    $nodeobject->InheritTaintStates($osinfo) == 0 or
+				warn "Node $node could not inherit taint ".
+				     "states from osid $osid\n";
+			}
+		    }
+
 		    my %part = (
 			'node_id' => $node,
 			'partition' => $i,
@@ -545,7 +577,7 @@ sub osload ($$) {
 	    $reload_mode = "UISP";
 	    $reload_func = \&SetupReloadUISP;
 	    $reboot_required = 0; # We don't reboot motes to reload them
-	    $zerofree = 0; # and we don't zero "the disk"
+	    $zeronode = 0; # and we don't zero "the disk"
 	} else {
 	    $reload_mode = "Frisbee";
 	    $reload_func = \&SetupReloadFrisbee;
@@ -589,7 +621,8 @@ sub osload ($$) {
 	    'osid'     => $defosid,
 	    'reboot'   => $reboot_required,
 	    'wait'     => $wait_required,
-	    'zerofree' => $zerofree,
+	    'zerofree' => $zeronode,
+	    'tstates'  => \@tstates,
 	    'prepare'  => $prepare,
 	    'maxwait'  => $maxwait,
 	    'isremote' => $isremote,
@@ -1010,7 +1043,21 @@ sub WaitTillReloadDone($$$$$@)
 		if (!$query_result->numrows) {
 		    print STDERR "osload ($node): left reloading mode at ".`date`
 			if ($debug);
-		    
+
+		    #
+		    # Now reset tainting for the node.  Start off
+		    # with a clean slate. Next, apply any taint states
+		    # previously identified and saved off for the
+		    # still-existing and/or newly-loaded OSes.  Doing this
+		    # now allows us to clear any remnant taint states
+		    # nullified by OS loading and/or disk zeroing.
+		    #
+		    $nodeobject->RemoveTaint();
+		    my $tstates = $reload_info->{$node}{'tstates'};
+		    if (@{$tstates}) {
+			$nodeobject->SetTaintStates(@{$tstates});
+		    }
+
 		    $count--;
 		    $done{$node} = 1;
 		    next;
diff --git a/tbsetup/libosload_new.pm.in b/tbsetup/libosload_new.pm.in
index a937e696eb..4d2036f99c 100644
--- a/tbsetup/libosload_new.pm.in
+++ b/tbsetup/libosload_new.pm.in
@@ -625,6 +625,14 @@ sub osload($$$) {
 	foreach my $k (keys(%{$nodeflags{$node}})) {
 	    $nargs{$k} = $nodeflags{$node}{$k};
 	}
+
+	# XXX: this probably belongs in the code calling into libosload?
+	# Check to see if the node is tainted.  If so, then the disk
+	# needs to be cleaned up (zeroed).
+	if ($nodeobject->IsTainted()) {
+	    $nargs{'zerofree'} = 2;  # use maximum firepower.
+	}
+
 	# Wait, don't do this -- waitmode is global; nowait is per-node!
 	#
         # XXX hack to handle that we would see a 'nowait' flag per-node,
@@ -1136,6 +1144,21 @@ sub WaitTillReloadDone($$$$$@)
 		    }
 		    else {
 			# success!
+
+			#
+			# Now reset tainting for the node.  Start off
+			# with a clean slate. Next, apply any taint states
+			# previously identified and saved off for the
+			# still-existing and/or newly-loaded OSes.  Doing this
+			# now allows us to clear any remnant taint states
+			# nullified by OS loading and/or disk zeroing.
+			#
+			$nodeobject->RemoveTaint();
+			my $tstates = $self->nodeinfo($nodeobject,'tstates');
+			if (@{$tstates}) {
+			    $nodeobject->SetTaintStates(@{$tstates});
+			}
+
 			$count--;
 			$done{$node} = 1;
 			$typeobject->ReloadDone($nodeobject);
@@ -1982,6 +2005,7 @@ sub UpdatePartitions($$)
     #
     my %partitions = ();
     my $curmbrvers = 0;
+    my @tstates = ();
 
     #
     # XXX assumes a DOS MBR, but this is ingrained in the DB schema
@@ -2077,6 +2101,26 @@ sub UpdatePartitions($$)
 
 	    my $osid = $rowref->{$partname};
 	    if (defined($osid)) {
+		# Have the node inherit taint states from each OS
+		# to be loaded on it (or that is already loaded on
+		# it).  This action is additive, i.e. the node
+		# will end up with the union of all taint states
+		# across the OSes set for each disk partition.  We
+		# also retain any taint states the node had
+		# previously; it's important not to clear these
+		# existing states until OS loading and disk
+		# zeroing have been performed.
+		my $osinfo = OSinfo->Lookup($osid);
+		if (defined($osinfo)) {
+		    if ($osinfo->IsTainted()) {
+			# Save new/incoming taint states for later...
+			push @tstates, $osinfo->GetTaintStates();
+			$nodeobject->InheritTaintStates($osinfo) == 0 or
+			    warn "Node $node_id could not inherit taint ".
+			    "states from osid $osid\n";
+		    }
+		}
+
 		my %part = (
 		    'node_id' => $node_id,
 		    'partition' => $i,
@@ -2093,6 +2137,9 @@ sub UpdatePartitions($$)
 	}
     }
 
+    # Store the taint states for this node object
+    $self->nodeinfo($nodeobject,'tstates',\@tstates);
+
     #
     # Now that we have processed all images, update the actual DB
     # partitions table entries for this node.
-- 
GitLab