User-mode emulation for PocketBook 6.1-6.11 firmware on Linux desktop (podman + Wayland)
README.md

shim — ARM LD_PRELOAD compatibility layer #

libshim.so is injected via LD_PRELOAD into every guest ARM process running under qemu-arm inside the Podman container. It makes PocketBook firmware behave as if running on real hardware by intercepting system calls and firmware-specific symbols.

Build #

# From repo root (builds inside container):
./pbemu build [firmware]

Output: src/shim/build-arm/libshim.so

The shim links against libc.so from the staged firmware's own rootfs/lib/ (not the host libc), so ./pbemu install <firmware.zip> must be run first.

Source map #

src/shim.c — hub / entry point #

The top-level LD_PRELOAD interceptor. Wraps open(), open64(), __open_2(), __open64_2(), openat(), openat64(), __openat_2(), __openat64_2(), fopen(), fopen64(), close(), access(), faccessat(), stat(), lstat(), __xstat(), __lxstat(), __xstat64(), __lxstat64(), dup(), dup2(), dup3(), fcntl() (F_DUPFD), syscall() (SYS_execve/SYS_execveat), execve(), execv(), execvp(), execl(), execlp(), fexecve(), execveat(), system(), chmod(), fchmodat(), msgget(), msgctl(), __msgctl64(), mq_open(), mq_close(), mq_receive(), mq_timedreceive(). Dispatches each call to the appropriate sub-module. Also forces 0644/0755 on /var/run/task paths so host-side tools can inspect task metadata.

src/fake_fb.c — framebuffer emulation #

Intercepts open() on /dev/fb0 and /dev/graphics/fb0, returning anonymous memfd-backed files pre-sized to the display resolution. Handles all standard Linux FB ioctls (FBIOGET_VSCREENINFO, FBIOPUT_VSCREENINFO, FBIOGET_FSCREENINFO, FBIOBLANK) and PocketBook vendor ioctls (waveform, rotation, partial update). Pixel data written by libinkview flows into the System V SHM segment that the host viewer reads.

Key functions:

  • path_is_fake_fb_target() — matches /dev/fb0 and /dev/graphics/fb0
  • create_fake_fb_fd() — allocates the memfd, registers it with fd_tracker
  • fake_fb_handle_ioctl() — dispatcher for all FB ioctls
  • fake_fb_reported_geometry() — handles portrait/landscape rotation and old-ABI compat

src/fake_rtc.c — RTC emulation #

Intercepts open() on /dev/rtc0 and /dev/misc/rtc. RTC ioctls (RTC_RD_TIME, RTC_SET_TIME, RTC_ALM_SET, RTC_AIE_ON/OFF) are synthesised from host wall-clock time via gmtime_r().

Key functions:

  • path_is_fake_rtc_target() — path matcher
  • create_fake_rtc_fd() — creates the anonymous temp fd
  • fake_rtc_handle_ioctl() — fills struct rtc_time from current host time

src/virtual_files.c — sysfs / procfs emulation #

Intercepts open() / fopen() on firmware-specific paths that would normally be served by kernel drivers. Returns pre-filled anonymous temp files.

Emulated paths include:

  • Waveform name (/sys/…/waveform_name → "A2PLUS")
  • Device serial, SD card serial (shared with the libIOC emulation in fake_ioc.c)
  • Backlight brightness sysfs nodes
  • OOM control (/proc/<pid>/oom_adj, /proc/<pid>/oom_score_adj) — per-process writable files

Key functions:

  • path_is_virtual_file_target() — matches all intercepted paths
  • create_virtual_file_fd() — creates pre-filled anonymous fd
  • open_virtual_oom_file() — creates a per-process persistent anonymous fd for OOM adj paths

src/exec_trampoline.c — execve() / system() dispatch #

When firmware calls execve() on an ARM ELF, re-launches it under a fresh qemu-arm instance, forwarding the sysroot (-L), LD_PRELOAD, and Qt/firmware environment variables via -E flags. Host binaries (under /bin, /usr/bin) are executed directly with LD_PRELOAD stripped.

Key functions:

  • shim_execve() — public interceptor, called from shim.c
  • canonical_exec_path() — resolves /proc/self/fd/<n> symlinks
  • path_is_firmware_exec_candidate() — detects ARM ELF paths
  • exec_as_qemu_guest() — builds and execs qemu-arm argv
  • run_system_without_host_ld_preload() — strips LD_PRELOAD for host utilities

src/alsa_mixer_fixups.c — ALSA mixer virtualisation #

Intercepts snd_mixer_open(), snd_mixer_attach(), snd_mixer_selem_register(), snd_mixer_load(), snd_mixer_find_selem(), snd_mixer_selem_has_playback_volume(), snd_mixer_selem_get_playback_volume_range(), snd_mixer_selem_get_playback_volume(), and snd_mixer_selem_set_playback_volume_all(). Routes every mixer-attachment attempt through a virtual mixer that exposes one dummy simple element with a 0–100 volume range. PCM (playback) paths are not affected; only the control/mixer layer is virtualised. Up to 32 virtual mixer handles tracked in a fixed slot array under a mutex.

src/ipc_fixups.c — SysV and POSIX message queue compatibility #

Patches two categories of IPC failure that arise from running as non-root under QEMU:

  • msgget() — adds world-read/write bits when firmware passes no permissions
  • msgctl(IPC_SET) — replaces uid/gid 0 with current process uid/gid
  • mq_open("/hwevent") — enforces mq_maxmsg=10, mq_msgsize=40 regardless of firmware request; tracks open /hwevent descriptors
  • mq_receive() / mq_timedreceive() on /hwevent — serves a legacy 32-byte view to older firmware readers (copies the first 32 bytes of the 40-byte on-wire message)

src/process_identity.c — guest process basename detection #

Reads /proc/self/cmdline via raw syscalls (avoiding re-entrant open()) and reports whether the current process was launched with a given basename as its argv[0] basename. The result is immutable for the lifetime of the process.

Key functions:

  • current_process_has_guest_basename() — returns non-zero if the current process basename matches

src/fake_ioc.c — libIOC (SWX bytecode interpreter) emulation #

Replaces the kernel-side libIOC device interface that does not exist in the emulated environment. The shim installs swx_first() via LD_PRELOAD; for any request code that can be answered synthetically (device serial, SD card serial, waveform count, etc.) the shim writes the response into the caller's state buffer and returns. Unrecognised requests fall through to the real swx_first via RTLD_NEXT. libIOC answers are device-level, so the shim does not gate on process identity.

Key functions:

  • swx_first() — public interceptor (signature fixed by the firmware); calls the internal fake_ioc_emit_response() first, falls through to RTLD_NEXT only when no response was emitted
  • fake_ioc_emit_response() — reads the request code at LIBIOC_OFFSET_REQUEST, switches on it, writes the appropriate response

src/monitor_patches.c — monitor.app-specific patches #

Patches behaviours specific to monitor.app (the PocketBook session manager): the hardware-event callback table and the suspend IPC emitter.

Key functions:

  • patch_monitor_callback_slots() — overwrites the hardware-event function pointer table with no-ops while preserving slot index 1 / offset 0, which is the recurring power-check callback on both tested U743g and U628 firmware; that preserved slot is mapped to monitor_one_callback(), which returns 1. Called from the shim constructor only after a basename check confirms the current process is monitor.app.
  • hw_sleep() — intercepts libinkview's suspend emitter so guest apps cannot send monitor IPC request 0x10a

src/monitor_callback_slots.c — locating monitor.app's callback table #

Finds the hardware-event callback function-pointer block in monitor.app's loaded image without relying on hardcoded addresses. The shim resolves the real monitor.app path from /proc/self/maps, opens that ELF image, and scans each writable PT_LOAD segment for the known trailer signature (two valid image pointers + 0xffffffff/0xfffffffe) that immediately follows the 16-word callback block. Segments are read in 4 KB chunks with a sliding overlap so matches that straddle chunk boundaries are not missed.

Key functions:

  • discover_monitor_callback_slots() — top-level; resolves the real monitor image path, performs the layout scan, then patches found slots
  • find_monitor_callback_block_vaddr_by_layout() — sliding-window scanner over writable PT_LOAD segments
  • monitor_callback_signature_matches() — checks the 4-word trailer signature
  • find_monitor_load_bias() — resolves the load bias using /proc/self/maps entries for the real monitor image

Callback slot word offsets relative to block start: {-1, 0, 1, 2, 8, 12}.

src/display_geometry.c — hwconfig / display geometry resolution #

Reads display width, height, default rotation, and native-orientation hints from libhwconfig.so at shim startup (once, via pthread_once). Falls back to compile-time defaults (FB_WIDTH_DEFAULT, FB_HEIGHT_DEFAULT). These values are used by the fake framebuffer to advertise the correct geometry to guest apps.

Key functions:

  • shim_fb_width() / shim_fb_height() — display dimensions
  • shim_fb_default_rotate() — default FB rotation constant
  • shim_fb_has_native_orientation() — true when device is natively portrait

src/task_launch_override.c — legacy NewTaskEx() fallback #

Detects legacy firmware by the absence of hw_launchapp. On modern firmware, the shim leaves NewTaskEx() alone so libinkview can talk to monitor_launch_service_manager() normally. On legacy firmware (for example U628), the shim keeps the actual target executable as the launched process, but tightens the surrounding plumbing: a host-side double-fork preserves the detached launch shape, and the target process's first intercepted hw_task_register() call writes the task id back to the parent over a pipe. This avoids /var/run/task polling races and zombie launcher processes while still staying within the qemu-safe constraint of “no non-trivial libinkview work after fork, before exec”.

Key functions:

  • firmware_needs_newtaskex_override() — enables the fallback only when hw_launchapp is missing
  • NewTaskEx() — public interceptor; forwards unchanged on modern firmware, lets the real firmware handle the legacy new_task.app detour when needed, and otherwise launches the real target directly with pipe-backed task-id reporting
  • build_launch_env() / launch_registered_task() — direct-launch plumbing that preserves NewTaskEx() metadata, returns the real task id synchronously, and avoids zombie launcher processes via a host-side double-fork
  • hw_task_register() — consumes one-shot SHIM_NEWTASK_* metadata plus a reporting pipe fd, forwards the real registration call unchanged aside from the requested metadata override, then reports the resulting task id back to the original NewTaskEx() caller

src/update_hooks.c — repaint hint publishing #

Wraps every libinkview screen-update function (Repaint, FullUpdate, FullUpdateHQ, SoftUpdate, SoftUpdateHQ, FineUpdate, PartialUpdate, PartialUpdateTextPage, PartialUpdateBlack, PartialUpdateBW, PartialUpdateHQ, PartialUpdateDU4, DynamicUpdate, DynamicUpdateWithoutImprovingLinesColor, DynamicUpdateBW, DynamicUpdateA2, UpdateEx, ScheduleUpdate, iv_update_screen, iv_update_many). After forwarding to the real function via RTLD_NEXT, records a full-repaint or dirty-rectangle hint into the pb_repaint_state SysV SHM segment (PB_REPAINT_SHM_KEY). The host viewer reads this hint to skip redundant blits. Dirty rectangles are union-merged; a full-repaint flag takes priority. The SHM segment is created on first call via pthread_once.

src/firmware_stubs.c — firmware-facing timing, battery, audio, network, and contract stubs #

  • hw_boottime_ms() — returns guest-session-relative monotonic milliseconds with the sign bit forced clear; prevents sign-extension bugs in firmware that stores the value in a 32-bit field
  • device_battery_percent(), GetBatteryPower(), hw_is_battery_full() — always return 100 %/full
  • device_audio(), device_has_audio(), device_hp_detect_cfg(), QueryHeadphone() — expose a fixed speaker-capable profile with no headphones inserted
  • configureAudioOutput*() and hw_ipcrequest(0x300/0x301) — short-circuit monitor-owned audio-route IPC and publish a synthetic speaker route to guest apps
  • hw_netmgr_status(), hw_wifi_status(), hw_get_current_network_state(), hw_querynetwork(), Net*() — report a coherent connected WiFi state
  • isDeviceContractLock() — always returns 0 (unlocked)
  • GetThemeString() — answers "1" for the panel_position key (every other key falls through), matching the real device's resolved theme. The guest then behaves exactly like on hardware: SetPanelType(1) stores fb_y_offset = PanelHeight() in the task SHM header (iv_fbinfo offset 0x8c), the panel painter blits the system bar into the BOTTOM framebuffer rows, and the display side (device EPDC / PC viewer) starts scanout fb_y_offset rows into the buffer with wrap-around — showing the bar at the top and app content below it. Pointer input is delivered in physical screen coordinates; the guest subtracts fb_y_offset itself.

src/fd_tracker.c — fake fd lifetime tracking #

Maintains two fixed slot arrays (g_fake_fb_fds[64], g_fake_rtc_fds[64]) protected by a single mutex. Allows shim.c's close() and ioctl() interceptors to quickly check whether a given fd is fake.

API: fd_tracker_remember_fb/rtc(), fd_tracker_forget_fb/rtc(), fd_tracker_is_fb/rtc(), fd_tracker_forget_any().

Key headers #

  • src/shim_internal.h — private cross-module header; defines SHIM_LOG, FB_PATH_*, RTC_PATH_*, FB_TRACKED_FDS, forward declarations for all cross-TU functions
  • src/fb_defs.h — framebuffer constants (FB_WIDTH_DEFAULT, FB_HEIGHT_DEFAULT, FB_SCRATCH_ROWS, vendor ioctl numbers)
  • src/hw_profile.h — compile-time hardware profile flags; private vendor ioctl numbers for the EPDC framebuffer and libIOC request codes
  • src/monitor_callback_slots.h — MONITOR_CALLBACK_SLOT_COUNT, discover_monitor_callback_slots() declaration
  • src/platform_fixups.h — declares shim_reinstall_sigsegv_handler() for reinserting the custom SIGSEGV handler after PrepareForLoop() overwrites it