Close the security hole in try_touch_experiments

There was an exploit in try_touch_experiments where a malicious string
could be input that when converted from hex to ascii would produce a
property name that could escape out and run whatever it would like on
the command line when the program shelled out to alter the X properties.

This patch attempts to fix this by two approaches.

First: Input Validation.  There is only a small subset of characters
that are legal in inputs.  The program now does a more thorough job
filtering and will reject any input that contains illegal characters
even after decoding the string from hex->ascii.

Second: No shelling out.  The X properties used to be changed using a
command line tool called xinput.  To prevent this being exploited, I
have converted the program to do it manually.  Now when property.cc
wants to alter a value, it actually connects to the X server itself and
changes the properties directly.

BUG=chromium:351857
TEST=manually tested on Pit and Peppy.  It compiled and was able to run
experiments on each successfully.  Known malicious input was
unsuccessful.

Change-Id: Ie4f0dafbbce2862feb98ba584ab8c16f54a5ea7c
Signed-off-by: Charlie Mooney <charliemooney@chromium.org>
diff --git a/try_touch_experiment/Makefile b/try_touch_experiment/Makefile
index f313762..02bd1ff 100644
--- a/try_touch_experiment/Makefile
+++ b/try_touch_experiment/Makefile
@@ -43,7 +43,9 @@
 
 LDLIBS=\
 	$(PC_LIBS) \
-	-lcurses
+	-lcurses \
+	-lX11 \
+	-lXi
 
 define auto_mkdir
   $(if $(wildcard $(dir $1)),$2,$(QUIET)mkdir -p "$(dir $1)")
diff --git a/try_touch_experiment/property.cc b/try_touch_experiment/property.cc
index f89e556..5d4fa33 100644
--- a/try_touch_experiment/property.cc
+++ b/try_touch_experiment/property.cc
@@ -15,22 +15,25 @@
   return is_valid_;
 }
 
-Property::Property(string const &property_string) {
+Property::Property(string const &property_string) : is_valid_(false) {
   vector<string> parts;
   SplitString(property_string, ':', &parts);
 
   name_ = "";
   if (parts.size() == 2) {
-    name_ = parts[0];
-    value_ = atof(parts[1].c_str());
-    device_ = GetDeviceNumber();
-    property_number_ = GetPropertyNumber();
-    old_value_ = GetCurrentValue();
-    is_valid_ = true;
-  }
+    // Check that the property name contains only legal characters
+    is_valid_ = (parts[0].length() > 0);
+    for (size_t i = 0; i < parts[0].length(); i++) {
+      is_valid_ &= (IsAsciiAlpha(parts[0][i]) || IsAsciiDigit(parts[0][i]) ||
+                    parts[0][i] == ' ' || parts[0][i] == '-');
+    }
 
-  if (name_.length() == 0)
-    is_valid_ = false;
+    if (is_valid_) {
+      name_ = parts[0];
+      value_ = atof(parts[1].c_str());
+      old_value_ = GetCurrentValue();
+    }
+  }
 }
 
 bool Property::Reset() const {
@@ -42,56 +45,131 @@
 }
 
 bool Property::SetValue(double new_value) const {
-  string command = StringPrintf("DISPLAY=:0 xinput set-prop %d %d %f",
-                                device_, property_number_, new_value);
+  // Connect to the X display and device we need
+  Display *display = GetX11Display();
+  XDevice *device = GetX11TouchpadDevice(display);
+  if (!display || !device)
+    return false;
 
-  double current_value = GetCurrentValue();
-  for (int i = 0; i < MAX_RETRIES && current_value != new_value; i++) {
-    RunCommand(command);
-    current_value = GetCurrentValue();
+  // Find the id of the property we want
+  Atom this_prop = XInternAtom(display, name_.c_str(), true);
+
+  // Select the property and see what format it is in
+  Atom type;
+  int format;
+  unsigned long nitems, bytes_after;
+  unsigned char *data;
+  unsigned char *new_data = NULL;
+  if (XGetDeviceProperty(display, device, this_prop, 0, 1000, False,
+                         AnyPropertyType, &type, &format,
+                         &nitems, &bytes_after, &data) == Success) {
+    if (nitems == 1) {
+      // Re-format the value into the type X is expecting
+      int8_t int8_t_val = (int8_t)new_value;
+      int16_t int16_t_val = (int16_t)new_value;
+      int32_t int32_t_val = (int32_t)new_value;
+      float float_val = (float)new_value;
+      Atom float_atom = XInternAtom(display, "FLOAT", True);
+      if (type == XA_INTEGER) {
+        switch(format) {
+          case 8: new_data = (unsigned char*)&int8_t_val; break;
+          case 16: new_data = (unsigned char*)&int16_t_val; break;
+          case 32: new_data = (unsigned char*)&int32_t_val; break;
+        }
+      } else if (float_atom != None && type == float_atom) {
+        new_data = (unsigned char*)&float_val;
+      }
+
+      // If everything went okay, apply the change
+      if (new_data) {
+        XChangeDeviceProperty(display, device, this_prop, type, format,
+                              PropModeReplace, new_data, 1);
+      }
+    }
   }
 
-  return (abs(static_cast<int>(current_value - new_value))
-          <= MAX_ALLOWABLE_DIFFERENCE);
+  // Clean up
+  XCloseDevice(display, device);
+  XCloseDisplay(display);
+
+  return (new_data != NULL);
 }
 
-int Property::GetDeviceNumber() const {
-  return atoi(RunCommand("/opt/google/touchpad/tpcontrol listdev").c_str());
+Display* Property::GetX11Display() const {
+  return XOpenDisplay(NULL);
 }
 
-int Property::GetPropertyNumber() const {
-  string command = StringPrintf("DISPLAY=:0 xinput list-props %d"
-                                " | grep '%s'"
-                                " | sed -e 's/[^(]*(\\([0-9]*\\)):.*/\\1/'",
-                                device_,
-                                name_.c_str());
-  return atoi(RunCommand(command).c_str());
+XDevice* Property::GetX11TouchpadDevice(Display *display) const {
+  if (!display)
+    return NULL;
+
+  // Get a list of the details of all the X devices availible
+  int num_device_infos;
+  XDeviceInfo *device_infos = XListInputDevices(display, &num_device_infos);
+
+  // Go through the list and find our touchpad
+  XDevice *touchpad_device = NULL;
+  for(int i = 0; i < num_device_infos; i++) {
+    if (device_infos[i].use != IsXExtensionPointer)
+      continue;
+
+    XDevice *tmp_device = XOpenDevice(display, device_infos[i].id);
+    if (!tmp_device)
+      continue;
+
+    // The touchpad will have a "Device Touchpad" Property set to 1.0
+    double val = GetPropertyValue(display, tmp_device, "Device Touchpad");
+    if (val == 1.0) {
+      touchpad_device = tmp_device;
+      break;
+    } else {
+      XCloseDevice(display, tmp_device);
+    }
+  }
+
+  return touchpad_device;
+}
+
+double Property::GetPropertyValue(Display *display, XDevice *device,
+                                  const char* name) const {
+  // Find the id of the property we want
+  Atom this_prop = XInternAtom(display, name, true);
+
+  // Parse the value of this property
+  Atom type;
+  int format;
+  unsigned long nitems, bytes_after;
+  unsigned char *data;
+  double val = -1;
+  if (XGetDeviceProperty(display, device, this_prop, 0, 1000, False,
+                         AnyPropertyType, &type, &format,
+                         &nitems, &bytes_after, &data) == Success) {
+    if (nitems == 1) {
+      Atom float_atom = XInternAtom(display, "FLOAT", True);
+      if (type == XA_INTEGER) {
+        switch(format) {
+          case 8: val = *((int8_t*)data); break;
+          case 16: val = *((int16_t*)data); break;
+          case 32: val = *((int32_t*)data); break;
+        }
+      } else if (float_atom != None && type == float_atom) {
+        val = *((float*)data);
+      }
+    }
+  }
+
+  return val;
 }
 
 double Property::GetCurrentValue() const {
-  string command = StringPrintf("DISPLAY=:0 xinput list-props %d"
-                                " | grep '%s'"
-                                " | sed -e 's/[^:]*:\\s*\\([-0-9.]*\\)$/\\1/'",
-                                device_,
-                                name_.c_str());
-  return atoi(RunCommand(command).c_str());
-}
+  Display *display = GetX11Display();
+  XDevice *device = GetX11TouchpadDevice(display);
+  if (!display || !device)
+    return -1.0;
 
-string Property::RunCommand(string const &command) const {
-  // Run a command from a shell and return the contents of stdout
-  FILE* pipe = popen(command.c_str(), "r");
-  if (!pipe)
-    return "";
+  double val = GetPropertyValue(display, device, name_.c_str());
 
-  char buffer[256];
-  string result = "";
-  while (!feof(pipe)) {
-    if (fgets(buffer, 256, pipe) != NULL)
-      result += buffer;
-  }
-
-  pclose(pipe);
-
-  TrimWhitespaceASCII(result, TRIM_ALL, &result);
-  return result;
+  XCloseDevice(display, device);
+  XCloseDisplay(display);
+  return val;
 }
diff --git a/try_touch_experiment/property.h b/try_touch_experiment/property.h
index b1ae903..f06a783 100644
--- a/try_touch_experiment/property.h
+++ b/try_touch_experiment/property.h
@@ -9,6 +9,13 @@
 #include <stdlib.h>
 #include <string>
 #include <vector>
+
+#include <X11/Xatom.h>
+#include <X11/extensions/XInput.h>
+#include <X11/Xlib.h>
+#include <X11/Xresource.h>
+#include <X11/Xutil.h>
+
 #include <base/strings/stringprintf.h>
 #include <base/strings/string_split.h>
 #include <base/strings/string_util.h>
@@ -28,16 +35,17 @@
 
   private:
     double GetCurrentValue() const;
-    int GetPropertyNumber() const;
     int GetDeviceNumber() const;
-    std::string RunCommand(std::string const &command) const;
     bool SetValue(double new_value) const;
 
+    Display* GetX11Display() const;
+    XDevice* GetX11TouchpadDevice(Display *display) const;
+    double GetPropertyValue(Display *display, XDevice *device,
+                            const char* name) const;
+
     std::string name_;
     double value_;
     double old_value_;
-    int device_;
-    int property_number_;
 
     bool is_valid_;
 };
diff --git a/try_touch_experiment/salsa_experiment_runner.cc b/try_touch_experiment/salsa_experiment_runner.cc
index d46ec18..eaba492 100644
--- a/try_touch_experiment/salsa_experiment_runner.cc
+++ b/try_touch_experiment/salsa_experiment_runner.cc
@@ -25,12 +25,26 @@
   string decoded_string = "";
   for (string::const_iterator it = exp_string.begin();
        it != exp_string.end(); ++it) {
-    int val1 = HexDigitToInt(*it);
-    int val2 = HexDigitToInt(*++it);
-    if (val1 < 0 || val2 < 0)
+    char c1 = *it;
+    char c2 = *++it;
+
+    if (IsHexDigit(c1) && IsHexDigit(c2)) {
+      int val1 = HexDigitToInt(c1);
+      int val2 = HexDigitToInt(c2);
+      char converted_char = static_cast<char>(val1 * 16 + val2);
+
+      // After decoding, these should be the only characters in the string
+      if (IsAsciiAlpha(converted_char) || IsAsciiDigit(converted_char) ||
+          converted_char == '+' || converted_char == ',' ||
+          converted_char == ':' || converted_char == '-' ||
+          converted_char == ' ' || converted_char == '.') {
+        decoded_string.push_back(converted_char);
+      } else {
+        return "";
+      }
+    } else {
       return "";
-    else
-      decoded_string.push_back(static_cast<char>(val1 * 16 + val2));
+    }
   }
 
   return decoded_string;