GObject lifetime tracker

by
4 views 595f19cf...

Description

Keep an eye on newly created objects in GNOME applications

How to Use

Download the script and run it with Frida CLI:

Download Script

Then run with Frida:

frida -U -f YOUR_PACKAGE_NAME -l gobject-lifetime-tracker.js

Replace YOUR_PACKAGE_NAME with the target app's package name.

Source Code

JavaScript
/*
 * E.g. to attach to a running GIMP:
 *
 *   $ frida gimp-2.10 --codeshare oleavr/gobject-lifetime-tracker
 *
 * Then use GIMP's GUI to open an image, and return to the REPL and try:
 *
 *   count()
 *   summarize()
 *   list()
 *
 * You can also snapshot() and later do a diff() to see which objects are new.
 *
 * Use reset() to go back to the initial state.
 */

var ENABLE_REFLOG = false;
var MAX_REFLOG_SIZE = 3;

var instances = {};
var snapshotInstances = {};

function reset() {
  instances = {};
  snapshotInstances = {};
}

function count() {
  return Object.keys(instances).length;
}

function summarize() {
  var countByType = Object.keys(instances)
    .reduce(function (counts, handle) {
      var type = instances[handle].type;
      counts[type] = (counts[type] || 0) + 1;
      return counts;
    }, {});

  var typeNames = Object.keys(countByType);
  typeNames.sort(function (a, b) {
    var result = countByType[b] - countByType[a];
    if (result === 0) {
      return a.localeCompare(b);
    }
    return result;
  });

  if (typeNames.length === 0) {
    console.log('No tracked objects.');
    return;
  }

  var widestTypeName = typeNames
    .reduce(function (widest, name) {
      return Math.max(name.length, widest);
    }, 0);

  var indent = '  ';
  var lines =
    [
      '',
      indent + '*** TOTAL: ' + count() + ' ***',
      '',
      indent + rightAdjust('TYPE', ' ', widestTypeName) + ' | COUNT',
      indent + rightAdjust('=', '=', widestTypeName)    + '=+========',
    ]
    .concat(typeNames.map(function (name) {
      return indent + rightAdjust(name, ' ', widestTypeName) + ' | ' + countByType[name];
    }))
    .concat([
      ''
    ]);
  console.log(lines.join('\n'));
}

function list() {
  var lines = Object.keys(instances)
    .map(function (handle) {
      var details = instances[handle];
      return handle + ': ' + JSON.stringify(details, null, 2);
    });
  console.log(lines.join('\n'));
}

function snapshot() {
  snapshotInstances = Object.keys(instances)
    .reduce(function (result, address) {
      result[address] = true;
      return result;
    }, {});
}

function diff() {
  var newInstances = Object.keys(instances)
    .filter(function (handle) {
      return !snapshotInstances.hasOwnProperty(handle);
    });

  var lines =
    ['*** ' + newInstances.length + ' new instances:']
    .concat(newInstances.map(function (handle) {
      var details = instances[handle];
      return handle + ': ' + JSON.stringify(details, null, 2);
    }));
  console.log(lines.join('\n'));
}

var _glibTypeName = null;
var _glibTypeCache = {};
var _gstListeners = {};

function initialize() {
  var glibTypeNameImpl = Module.findExportByName(null, 'g_type_name');
  if (glibTypeNameImpl === null) {
    console.error('GLib not loaded.');
    console.error('FIXME: Statically linked version could be supported by using the DebugSymbol API.');
    return;
  }

  _glibTypeName = new NativeFunction(glibTypeNameImpl, 'pointer', ['pointer']);

  hook('g_type_create_instance', {
    onLeave: function (retval) {
      instances[retval] = {
        type: glibTypeName(glibTypeFromInstance(retval)),
        creator: backtrace(this.context),
        log: []
      };
    }
  });

  if (ENABLE_REFLOG) {
    hookRefCountFunc('g_object_ref');
    hookRefCountFunc('g_object_unref');
  }

  hook('g_type_free_instance', onFree);

  var gstMiniObjectInitImpl = Module.findExportByName(null, 'gst_mini_object_init');
  if (gstMiniObjectInitImpl !== null) {
    Interceptor.attach(gstMiniObjectInitImpl, function (args) {
      var miniObject = args[0];
      var gtype = args[2];

      var free = args[5];
      if (free.isNull()) {
        return;
      }

      instances[miniObject] = {
        type: glibTypeName(gtype),
        creator: backtrace(this.context),
        log: []
      };

      var key = free.toString();
      if (_gstListeners[key] === undefined) {
        _gstListeners[key] = Interceptor.attach(free, onFree);
      }
    });

    hook('gst_mini_object_copy', {
      onLeave: function (retval) {
        instances[retval] = {
          type: glibTypeName(glibTypeFromMiniObject(retval)),
          creator: backtrace(this.context),
          log: []
        };
      }
    });

    if (ENABLE_REFLOG) {
      hookRefCountFunc('gst_mini_object_ref');
      hookRefCountFunc('gst_mini_object_unref');
    }
  }

  console.log('Ready. GStreamer detected: ' + ((gstMiniObjectInitImpl !== null) ? 'yes' : 'no'));
}

function hookRefCountFunc(name) {
  hook(name, function (args) {
    var details = instances[args[0]];
    if (details === undefined) {
      return;
    }

    var log = details.log;
    log.push({
      event: name,
      caller: backtrace(this.context)
    });
    if (log.length > MAX_REFLOG_SIZE) {
      log.shift();
    }
  });
}

function onFree(args) {
  var handle = args[0];
  delete instances[handle];
}

function glibTypeFromInstance(instance) {
  var klass = instance.readPointer();
  var gtype = klass.readPointer();
  return gtype;
}

function glibTypeFromMiniObject(miniObject) {
  var gtype = miniObject.readPointer();
  return gtype;
}

function glibTypeName(type) {
  var key = type.toString();

  var name = _glibTypeCache[key];
  if (name !== undefined) {
    return name;
  }

  name = _glibTypeName(type).readUtf8String();
  _glibTypeCache[key] = name;

  return name;
}

function hook(name, callbacks) {
  Interceptor.attach(Module.getExportByName(null, name), callbacks);
}

function backtrace(context) {
  return Thread.backtrace(context)
    .map(DebugSymbol.fromAddress)
    .map(function (symbol) {
      return symbol.toString();
    });
}

function rightAdjust(str, character, width) {
  while (str.length < width) {
    str = character + str;
  }
  return str;
}

initialize();
Share this script:
Twitter LinkedIn

Comments

Login or Sign up to leave a comment.
Loading comments...