am 91678546: am accc6844: CtsVerifier Suid File Scanner

Merge commit '916785461c85787f7212e0f0772643a9b2dee1ab' into gingerbread

* commit '916785461c85787f7212e0f0772643a9b2dee1ab':
  CtsVerifier Suid File Scanner
diff --git a/apps/CtsVerifier/Android.mk b/apps/CtsVerifier/Android.mk
index 7bc2249..112b1bf 100644
--- a/apps/CtsVerifier/Android.mk
+++ b/apps/CtsVerifier/Android.mk
@@ -25,6 +25,8 @@
 
 LOCAL_PACKAGE_NAME := CtsVerifier
 
+LOCAL_JNI_SHARED_LIBRARIES := libctsverifier_jni
+
 LOCAL_SDK_VERSION := current
 
 include $(BUILD_PACKAGE)
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index 605404a..08d96e8 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -19,6 +19,8 @@
       package="com.android.cts.verifier"
       android:versionCode="1"
       android:versionName="1.0">
+      
+    <uses-sdk android:minSdkVersion="5" />
 
     <application android:label="@string/app_name">
 
@@ -31,7 +33,9 @@
 
         <activity android:name=".TestListActivity" android:label="@string/test_list_title" />
 
-        <activity android:name=".suid.SuidBinariesActivity" android:label="@string/suid_binaries">
+        <activity android:name=".suid.SuidFilesActivity" 
+                android:label="@string/suid_files"
+                android:configChanges="keyboardHidden|orientation">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.cts.intent.category.MANUAL_TEST" />
diff --git a/apps/CtsVerifier/jni/Android.mk b/apps/CtsVerifier/jni/Android.mk
new file mode 100644
index 0000000..98a4678
--- /dev/null
+++ b/apps/CtsVerifier/jni/Android.mk
@@ -0,0 +1,32 @@
+#
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := libctsverifier_jni
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PRELINK_MODULE := false
+
+LOCAL_SRC_FILES := \
+		CtsVerifierJniOnLoad.cpp \
+		com_android_cts_verifier_os_FileUtils.cpp	
+
+LOCAL_C_INCLUDES := $(JNI_H_INCLUDE)
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/suid/SuidBinariesActivity.java b/apps/CtsVerifier/jni/CtsVerifierJniOnLoad.cpp
similarity index 61%
rename from apps/CtsVerifier/src/com/android/cts/verifier/suid/SuidBinariesActivity.java
rename to apps/CtsVerifier/jni/CtsVerifierJniOnLoad.cpp
index 9f973bc..81e5690 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/suid/SuidBinariesActivity.java
+++ b/apps/CtsVerifier/jni/CtsVerifierJniOnLoad.cpp
@@ -1,4 +1,4 @@
-/*
+/* 
  * Copyright (C) 2010 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,15 +14,21 @@
  * limitations under the License.
  */
 
-package com.android.cts.verifier.suid;
+#include <jni.h>
+#include <stdio.h>
 
-import android.app.Activity;
-import android.os.Bundle;
+extern int register_com_android_cts_verifier_os_FileUtils(JNIEnv*);
 
-public class SuidBinariesActivity extends Activity {
+jint JNI_OnLoad(JavaVM *vm, void *reserved) {
+    JNIEnv *env = NULL;
 
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
+    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
+        return JNI_ERR;
     }
+
+    if (register_com_android_cts_verifier_os_FileUtils(env)) {
+        return JNI_ERR;
+    }
+
+    return JNI_VERSION_1_4;
 }
diff --git a/apps/CtsVerifier/jni/com_android_cts_verifier_os_FileUtils.cpp b/apps/CtsVerifier/jni/com_android_cts_verifier_os_FileUtils.cpp
new file mode 100644
index 0000000..14e58eb
--- /dev/null
+++ b/apps/CtsVerifier/jni/com_android_cts_verifier_os_FileUtils.cpp
@@ -0,0 +1,114 @@
+/* 
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <jni.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <grp.h>
+#include <pwd.h>
+
+static jclass gFileStatusClass;
+static jfieldID gFileStatusDevFieldID;
+static jfieldID gFileStatusInoFieldID;
+static jfieldID gFileStatusModeFieldID;
+static jfieldID gFileStatusNlinkFieldID;
+static jfieldID gFileStatusUidFieldID;
+static jfieldID gFileStatusGidFieldID;
+static jfieldID gFileStatusSizeFieldID;
+static jfieldID gFileStatusBlksizeFieldID;
+static jfieldID gFileStatusBlocksFieldID;
+static jfieldID gFileStatusAtimeFieldID;
+static jfieldID gFileStatusMtimeFieldID;
+static jfieldID gFileStatusCtimeFieldID;
+
+/* Copied from hidden API: frameworks/base/core/jni/android_os_FileUtils.cpp */
+jboolean com_android_cts_verifier_os_FileUtils_getFileStatus(JNIEnv* env, jobject thiz,
+        jstring path, jobject fileStatus, jboolean statLinks)
+{
+    const char* pathStr = env->GetStringUTFChars(path, NULL);
+    jboolean ret = false;
+    struct stat s;
+
+    int res = statLinks == true ? lstat(pathStr, &s) : stat(pathStr, &s);
+
+    if (res == 0) {
+        ret = true;
+        if (fileStatus != NULL) {
+            env->SetIntField(fileStatus, gFileStatusDevFieldID, s.st_dev);
+            env->SetIntField(fileStatus, gFileStatusInoFieldID, s.st_ino);
+            env->SetIntField(fileStatus, gFileStatusModeFieldID, s.st_mode);
+            env->SetIntField(fileStatus, gFileStatusNlinkFieldID, s.st_nlink);
+            env->SetIntField(fileStatus, gFileStatusUidFieldID, s.st_uid);
+            env->SetIntField(fileStatus, gFileStatusGidFieldID, s.st_gid);
+            env->SetLongField(fileStatus, gFileStatusSizeFieldID, s.st_size);
+            env->SetIntField(fileStatus, gFileStatusBlksizeFieldID, s.st_blksize);
+            env->SetLongField(fileStatus, gFileStatusBlocksFieldID, s.st_blocks);
+            env->SetLongField(fileStatus, gFileStatusAtimeFieldID, s.st_atime);
+            env->SetLongField(fileStatus, gFileStatusMtimeFieldID, s.st_mtime);
+            env->SetLongField(fileStatus, gFileStatusCtimeFieldID, s.st_ctime);
+        }
+    }
+
+    env->ReleaseStringUTFChars(path, pathStr);
+
+    return ret;
+}
+
+jstring com_android_cts_verifier_os_FileUtils_getUserName(JNIEnv* env, jobject thiz,
+        jint uid)
+{
+    struct passwd *pwd = getpwuid(uid);
+    return env->NewStringUTF(pwd->pw_name);
+}
+
+jstring com_android_cts_verifier_os_FileUtils_getGroupName(JNIEnv* env, jobject thiz,
+        jint gid)
+{
+    struct group *grp = getgrgid(gid);
+    return env->NewStringUTF(grp->gr_name);
+}
+
+static JNINativeMethod gMethods[] = {
+    {  "getFileStatus", "(Ljava/lang/String;Lcom/android/cts/verifier/os/FileUtils$FileStatus;Z)Z",
+            (void *) com_android_cts_verifier_os_FileUtils_getFileStatus  },
+    {  "getUserName", "(I)Ljava/lang/String;",
+            (void *) com_android_cts_verifier_os_FileUtils_getUserName  },
+    {  "getGroupName", "(I)Ljava/lang/String;",
+            (void *) com_android_cts_verifier_os_FileUtils_getGroupName  },
+};
+
+int register_com_android_cts_verifier_os_FileUtils(JNIEnv* env)
+{
+    jclass clazz = env->FindClass("com/android/cts/verifier/os/FileUtils");
+
+    gFileStatusClass = env->FindClass("com/android/cts/verifier/os/FileUtils$FileStatus");
+    gFileStatusDevFieldID = env->GetFieldID(gFileStatusClass, "dev", "I");
+    gFileStatusInoFieldID = env->GetFieldID(gFileStatusClass, "ino", "I");
+    gFileStatusModeFieldID = env->GetFieldID(gFileStatusClass, "mode", "I");
+    gFileStatusNlinkFieldID = env->GetFieldID(gFileStatusClass, "nlink", "I");
+    gFileStatusUidFieldID = env->GetFieldID(gFileStatusClass, "uid", "I");
+    gFileStatusGidFieldID = env->GetFieldID(gFileStatusClass, "gid", "I");
+    gFileStatusSizeFieldID = env->GetFieldID(gFileStatusClass, "size", "J");
+    gFileStatusBlksizeFieldID = env->GetFieldID(gFileStatusClass, "blksize", "I");
+    gFileStatusBlocksFieldID = env->GetFieldID(gFileStatusClass, "blocks", "J");
+    gFileStatusAtimeFieldID = env->GetFieldID(gFileStatusClass, "atime", "J");
+    gFileStatusMtimeFieldID = env->GetFieldID(gFileStatusClass, "mtime", "J");
+    gFileStatusCtimeFieldID = env->GetFieldID(gFileStatusClass, "ctime", "J");
+
+    return env->RegisterNatives(clazz, gMethods, 
+            sizeof(gMethods) / sizeof(JNINativeMethod)); 
+}
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 57c0b81..2f62a06 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -18,16 +18,23 @@
     <string name="welcome_text">Welcome to the CTS Verifier!</string>
     <string name="continue_button_text">Continue</string>
     <string name="test_list_title">Manual Test List</string>
-    <string name="suid_binaries">SUID Binaries</string>
 
-    <!-- strings for FeatureSummaryActivity -->
+    <!-- Strings for FeatureSummaryActivity -->
     <string name="feature_summary">Hardware/Software Feature Summary</string>
     <string name="fs_disallowed">WARNING: device reports a disallowed feature name</string>
     <string name="fs_missing_wifi_telephony">WARNING: device reports neither WiFi nor telephony</string>
     <string name="fs_no_data">No data.</string>
     <string name="empty"></string>
 
-    <!-- strings for AccelerometerTestActivity and MagnetometerTestActivity -->
+    <!-- Strings for AccelerometerTestActivity and MagnetometerTestActivity -->
     <string name="snsr_accel_test">Accelerometer Test</string>
     <string name="snsr_mag_test">Magnetometer Test</string>
+
+    <!-- Strings for SuidFilesActivity -->
+    <string name="suid_files">SUID Files</string>
+    <string name="starting_scan">Starting scan...</string>
+    <string name="file_status">User: %s\nGroup: %s\nPermissions: %s\nPath: %s</string>
+    <string name="no_file_status">Could not stat file...</string>
+    <string name="congratulations">Congratulations!</string>
+    <string name="no_suid_files">No unauthorized suid files detected!</string>
 </resources>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
index 143f44d..9bf06ee 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
@@ -29,6 +29,8 @@
 import android.widget.SimpleAdapter;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -91,6 +93,14 @@
                 addItem(data, title, intent);
             }
 
+            Collections.sort(data, new Comparator<Map<String, ?>> () {
+                public int compare(Map<String, ?> item, Map<String, ?> otherItem) {
+                    String title = (String) item.get(TITLE);
+                    String otherTitle = (String) otherItem.get(TITLE);
+                    return title.compareTo(otherTitle);
+                }
+            });
+
             return data;
         }
 
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/os/FileUtils.java b/apps/CtsVerifier/src/com/android/cts/verifier/os/FileUtils.java
new file mode 100644
index 0000000..c767e7a
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/os/FileUtils.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.os;
+
+/** Bits and pieces copied from hidden API of android.os.FileUtils. */
+public class FileUtils {
+
+    private static final int S_IFSOCK = 0140000;
+    private static final int S_IFLNK = 0120000;
+    private static final int S_IFREG = 0100000;
+    private static final int S_IFBLK = 0060000;
+    private static final int S_IFDIR = 0040000;
+    private static final int S_IFCHR = 0020000;
+    private static final int S_IFIFO = 0010000;
+
+    private static final int S_ISUID = 0004000;
+    private static final int S_ISGID = 0002000;
+    private static final int S_ISVTX = 0001000;
+
+    private static final int S_IRUSR = 00400;
+    private static final int S_IWUSR = 00200;
+    private static final int S_IXUSR = 00100;
+
+    private static final int S_IRGRP = 00040;
+    private static final int S_IWGRP = 00020;
+    private static final int S_IXGRP = 00010;
+
+    private static final int S_IROTH = 00004;
+    private static final int S_IWOTH = 00002;
+    private static final int S_IXOTH = 00001;
+
+    static {
+        System.loadLibrary("ctsverifier_jni");
+    }
+
+    public static class FileStatus {
+
+        private int dev;
+        private int ino;
+        private int mode;
+        private int nlink;
+        private int uid;
+        private int gid;
+        private int rdev;
+        private long size;
+        private int blksize;
+        private long blocks;
+        private long atime;
+        private long mtime;
+        private long ctime;
+
+        public int getUid() {
+            return uid;
+        }
+
+        public int getGid() {
+            return gid;
+        }
+
+        public int getMode() {
+            return mode;
+        }
+
+        public boolean isDirectory() {
+            return hasModeFlag(mode, S_IFDIR);
+        }
+
+        public boolean isSymbolicLink() {
+            return hasModeFlag(mode, S_IFLNK);
+        }
+
+        public boolean isSetUid() {
+            return hasModeFlag(mode, S_ISUID);
+        }
+
+        public boolean isSetGid() {
+            return hasModeFlag(mode, S_ISGID);
+        }
+    }
+
+    /**
+     * @param path of the file to stat
+     * @param status object to set the fields on
+     * @param statLinks or don't stat links (lstat vs stat)
+     * @return whether or not we were able to stat the file
+     */
+    public native static boolean getFileStatus(String path, FileStatus status, boolean statLinks);
+
+    public native static String getUserName(int uid);
+
+    public native static String getGroupName(int gid);
+
+    /** Display the file's mode like "ls -l" does. */
+    public static String getFormattedPermissions(int mode) {
+        StringBuilder permissions = new StringBuilder("-rwxrwxrwx");
+
+        int[] typeMasks = {S_IFSOCK, S_IFLNK, S_IFREG, S_IFBLK, S_IFDIR, S_IFCHR, S_IFIFO};
+        char[] typeSymbols = {'s', 'l', '-', 'b', 'd', 'c', 'p'};
+        for (int i = 0; i < typeMasks.length; i++) {
+            if (hasModeFlag(mode, typeMasks[i])) {
+                permissions.setCharAt(0, typeSymbols[i]);
+                break;
+            }
+        }
+
+        int[] masks = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP,
+                S_IROTH, S_IWOTH, S_IXOTH};
+        for (int i = 0; i < masks.length; i++) {
+            if (!hasModeFlag(mode, masks[i])) {
+                permissions.setCharAt(1 + i, '-');
+            }
+        }
+
+
+        if (hasModeFlag(mode, S_ISUID)) {
+            permissions.setCharAt(3, hasModeFlag(mode, S_IXUSR) ? 's' : 'S');
+        }
+
+        if (hasModeFlag(mode, S_ISGID)) {
+            permissions.setCharAt(6, hasModeFlag(mode, S_IXGRP) ? 's' : 'S');
+        }
+
+        if (hasModeFlag(mode, S_ISVTX)) {
+            permissions.setCharAt(9, hasModeFlag(mode, S_IXOTH) ? 't' : 'T');
+        }
+
+        return permissions.toString();
+    }
+
+    private static boolean hasModeFlag(int mode, int flag) {
+        return (mode & flag) == flag;
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/suid/SuidFilesActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/suid/SuidFilesActivity.java
new file mode 100644
index 0000000..c89b8b4
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/suid/SuidFilesActivity.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.suid;
+
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.os.FileUtils;
+import com.android.cts.verifier.os.FileUtils.FileStatus;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/** {@link Activity} that tries to find suid files. */
+public class SuidFilesActivity extends ListActivity {
+
+    private static final String TAG = SuidFilesActivity.class.getSimpleName();
+
+    /** These programs are expected suid binaries. */
+    private static final Set<String> WHITELIST = new HashSet<String>(Arrays.asList(
+            "run-as"
+    ));
+
+    private ProgressDialog mProgressDialog;
+
+    private SuidFilesAdapter mAdapter;
+
+    private SuidFilesTask mFindSuidFilesTask;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mAdapter = new SuidFilesAdapter();
+        setListAdapter(mAdapter);
+
+        mProgressDialog = new ProgressDialog(this);
+        mProgressDialog.setMessage(getString(R.string.starting_scan));
+        mProgressDialog.setOnCancelListener(new OnCancelListener() {
+            public void onCancel(DialogInterface dialog) {
+                // If the scanning dialog is cancelled, then stop the task and finish the activity
+                // to prevent the user from just seeing a blank listview.
+                if (mFindSuidFilesTask != null) {
+                    mFindSuidFilesTask.cancel(true);
+                }
+                finish();
+            }
+        });
+
+        // Start searching for suid files using a background thread.
+        mFindSuidFilesTask = new SuidFilesTask();
+        mFindSuidFilesTask.execute(new File("/"));
+    }
+
+    @Override
+    protected void onListItemClick(ListView listView, View view, int position, long id) {
+        super.onListItemClick(listView, view, position, id);
+        File file = mAdapter.getItem(position);
+        String message = getMessage(file);
+        new AlertDialog.Builder(this)
+                .setTitle(file.getName())
+                .setMessage(message)
+                .show();
+    }
+
+    private String getMessage(File file) {
+        FileStatus status = new FileStatus();
+        if (FileUtils.getFileStatus(file.getAbsolutePath(), status, true)) {
+            return getString(R.string.file_status,
+                    FileUtils.getUserName(status.getUid()),
+                    FileUtils.getGroupName(status.getGid()),
+                    FileUtils.getFormattedPermissions(status.getMode()),
+                    file.getAbsolutePath());
+        } else {
+            return getString(R.string.no_file_status);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mFindSuidFilesTask != null) {
+            mFindSuidFilesTask.cancel(true);
+        }
+    }
+
+    /** {@link ListView} items display the basenames of the suid files. */
+    class SuidFilesAdapter extends ArrayAdapter<File> {
+
+        SuidFilesAdapter() {
+            super(SuidFilesActivity.this, android.R.layout.simple_list_item_1);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            TextView view = (TextView) super.getView(position, convertView, parent);
+            File file = getItem(position);
+            view.setText(file.getName());
+            return view;
+        }
+    }
+
+    /** {@link AsyncTask} that searches the file system for suid files. */
+    class SuidFilesTask extends AsyncTask<File, File, Set<File>> {
+
+        @Override
+        protected void onPreExecute() {
+            super.onPreExecute();
+            mProgressDialog.show();
+        }
+
+        @Override
+        protected Set<File> doInBackground(File... paths) {
+            Set<File> suidFiles = new HashSet<File>();
+            DirectoryFileFilter dirFilter = new DirectoryFileFilter();
+            SuidFileFilter suidFilter = new SuidFileFilter();
+            for (File path : paths) {
+                findSuidFiles(path, suidFiles, dirFilter, suidFilter);
+            }
+            return suidFiles;
+        }
+
+        private void findSuidFiles(File dir, Set<File> foundSuidFiles,
+                DirectoryFileFilter dirFilter, SuidFileFilter suidFilter) {
+
+            // Recursively traverse sub directories...
+            File[] subDirs = dir.listFiles(dirFilter);
+            if (subDirs != null && subDirs.length > 0) {
+                for (File subDir : subDirs) {
+                    findSuidFiles(subDir, foundSuidFiles, dirFilter, suidFilter);
+                }
+            }
+
+            // / ...then inspect files in directory to find offending binaries.
+            publishProgress(dir);
+            File[] suidFiles = dir.listFiles(suidFilter);
+            if (suidFiles != null && suidFiles.length > 0) {
+                Collections.addAll(foundSuidFiles, suidFiles);
+            }
+        }
+
+        /** {@link FileFilter} that returns only directories that are not symbolic links. */
+        private class DirectoryFileFilter implements FileFilter {
+
+            private final FileStatus status = new FileStatus();
+
+            public boolean accept(File pathname) {
+                // Don't follow symlinks to avoid infinite looping.
+                if (FileUtils.getFileStatus(pathname.getPath(), status, true)) {
+                    return status.isDirectory() && !status.isSymbolicLink();
+                } else {
+                    Log.w(TAG, "Could not stat " + pathname);
+                    return false;
+                }
+            }
+        }
+
+        /** {@link FileFilter} that returns files that have setuid root or setgid root. */
+        private class SuidFileFilter implements FileFilter {
+
+            private final FileStatus status = new FileStatus();
+
+            public boolean accept(File pathname) {
+                if (!WHITELIST.contains(pathname.getName())
+                        && FileUtils.getFileStatus(pathname.getPath(), status, true)) {
+                    return !status.isDirectory()
+                            && !status.isSymbolicLink()
+                            && status.isSetUid();
+                } else {
+                    Log.w(TAG, "Could not stat " + pathname);
+                    return false;
+                }
+            }
+        }
+
+        @Override
+        protected void onPostExecute(Set<File> results) {
+            super.onPostExecute(results);
+            mProgressDialog.hide();
+
+            // Task could be cancled and results could be null but don't bother doing anything.
+            if (results != null) {
+                for (File result : results) {
+                    mAdapter.add(result);
+                }
+
+                // Alert the user that nothing was found rather than showing an empty list view.
+                if (results.isEmpty()) {
+                    new AlertDialog.Builder(SuidFilesActivity.this)
+                            .setTitle(R.string.congratulations)
+                            .setMessage(R.string.no_suid_files)
+                            .setOnCancelListener(new OnCancelListener() {
+                                public void onCancel(DialogInterface dialog) {
+                                    // No reason to hang around if there were no offending files.
+                                    finish();
+                                }
+                            })
+                            .show();
+                }
+            }
+        }
+
+        @Override
+        protected void onProgressUpdate(File... values) {
+            super.onProgressUpdate(values);
+
+            // Show the current directory being scanned...
+            mProgressDialog.setMessage(values[0].getAbsolutePath());
+        }
+    }
+}