blob: 6e42aee3ba2aa4885ad5577fc611b8e59998472f [file] [log] [blame]
/*
* Copyright (C) 2013 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.server.am;
import android.app.ApplicationErrorReport.CrashInfo;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructTimeval;
import android.system.UnixSocketAddress;
import android.util.Slog;
import static android.system.OsConstants.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.InterruptedIOException;
/**
* Set up a Unix domain socket that debuggerd will connect() to in
* order to write a description of a native crash. The crash info is
* then parsed and forwarded to the ActivityManagerService's normal
* crash handling code.
*
* Note that this component runs in a separate thread.
*/
final class NativeCrashListener extends Thread {
static final String TAG = "NativeCrashListener";
static final boolean DEBUG = false;
static final boolean MORE_DEBUG = DEBUG && false;
// Must match the path defined in debuggerd.c.
static final String DEBUGGERD_SOCKET_PATH = "/data/system/ndebugsocket";
// Use a short timeout on socket operations and abandon the connection
// on hard errors, just in case debuggerd goes out to lunch.
static final long SOCKET_TIMEOUT_MILLIS = 10000; // 10 seconds
final ActivityManagerService mAm;
/*
* Spin the actual work of handling a debuggerd crash report into a
* separate thread so that the listener can go immediately back to
* accepting incoming connections.
*/
class NativeCrashReporter extends Thread {
ProcessRecord mApp;
int mSignal;
String mCrashReport;
NativeCrashReporter(ProcessRecord app, int signal, String report) {
super("NativeCrashReport");
mApp = app;
mSignal = signal;
mCrashReport = report;
}
@Override
public void run() {
try {
CrashInfo ci = new CrashInfo();
ci.exceptionClassName = "Native crash";
ci.exceptionMessage = Os.strsignal(mSignal);
ci.throwFileName = "unknown";
ci.throwClassName = "unknown";
ci.throwMethodName = "unknown";
ci.stackTrace = mCrashReport;
if (DEBUG) Slog.v(TAG, "Calling handleApplicationCrash()");
mAm.handleApplicationCrashInner("native_crash", mApp, mApp.processName, ci);
if (DEBUG) Slog.v(TAG, "<-- handleApplicationCrash() returned");
} catch (Exception e) {
Slog.e(TAG, "Unable to report native crash", e);
}
}
}
/*
* Daemon thread that accept()s incoming domain socket connections from debuggerd
* and processes the crash dump that is passed through.
*/
NativeCrashListener(ActivityManagerService am) {
mAm = am;
}
@Override
public void run() {
final byte[] ackSignal = new byte[1];
if (DEBUG) Slog.i(TAG, "Starting up");
// The file system entity for this socket is created with 0777 perms, owned
// by system:system. selinux restricts things so that only crash_dump can
// access it.
{
File socketFile = new File(DEBUGGERD_SOCKET_PATH);
if (socketFile.exists()) {
socketFile.delete();
}
}
try {
FileDescriptor serverFd = Os.socket(AF_UNIX, SOCK_STREAM, 0);
final UnixSocketAddress sockAddr = UnixSocketAddress.createFileSystem(
DEBUGGERD_SOCKET_PATH);
Os.bind(serverFd, sockAddr);
Os.listen(serverFd, 1);
Os.chmod(DEBUGGERD_SOCKET_PATH, 0777);
while (true) {
FileDescriptor peerFd = null;
try {
if (MORE_DEBUG) Slog.v(TAG, "Waiting for debuggerd connection");
peerFd = Os.accept(serverFd, null /* peerAddress */);
if (MORE_DEBUG) Slog.v(TAG, "Got debuggerd socket " + peerFd);
if (peerFd != null) {
// the reporting thread may take responsibility for
// acking the debugger; make sure we play along.
consumeNativeCrashData(peerFd);
}
} catch (Exception e) {
Slog.w(TAG, "Error handling connection", e);
} finally {
// Always ack crash_dump's connection to us. The actual
// byte written is irrelevant.
if (peerFd != null) {
try {
Os.write(peerFd, ackSignal, 0, 1);
} catch (Exception e) {
/* we don't care about failures here */
if (MORE_DEBUG) {
Slog.d(TAG, "Exception writing ack: " + e.getMessage());
}
}
try {
Os.close(peerFd);
} catch (ErrnoException e) {
if (MORE_DEBUG) {
Slog.d(TAG, "Exception closing socket: " + e.getMessage());
}
}
}
}
}
} catch (Exception e) {
Slog.e(TAG, "Unable to init native debug socket!", e);
}
}
static int unpackInt(byte[] buf, int offset) {
int b0, b1, b2, b3;
b0 = ((int) buf[offset]) & 0xFF; // mask against sign extension
b1 = ((int) buf[offset+1]) & 0xFF;
b2 = ((int) buf[offset+2]) & 0xFF;
b3 = ((int) buf[offset+3]) & 0xFF;
return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
}
static int readExactly(FileDescriptor fd, byte[] buffer, int offset, int numBytes)
throws ErrnoException, InterruptedIOException {
int totalRead = 0;
while (numBytes > 0) {
int n = Os.read(fd, buffer, offset + totalRead, numBytes);
if (n <= 0) {
if (DEBUG) {
Slog.w(TAG, "Needed " + numBytes + " but saw " + n);
}
return -1; // premature EOF or timeout
}
numBytes -= n;
totalRead += n;
}
return totalRead;
}
// Read a crash report from the connection
void consumeNativeCrashData(FileDescriptor fd) {
if (MORE_DEBUG) Slog.i(TAG, "debuggerd connected");
final byte[] buf = new byte[4096];
final ByteArrayOutputStream os = new ByteArrayOutputStream(4096);
try {
StructTimeval timeout = StructTimeval.fromMillis(SOCKET_TIMEOUT_MILLIS);
Os.setsockoptTimeval(fd, SOL_SOCKET, SO_RCVTIMEO, timeout);
Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO, timeout);
// The socket is guarded by an selinux neverallow rule that only
// permits crash_dump to connect to it. This allows us to trust the
// received values.
// first, the pid and signal number
int headerBytes = readExactly(fd, buf, 0, 8);
if (headerBytes != 8) {
// protocol failure; give up
Slog.e(TAG, "Unable to read from debuggerd");
return;
}
int pid = unpackInt(buf, 0);
int signal = unpackInt(buf, 4);
if (DEBUG) {
Slog.v(TAG, "Read pid=" + pid + " signal=" + signal);
}
// now the text of the dump
if (pid > 0) {
final ProcessRecord pr;
synchronized (mAm.mPidsSelfLocked) {
pr = mAm.mPidsSelfLocked.get(pid);
}
if (pr != null) {
// Don't attempt crash reporting for persistent apps
if (pr.isPersistent()) {
if (DEBUG) {
Slog.v(TAG, "Skipping report for persistent app " + pr);
}
return;
}
int bytes;
do {
// get some data
bytes = Os.read(fd, buf, 0, buf.length);
if (bytes > 0) {
if (MORE_DEBUG) {
String s = new String(buf, 0, bytes, "UTF-8");
Slog.v(TAG, "READ=" + bytes + "> " + s);
}
// did we just get the EOD null byte?
if (buf[bytes-1] == 0) {
os.write(buf, 0, bytes-1); // exclude the EOD token
break;
}
// no EOD, so collect it and read more
os.write(buf, 0, bytes);
}
} while (bytes > 0);
// Okay, we've got the report.
if (DEBUG) Slog.v(TAG, "processing");
// Mark the process record as being a native crash so that the
// cleanup mechanism knows we're still submitting the report
// even though the process will vanish as soon as we let
// debuggerd proceed.
synchronized (mAm) {
pr.setCrashing(true);
pr.forceCrashReport = true;
}
// Crash reporting is synchronous but we want to let debuggerd
// go about it business right away, so we spin off the actual
// reporting logic on a thread and let it take it's time.
final String reportString = new String(os.toByteArray(), "UTF-8");
(new NativeCrashReporter(pr, signal, reportString)).start();
} else {
Slog.w(TAG, "Couldn't find ProcessRecord for pid " + pid);
}
} else {
Slog.e(TAG, "Bogus pid!");
}
} catch (Exception e) {
Slog.e(TAG, "Exception dealing with report", e);
// ugh, fail.
}
}
}