Initial commit
Nothing has actually been tested yet.
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# make gitignore
|
||||
/KoboRoot
|
||||
/src/libnmi.so
|
||||
|
||||
/src/qtplugin.moc
|
||||
/src/qtplugin.o
|
||||
|
||||
/src/failsafe.o
|
||||
/src/init.o
|
||||
/src/subsys_c.o
|
||||
/src/config.o
|
||||
/src/dlhook.o
|
||||
|
||||
/src/menu.o
|
||||
/src/subsys_cc.o
|
||||
94
Makefile
Normal file
94
Makefile
Normal file
@@ -0,0 +1,94 @@
|
||||
CROSS_COMPILE = arm-nickel-linux-gnueabihf-
|
||||
MOC = moc
|
||||
CC = $(CROSS_COMPILE)gcc
|
||||
CXX = $(CROSS_COMPILE)g++
|
||||
PKG_CONFIG = $(CROSS_COMPILE)pkg-config
|
||||
|
||||
DESTDIR =
|
||||
|
||||
ifneq ($(if $(MAKECMDGOALS),$(if $(filter-out clean gitignore install koboroot,$(MAKECMDGOALS)),YES,NO),YES),YES)
|
||||
$(info -- Skipping configure)
|
||||
else
|
||||
PTHREAD_CFLAGS := -pthread
|
||||
PTHREAD_LIBS := -pthread
|
||||
|
||||
define pkgconf =
|
||||
$(if $(filter-out undefined,$(origin $(1)_CFLAGS) $(origin $(1)_LIBS)) \
|
||||
,$(info -- Using provided CFLAGS and LIBS for $(2)) \
|
||||
,$(if $(shell $(PKG_CONFIG) --exists $(2) >/dev/null 2>/dev/null && echo y) \
|
||||
,$(info -- Found $(2) ($(shell $(PKG_CONFIG) --modversion $(2))) with pkg-config) \
|
||||
$(eval $(1)_CFLAGS := $(shell $(PKG_CONFIG) --silence-errors --cflags $(2))) \
|
||||
$(eval $(1)_LIBS := $(shell $(PKG_CONFIG) --silence-errors --libs $(2))) \
|
||||
$(if $(3) \
|
||||
,$(if $(shell $(PKG_CONFIG) $(3) $(2) >/dev/null 2>/dev/null && echo y) \
|
||||
,$(info .. Satisfies constraint $(3)) \
|
||||
,$(info .. Does not satisfy constraint $(3)) \
|
||||
$(error Dependencies do not satisfy constraints)) \
|
||||
,) \
|
||||
,$(info -- Could not automatically detect $(2) with pkg-config. Please specify $(1)_CFLAGS and/or $(1)_LIBS manually) \
|
||||
$(error Missing dependencies)))
|
||||
endef
|
||||
|
||||
$(call pkgconf,QT5CORE,Qt5Core)
|
||||
$(call pkgconf,QT5WIDGETS,Qt5Widgets)
|
||||
|
||||
# temporary workaround for broken cflags in kobo-toolchain Docker image:
|
||||
QT5CORE_CFLAGS := $(shell echo "$(QT5CORE_CFLAGS)" | sed 's:/toolchain/arm-nickel-linux-gnueabihf/arm-nickel-linux-gnueabihf/sysroot/toolchain/arm-nickel-linux-gnueabihf/arm-nickel-linux-gnueabihf/sysroot/:/toolchain/arm-nickel-linux-gnueabihf/arm-nickel-linux-gnueabihf/sysroot/:g')
|
||||
QT5WIDGETS_CFLAGS := $(shell echo "$(QT5WIDGETS_CFLAGS)" | sed 's:/toolchain/arm-nickel-linux-gnueabihf/arm-nickel-linux-gnueabihf/sysroot/toolchain/arm-nickel-linux-gnueabihf/arm-nickel-linux-gnueabihf/sysroot/:/toolchain/arm-nickel-linux-gnueabihf/arm-nickel-linux-gnueabihf/sysroot/:g')
|
||||
override CFLAGS += -I/toolchain/arm-nickel-linux-gnueabihf/arm-nickel-linux-gnueabihf/sysroot/include
|
||||
override CXXFLAGS += -I/toolchain/arm-nickel-linux-gnueabihf/arm-nickel-linux-gnueabihf/sysroot/include
|
||||
|
||||
override CFLAGS += -march=armv7-a -mtune=cortex-a8 -mfpu=neon -mfloat-abi=hard -mthumb -std=gnu11
|
||||
override CXXFLAGS += -march=armv7-a -mtune=cortex-a8 -mfpu=neon -mfloat-abi=hard -mthumb -std=gnu++11
|
||||
override LDFLAGS += -Wl,-rpath,/usr/local/Kobo -Wl,-rpath,/usr/local/Qt-5.2.1-arm/lib
|
||||
endif
|
||||
|
||||
all: src/libnmi.so
|
||||
|
||||
clean:
|
||||
rm -f $(GENERATED)
|
||||
|
||||
gitignore:
|
||||
echo '# make gitignore' > .gitignore
|
||||
echo '$(GENERATED)' | \
|
||||
sed 's/ /\n/g' | \
|
||||
sed 's/^./\/&/' >> .gitignore
|
||||
|
||||
install:
|
||||
install -Dm644 src/libnmi.so $(DESTDIR)/usr/local/Kobo/plugins/libnmi.so
|
||||
install -Dm644 res/doc $(DESTDIR)/mnt/onboard/.adds/nmi/doc
|
||||
|
||||
koboroot:
|
||||
make install DESTDIR=KoboRoot
|
||||
tar czf KoboRoot.tgz -C KoboRoot .; rm -rf KoboRoot
|
||||
|
||||
.PHONY: all clean gitignore install koboroot
|
||||
override GENERATED += KoboRoot
|
||||
|
||||
src/libnmi.so: override CFLAGS += $(PTHREAD_CFLAGS) -fPIC
|
||||
src/libnmi.so: override CXXFLAGS += $(PTHREAD_CFLAGS) $(QT5CORE_CFLAGS) $(QT5WIDGETS_CFLAGS) -fPIC
|
||||
src/libnmi.so: override LDFLAGS += $(PTHREAD_LIBS) $(QT5CORE_LIBS) $(QT5WIDGETS_LIBS) -ldl -Wl,-soname,libnmi.so
|
||||
src/libnmi.so: src/qtplugin.o src/init.o src/config.o src/dlhook.o src/failsafe.o src/menu.o src/subsys_c.o src/subsys_cc.o
|
||||
|
||||
override LIBRARIES += src/libnmi.so
|
||||
override MOCS += src/qtplugin.moc
|
||||
|
||||
define patw =
|
||||
$(foreach dir,src res,$(dir)/*$(1))
|
||||
endef
|
||||
define rpatw =
|
||||
$(patsubst %$(1),%$(2),$(foreach w,$(call patw,$(1)),$(wildcard $(w))))
|
||||
endef
|
||||
|
||||
$(LIBRARIES): src/%.so:
|
||||
$(CC) -shared -o $@ $^ $(LDFLAGS)
|
||||
$(MOCS): %.moc: %.h
|
||||
$(MOC) $< -o $@
|
||||
$(patsubst %.moc,%.o,$(MOCS)): %.o: %.moc
|
||||
$(CXX) -xc++ $(CXXFLAGS) -c $< -o $@
|
||||
$(call rpatw,.c,.o): %.o: %.c
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
$(call rpatw,.cc,.o): %.o: %.cc
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
override GENERATED += $(LIBRARIES) $(MOCS) $(patsubst %.moc,%.o,$(MOCS)) $(call rpatw,.c,.o) $(call rpatw,.cc,.o)
|
||||
41
res/doc
Normal file
41
res/doc
Normal file
@@ -0,0 +1,41 @@
|
||||
# nickel-menu-inject (libnmi.so)
|
||||
#
|
||||
# This tool injects menu items into Nickel.
|
||||
#
|
||||
# It should work on firmware 4.6+, but it has only been tested on 4.20.14622. It
|
||||
# is perfectly safe to try out on any newer firmware version, as it has a lot of
|
||||
# error checking, and a failsafe mechanism which automatically uninstalls it as
|
||||
# a last resort.
|
||||
#
|
||||
# Place your configuration files in this folder (/mnt/onboard/.kobo/.adds/nmi).
|
||||
# They can be named anything, and should consist of multiple lines either
|
||||
# starting with # for a comment, or in the one of the following formats:
|
||||
#
|
||||
# menu_item:<location>:<label>:<subsystem>:<arg>
|
||||
# Adds a menu item (spaces around fields are ignored).
|
||||
#
|
||||
# <location> the menu to add the item to, one of:
|
||||
# main - the menu in the top-left corner of the home screen
|
||||
# reader - the overflow menu in the reader
|
||||
# <label> the label to show on the menu item (must not contain :)
|
||||
# <subsystem> the type of action to run, one of:
|
||||
# dbg_syslog - writes a message to syslog (for testing)
|
||||
# dbg_error - always returns an error (for testing)
|
||||
# kfmon - triggers a kfmon action (coming soon)
|
||||
# nickel_setting - toggles a boolean setting (coming soon)
|
||||
# <arg> the argument passed to the subsystem:
|
||||
# dbg_syslog - the text to write
|
||||
# dbg_error - the error message
|
||||
# kfmon - TODO
|
||||
# nickel_setting - one of:
|
||||
# invert - toggles FeatureSettings.InvertScreen (all versions)
|
||||
# screenshots - toggles FeatureSettings.Screenshots (all versions)
|
||||
#
|
||||
# uninstall
|
||||
# Makes nickel-menu-inject uninstall itself.
|
||||
#
|
||||
# For example, you might have a configuration file in KOBOeReader/.adds/nmi/mystuff like:
|
||||
#
|
||||
# menu_item :main :Show an error :dbg_error :This is an error message!
|
||||
# menu_item :reader :Invert Screen :nickel_setting :invert
|
||||
#
|
||||
42
src/config.c
Normal file
42
src/config.c
Normal file
@@ -0,0 +1,42 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "util.h"
|
||||
#include "subsys_c.h"
|
||||
#include "subsys_cc.h"
|
||||
|
||||
// TODO: actually parse it
|
||||
|
||||
nmi_menu_entry_t *nmi_config_parse(size_t *n, char **err_out) {
|
||||
#define NMI_ERR_RET NULL
|
||||
NMI_ASSERT(n, "required argument is null");
|
||||
nmi_menu_entry_t *me = calloc(4, sizeof(nmi_menu_entry_t));
|
||||
|
||||
me[0].loc = NMI_MENU_LOCATION_MAIN_MENU;
|
||||
me[0].lbl = strdup("Test Log (main)");
|
||||
me[0].arg = strdup("It worked!");
|
||||
me[0].execute = nmi_subsys_dbgsyslog;
|
||||
|
||||
me[1].loc = NMI_MENU_LOCATION_READER_MENU;
|
||||
me[1].lbl = strdup("Test Log (reader)");
|
||||
me[1].arg = strdup("It worked!");
|
||||
me[1].execute = nmi_subsys_dbgsyslog;
|
||||
|
||||
me[2].loc = NMI_MENU_LOCATION_MAIN_MENU;
|
||||
me[2].lbl = strdup("Test Error (main)");
|
||||
me[2].arg = strdup("It's a fake error!");
|
||||
me[2].execute = nmi_subsys_dbgerror;
|
||||
|
||||
me[3].loc = NMI_MENU_LOCATION_READER_MENU;
|
||||
me[3].lbl = strdup("Test Error (reader)");
|
||||
me[3].arg = strdup("It's a fake error!");
|
||||
me[3].execute = nmi_subsys_dbgerror;
|
||||
|
||||
*n = 4;
|
||||
NMI_RETURN_OK(me);
|
||||
#undef NMI_ERR_RET
|
||||
}
|
||||
|
||||
15
src/config.h
Normal file
15
src/config.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#ifndef NMI_CONFIG_H
|
||||
#define NMI_CONFIG_H
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "menu.h"
|
||||
|
||||
// TODO
|
||||
nmi_menu_entry_t *nmi_config_parse(size_t *n, char **err_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
107
src/dlhook.c
Normal file
107
src/dlhook.c
Normal file
@@ -0,0 +1,107 @@
|
||||
#define _GNU_SOURCE // dladdr, dladdr1, dlinfo, Dl_info
|
||||
#include <dlfcn.h>
|
||||
#include <elf.h>
|
||||
#include <link.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include "dlhook.h"
|
||||
#include "util.h"
|
||||
|
||||
#ifndef ELFW
|
||||
#define ELFW(type) _ElfW(ELF, __ELF_NATIVE_CLASS, type)
|
||||
#endif
|
||||
|
||||
#if __x86_64__
|
||||
#define R_JUMP_SLOT R_X86_64_JUMP_SLOT
|
||||
#else
|
||||
#if __arm__
|
||||
#define R_JUMP_SLOT R_ARM_JUMP_SLOT
|
||||
#else
|
||||
#error Please define the architecture-specific R_*_JUMP_SLOT type.
|
||||
#endif
|
||||
#endif
|
||||
|
||||
void *nmi_dlhook(void *handle, const char *symname, void *target, char **err_out) {
|
||||
#define NMI_ERR_RET NULL
|
||||
|
||||
NMI_ASSERT(handle && symname && target, "BUG: required arguments are null");
|
||||
|
||||
// the link_map conveniently gives use the base address without /proc/maps, and it gives us a pointer to dyn
|
||||
struct link_map *lm;
|
||||
NMI_ASSERT(!dlinfo(handle, RTLD_DI_LINKMAP, &lm), "could not get link_map for lib");
|
||||
NMI_LOG("lib %s is mapped at %lx", lm->l_name, lm->l_addr);
|
||||
|
||||
// stuff extracted from DT_DYNAMIC
|
||||
struct {
|
||||
bool plt_is_rela;
|
||||
union {
|
||||
ElfW(Addr) _;
|
||||
ElfW(Rel) *rel;
|
||||
ElfW(Rela) *rela;
|
||||
} plt;
|
||||
ElfW(Xword) plt_sz;
|
||||
ElfW(Xword) plt_ent_sz;
|
||||
ElfW(Sym) *sym;
|
||||
const char *str;
|
||||
} dyn = {0};
|
||||
|
||||
// parse DT_DYNAMIC
|
||||
for (size_t i = 0; lm->l_ld[i].d_tag != DT_NULL; i++) {
|
||||
switch (lm->l_ld[i].d_tag) {
|
||||
case DT_PLTREL: dyn.plt_is_rela = lm->l_ld[i].d_un.d_val == DT_RELA; break; // .rel.plt - is Rel or Rela
|
||||
case DT_JMPREL: dyn.plt._ = lm->l_ld[i].d_un.d_ptr; break; // .rel.plt - offset from base
|
||||
case DT_PLTRELSZ: dyn.plt_sz = lm->l_ld[i].d_un.d_val; break; // .rel.plt - size in bytes
|
||||
case DT_RELENT: dyn.plt_ent_sz = lm->l_ld[i].d_un.d_val; break; // .rel.plt - entry size if Rel
|
||||
case DT_RELAENT: dyn.plt_ent_sz = lm->l_ld[i].d_un.d_val; break; // .rel.plt - entry size if Rela
|
||||
case DT_SYMTAB: dyn.sym = (ElfW(Sym)*)(lm->l_ld[i].d_un.d_val); break; // .dynsym - offset
|
||||
case DT_STRTAB: dyn.str = (const char*)(lm->l_ld[i].d_un.d_val); break; // .dynstr - offset
|
||||
}
|
||||
}
|
||||
NMI_LOG("DT_DYNAMIC: plt_is_rela=%d plt=%p plt_sz=%lu plt_ent_sz=%lu sym=%p str=%p", dyn.plt_is_rela, (void*)(dyn.plt)._, dyn.plt_sz, dyn.plt_ent_sz, dyn.sym, dyn.str);
|
||||
|
||||
ElfW(Xword) plt_ent_n = dyn.plt_sz / dyn.plt_ent_sz;
|
||||
NMI_ASSERT(dyn.plt_sz%dyn.plt_ent_sz == 0, ".rel.plt length is not a multiple of plt_ent_sz");
|
||||
NMI_ASSERT((dyn.plt_is_rela ? sizeof(*dyn.plt.rela) : sizeof(*dyn.plt.rel)) == dyn.plt_ent_sz, "size mismatch (%lu != %lu)", dyn.plt_is_rela ? sizeof(*dyn.plt.rela) : sizeof(*dyn.plt.rel), dyn.plt_ent_sz);
|
||||
|
||||
// parse the dynamic symbol table, resolve symbols to relocations, then GOT entries
|
||||
for (size_t i = 0; i < plt_ent_n; i++) {
|
||||
// note: Rela is a superset of Rel
|
||||
ElfW(Rel) *rel = dyn.plt_is_rela
|
||||
? (ElfW(Rel)*)(&dyn.plt.rela[i])
|
||||
: &dyn.plt.rel[i];
|
||||
NMI_ASSERT(ELFW(R_TYPE)(rel->r_info) == R_JUMP_SLOT, "not a jump slot relocation (R_TYPE=%lu)", ELFW(R_TYPE)(rel->r_info));
|
||||
|
||||
ElfW(Sym) *sym = &dyn.sym[ELFW(R_SYM)(rel->r_info)];
|
||||
NMI_ASSERT(ELFW(ST_TYPE)(sym->st_info) != STT_GNU_IFUNC, "STT_GNU_IFUNC not implemented");
|
||||
NMI_ASSERT(ELFW(ST_TYPE)(sym->st_info) == STT_FUNC, "not a function symbol (ST_TYPE=%d)", ELFW(ST_TYPE)(sym->st_info));
|
||||
NMI_ASSERT(ELFW(ST_BIND)(sym->st_info) == STB_GLOBAL, "not a globally bound symbol (ST_BIND=%d)", ELFW(ST_BIND)(sym->st_info));
|
||||
|
||||
const char *str = &dyn.str[sym->st_name];
|
||||
if (strcmp(str, symname))
|
||||
continue;
|
||||
|
||||
void **gotoff = (void**)(lm->l_addr + rel->r_offset);
|
||||
NMI_LOG("found symbol %s (gotoff=%p [mapped=%p])", str, (void*)(rel->r_offset), gotoff);
|
||||
|
||||
// remove memory protection (to bypass RELRO if it is enabled)
|
||||
// note: this doesn't seem to be used on the Kobo, but we might as well stay on the safe side (plus, I test this on my local machine too)
|
||||
// note: the only way to read the current memory protection is to parse /proc/maps, but there's no harm in unprotecting it again if it's not protected
|
||||
// note: we won't put it back afterwards, as if full RELRO (i.e. RTLD_NOW) wasn't enabled, it would cause segfaults when resolving symbols later on
|
||||
long pagesize = sysconf(_SC_PAGESIZE);
|
||||
NMI_ASSERT(pagesize != -1, "could not get memory page size");
|
||||
void *gotpage = (void*)((size_t)(gotoff) & ~(pagesize-1));
|
||||
NMI_ASSERT(!mprotect(gotpage, pagesize, PROT_READ|PROT_WRITE), "could not set memory protection of page %p containing %p to PROT_READ|PROT_WRITE", gotpage, gotoff);
|
||||
|
||||
// replace the target offset
|
||||
void *orig = *gotoff;
|
||||
*gotoff = target;
|
||||
NMI_RETURN_OK(orig);
|
||||
}
|
||||
|
||||
NMI_RETURN_ERR("could not find symbol");
|
||||
#undef NMI_err_ret
|
||||
}
|
||||
18
src/dlhook.h
Normal file
18
src/dlhook.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef NMI_DLHOOK_H
|
||||
#define NMI_DLHOOK_H
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// nmi_dlhook takes a lib handle from dlopen and redirects the specified symbol
|
||||
// to another, returning a pointer to the original one. Only calls from within
|
||||
// that library itself are affected (because it replaces that library's GOT). If
|
||||
// an error occurs, NULL is returned and if err is a valid pointer, it is set to
|
||||
// a malloc'd string describing it. This function requires glibc and Linux. It
|
||||
// should work on any architecture, and it should be resilient to most errors.
|
||||
void *nmi_dlhook(void *handle, const char *symname, void *target, char **err_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
66
src/failsafe.c
Normal file
66
src/failsafe.c
Normal file
@@ -0,0 +1,66 @@
|
||||
#define _GNU_SOURCE // asprintf, Dl_info, dladdr
|
||||
#include <link.h>
|
||||
#include <dlfcn.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "failsafe.h"
|
||||
#include "util.h"
|
||||
|
||||
struct nmi_failsafe_t {
|
||||
char *orig;
|
||||
char *tmp;
|
||||
int delay;
|
||||
};
|
||||
|
||||
nmi_failsafe_t *nmi_failsafe_create(char **err_out) {
|
||||
#define NMI_ERR_RET NULL
|
||||
|
||||
NMI_LOG("failsafe: allocating memory");
|
||||
nmi_failsafe_t *fs;
|
||||
NMI_ASSERT((fs = calloc(1, sizeof(nmi_failsafe_t))), "could not allocate memory");
|
||||
|
||||
NMI_LOG("failsafe: finding filenames");
|
||||
Dl_info info;
|
||||
NMI_ASSERT(dladdr(nmi_failsafe_create, &info), "could not find own path");
|
||||
NMI_ASSERT(info.dli_fname, "dladdr did not return a filename");
|
||||
NMI_ASSERT((fs->orig = realpath(info.dli_fname, NULL)), "could not resolve %s", info.dli_fname);
|
||||
NMI_ASSERT(asprintf(&fs->tmp, "%s.failsafe", fs->orig) != -1, "could not generate temp filename");
|
||||
|
||||
NMI_LOG("failsafe: renaming %s to %s", fs->orig, fs->tmp);
|
||||
NMI_ASSERT(!rename(fs->orig, fs->tmp), "could not rename lib");
|
||||
|
||||
NMI_LOG("failsafe: ensuring own lib remains in memory even if it is dlclosed after being loaded with a dlopen");
|
||||
NMI_ASSERT(!dlopen(fs->orig, RTLD_LAZY|RTLD_NODELETE), "could not dlopen self");
|
||||
|
||||
NMI_RETURN_OK(fs);
|
||||
#undef NMI_ERR_RET
|
||||
}
|
||||
|
||||
static void *_nmi_failsafe_destroy(void* _fs) {
|
||||
nmi_failsafe_t *fs = (nmi_failsafe_t*)(_fs);
|
||||
|
||||
NMI_LOG("failsafe: restoring after %d seconds", fs->delay);
|
||||
sleep(fs->delay);
|
||||
|
||||
NMI_LOG("failsafe: renaming %s to %s", fs->tmp, fs->orig);
|
||||
if (!rename(fs->tmp, fs->orig))
|
||||
NMI_LOG("error: could not rename lib");
|
||||
|
||||
NMI_LOG("failsafe: freeing memory");
|
||||
free(fs->orig);
|
||||
free(fs->tmp);
|
||||
free(fs);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void nmi_failsafe_destroy(nmi_failsafe_t *fs, int delay) {
|
||||
fs->delay = delay;
|
||||
|
||||
NMI_LOG("failsafe: starting restore thread");
|
||||
pthread_t t;
|
||||
pthread_create(&t, NULL, _nmi_failsafe_destroy, fs);
|
||||
}
|
||||
25
src/failsafe.h
Normal file
25
src/failsafe.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#ifndef NMI_FAILSAFE_H
|
||||
#define NMI_FAILSAFE_H
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// nmi_failsafe_t is a failsafe mechanism for injected shared libraries. It
|
||||
// works by moving it to a temporary file (so it won't get loaded the next time)
|
||||
// and dlopening itself (to prevent it from being unloaded if it is dlclose'd by
|
||||
// whatever dlopen'd it). When it is disarmed, the library is moved back to its
|
||||
// original location.
|
||||
typedef struct nmi_failsafe_t nmi_failsafe_t;
|
||||
|
||||
// nmi_failsafe_create allocates and arms a failsafe mechanism for the currently
|
||||
// dlopen'd or LD_PRELOAD'd library.
|
||||
nmi_failsafe_t *nmi_failsafe_create(char **err_out);
|
||||
|
||||
// nmi_failsafe_destroy starts a pthread which disarms and frees the failsafe
|
||||
// after a delay. The nmi_failsafe_t must not be used afterwards.
|
||||
void nmi_failsafe_destroy(nmi_failsafe_t *fs, int delay);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
55
src/init.c
Normal file
55
src/init.c
Normal file
@@ -0,0 +1,55 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <link.h>
|
||||
#include <dlfcn.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "failsafe.h"
|
||||
#include "menu.h"
|
||||
#include "util.h"
|
||||
|
||||
__attribute__((constructor)) void nmi_init() {
|
||||
char *err;
|
||||
|
||||
NMI_LOG("init: creating failsafe");
|
||||
nmi_failsafe_t *fs;
|
||||
if (!(fs = nmi_failsafe_create(&err)) && err) {
|
||||
NMI_LOG("error: could not create failsafe: %s, stopping", err);
|
||||
free(err);
|
||||
goto stop;
|
||||
}
|
||||
|
||||
// TODO: handle uninstall
|
||||
|
||||
NMI_LOG("init: parsing config");
|
||||
size_t entries_n;
|
||||
nmi_menu_entry_t *entries;
|
||||
if (!(entries = nmi_config_parse(&entries_n, &err)) && err) {
|
||||
NMI_LOG("error: could not parse config: %s, stopping", err);
|
||||
free(err);
|
||||
goto stop_fs;
|
||||
}
|
||||
|
||||
NMI_LOG("init: opening libnickel");
|
||||
void *libnickel = dlopen("libnickel.so.1.0.0", RTLD_LAZY|RTLD_NODELETE);
|
||||
if (!libnickel) {
|
||||
NMI_LOG("error: could not dlopen libnickel, stopping");
|
||||
goto stop_fs;
|
||||
}
|
||||
|
||||
NMI_LOG("init: hooking libnickel");
|
||||
if (nmi_menu_hook(libnickel, entries, entries_n, &err) && err) {
|
||||
NMI_LOG("error: could not hook libnickel: %s, stopping", err);
|
||||
free(err);
|
||||
goto stop_fs;
|
||||
}
|
||||
|
||||
stop_fs:
|
||||
NMI_LOG("init: destroying failsafe");
|
||||
nmi_failsafe_destroy(fs, 20);
|
||||
|
||||
stop:
|
||||
NMI_LOG("init: done");
|
||||
return;
|
||||
}
|
||||
58
src/menu.cc
Normal file
58
src/menu.cc
Normal file
@@ -0,0 +1,58 @@
|
||||
#include <QAction>
|
||||
#include <QMenu>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "dlhook.h"
|
||||
#include "menu.h"
|
||||
#include "util.h"
|
||||
|
||||
typedef QWidget MenuTextItem; // it's actually a subclass, but we don't need it's functionality directly, so we'll stay on the safe side
|
||||
|
||||
// AbstractNickelMenuController::createMenuTextItem creates a menu item in the
|
||||
// given QMenu with the specified label. The first bool parameter adds space to
|
||||
// the left of the item for a checkbox, and the second bool checks it (see the
|
||||
// search menu for an example of this being used). IDK what the last parameter
|
||||
// is for, but I suspect it might be passed to QObject::setObjectName (I haven't
|
||||
// tested it directly, and it's been empty in the menus I've tried so far).
|
||||
static MenuTextItem* (*AbstractNickelMenuController_createMenuTextItem_orig)(void*, QMenu*, QString const&, bool, bool, QString const&) = NULL;
|
||||
static MenuTextItem* (*AbstractNickelMenuController_createMenuTextItem)(void*, QMenu*, QString const&, bool, bool, QString const&);
|
||||
|
||||
// AbstractNickelMenuController::createAction finishes adding the action to the
|
||||
// menu. IDK what the bool params are for, but thing1 and thing2 always seem to
|
||||
// be true (I know one or the other is for if it is greyed out).
|
||||
static QAction* (*AbstractNickelMenuController_createAction)(void*, QMenu*, QWidget*, bool, bool, bool);
|
||||
|
||||
// ConfirmationDialogFactory::showOKDialog does what it says, with the provided
|
||||
// title and body text. Note that it should be called from the GUI thread (or
|
||||
// a signal handler).
|
||||
static void (*ConfirmationDialogFactory_showOKDialog)(QString const&, QString const&);
|
||||
|
||||
extern "C" MenuTextItem* AbstractNickelMenuController_createMenuTextItem_hook(void* _this, QMenu* menu, QString const& label, bool checkable, bool checked, QString const& thingy) {
|
||||
NMI_LOG("AbstractNickelMenuController::createMenuTextItem(%p, `%s`, %d, %d, `%s`)", menu, qPrintable(label), checkable, checked, qPrintable(thingy));
|
||||
|
||||
// TODO: add items
|
||||
|
||||
return AbstractNickelMenuController_createMenuTextItem_orig(_this, menu, label, checkable, checked, thingy);
|
||||
}
|
||||
|
||||
extern "C" int nmi_menu_hook(void *libnickel, nmi_menu_entry_t *entries, size_t entries_n, char **err_out) {
|
||||
#define NMI_ERR_RET 1
|
||||
NMI_SYM(AbstractNickelMenuController_createMenuTextItem, "_ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_");
|
||||
NMI_SYM(AbstractNickelMenuController_createAction, "_ZN22AbstractMenuController12createActionEP5QMenuP7QWidgetbbb");
|
||||
NMI_SYM(ConfirmationDialogFactory_showOKDialog, "_ZN25ConfirmationDialogFactory12showOKDialogERK7QStringS2_");
|
||||
|
||||
NMI_ASSERT(AbstractNickelMenuController_createMenuTextItem, "unsupported firmware: could not find AbstractNickelMenuController::createMenuTextItem(void* _this, QMenu*, QString, bool, bool, QString const&)");
|
||||
NMI_ASSERT(AbstractNickelMenuController_createAction, "unsupported firmware: could not find AbstractNickelMenuController::createAction(void* _this, QMenu*, QWidget*, bool, bool, bool)");
|
||||
NMI_ASSERT(AbstractNickelMenuController_createAction, "unsupported firmware: could not find ConfirmationDialogFactory::showOKDialog(QString const&, QString const&)");
|
||||
|
||||
char *err;
|
||||
reinterpret_cast<void*&>(AbstractNickelMenuController_createMenuTextItem_orig) = nmi_dlhook(libnickel, "_ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_", reinterpret_cast<void*&>(AbstractNickelMenuController_createMenuTextItem_hook), &err);
|
||||
NMI_ASSERT(AbstractNickelMenuController_createMenuTextItem_orig, "failed to hook _ZN28AbstractNickelMenuController18createMenuTextItemEP5QMenuRK7QStringbbS4_: %s", err);
|
||||
|
||||
#undef NMI_ERR_RET
|
||||
}
|
||||
|
||||
// TODO
|
||||
31
src/menu.h
Normal file
31
src/menu.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#ifndef NMI_MENU_H
|
||||
#define NMI_MENU_H
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
typedef enum {
|
||||
NMI_MENU_LOCATION_MAIN_MENU = 1,
|
||||
NMI_MENU_LOCATION_READER_MENU = 2,
|
||||
} nmi_menu_location_t;
|
||||
|
||||
typedef struct {
|
||||
nmi_menu_location_t loc;
|
||||
const char *lbl;
|
||||
const char *arg;
|
||||
int (*execute)(const char *arg, char **out_err); // can block, must return 0 on success, nonzero with out_err set to the malloc'd error message on error
|
||||
} nmi_menu_entry_t;
|
||||
|
||||
// nmi_menu_hook hooks a dlopen'd libnickel handle to add the specified menus,
|
||||
// and returns 0 on success, or 1 with err_out (if provided) set to the malloc'd
|
||||
// error message on error. The provided configuration and all pointers it
|
||||
// references must remain valid for the lifetime of the program (i.e. not stack
|
||||
// allocated).
|
||||
int nmi_menu_hook(void *libnickel, nmi_menu_entry_t *entries, size_t entries_n, char **err_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
9
src/qtplugin.h
Normal file
9
src/qtplugin.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#ifdef __cplusplus
|
||||
#include <QObject>
|
||||
#include <QtPlugin>
|
||||
|
||||
class NMIPlugin : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "net.pgaskin.nmi");
|
||||
};
|
||||
#endif
|
||||
23
src/subsys_c.c
Normal file
23
src/subsys_c.c
Normal file
@@ -0,0 +1,23 @@
|
||||
#define _GNU_SOURCE // asprintf
|
||||
|
||||
#include "subsys_c.h"
|
||||
#include "util.h"
|
||||
|
||||
int nmi_subsys_dbgsyslog(const char *arg, char **err_out) {
|
||||
#define NMI_ERR_RET 1
|
||||
NMI_LOG("dbgsyslog: %s", arg);
|
||||
NMI_RETURN_OK(0);
|
||||
#undef NMI_ERR_RET
|
||||
}
|
||||
|
||||
int nmi_subsys_dbgerror(const char *arg, char **err_out) {
|
||||
#define NMI_ERR_RET 1
|
||||
NMI_RETURN_ERR("%s", arg);
|
||||
#undef NMI_ERR_RET
|
||||
}
|
||||
|
||||
int nmi_subsys_kfmon(const char *arg, char **err_out) {
|
||||
#define NMI_ERR_RET 1
|
||||
NMI_RETURN_ERR("not implemented yet"); // TODO
|
||||
#undef NMI_ERR_RET
|
||||
}
|
||||
14
src/subsys_c.h
Normal file
14
src/subsys_c.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef NMI_SUBSYS_C_H
|
||||
#define NMI_SUBSYS_C_H
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int nmi_subsys_dbgsyslog(const char *arg, char **err_out);
|
||||
int nmi_subsys_dbgerror(const char *arg, char **err_out);
|
||||
int nmi_subsys_kfmon(const char *arg, char **err_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
10
src/subsys_cc.cc
Normal file
10
src/subsys_cc.cc
Normal file
@@ -0,0 +1,10 @@
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include "subsys_cc.h"
|
||||
#include "util.h"
|
||||
|
||||
extern "C" int nmi_subsys_nickelsetting(const char *arg, char **err_out) {
|
||||
#define NMI_ERR_RET 1
|
||||
NMI_RETURN_ERR("not implemented yet"); // TODO
|
||||
#undef NMI_ERR_RET
|
||||
}
|
||||
13
src/subsys_cc.h
Normal file
13
src/subsys_cc.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef NMI_SUBSYS_CC_H
|
||||
#define NMI_SUBSYS_CC_H
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int nmi_subsys_dbgsyslog(const char *arg, char **err_out);
|
||||
int nmi_subsys_dbgerror(const char *arg, char **err_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
56
src/util.h
Normal file
56
src/util.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#ifndef NMI_UTIL_H
|
||||
#define NMI_UTIL_H
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE // asprintf
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <syslog.h>
|
||||
|
||||
// A bunch of useful macros to simplify error handling and logging.
|
||||
|
||||
// NMI_LOG writes a log message.
|
||||
#define NMI_LOG(fmt, ...) syslog(LOG_DEBUG, "(nickel-menu-inject) " fmt " (%s:%d)", ##__VA_ARGS__, __FILE__, __LINE__)
|
||||
|
||||
// NMI_RETURN returns ret, and if ret is NMI_ERR_RET and err_out is not NULL, it
|
||||
// writes the formatted error message to *err_out as a malloc'd string. The
|
||||
// arguments may or may not be evaluated more than once.
|
||||
#define NMI_RETURN(ret, fmt, ...) do { \
|
||||
typeof(ret) _ret = (ret); \
|
||||
if (err_out) { \
|
||||
if (_ret == NMI_ERR_RET) asprintf(err_out, fmt " (%s:%d)", ##__VA_ARGS__, __FILE__, __LINE__); \
|
||||
else *err_out = NULL; \
|
||||
} \
|
||||
return _ret; \
|
||||
} while (0)
|
||||
|
||||
// NMI_ASSERT is like assert, but it writes the formatted error message to
|
||||
// err_out as a malloc'd string, and returns NMI_ERR_RET. Cond will always be
|
||||
// evaluated exactly once. The other arguments may or may not be evaluated one
|
||||
// or more times.
|
||||
#define NMI_ASSERT(cond, fmt, ...) do { \
|
||||
if (!(cond)) NMI_RETURN(NMI_ERR_RET, fmt " (assertion failed: %s)", ##__VA_ARGS__, #cond); \
|
||||
} while (0)
|
||||
|
||||
// NMI_RETURN_ERR is the same as NMI_RETURN(NMI_ERR_RET, fmt, ...).
|
||||
#define NMI_RETURN_ERR(fmt, ...) NMI_RETURN(NMI_ERR_RET, fmt, ##__VA_ARGS__)
|
||||
|
||||
// NMI_RETURN_OK is the same as NMI_RETURN(ret, "").
|
||||
#define NMI_RETURN_OK(ret) NMI_RETURN(ret, "")
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#ifdef __cplusplus
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
// NMI_SYM loads a symbol from the global scope.
|
||||
#define NMI_SYM(var, sym) reinterpret_cast<void*&>(var) = dlsym(RTLD_DEFAULT, sym)
|
||||
|
||||
#endif
|
||||
#endif
|
||||
Reference in New Issue
Block a user