fix(koplugin): harden cover-download subprocess against Adreno exit crash (#4169)

Browsing a folder in the Readest Library spawns a forked child per
cloud cover via syncbooks.downloadCover. On Boox / Adreno devices the
child crashed with a SIGSEGV (issue #4165): it terminated through the
libc exit() path, and __cxa_finalize ran the destructor of the GL
driver inherited from the parent, which segfaults on Adreno.

Terminate the child with ffi.C._exit(0) instead. _exit() skips libc
atexit handlers, so __cxa_finalize — and the Adreno destructor it runs
— never execute. The body is also wrapped in pcall so a network error
in http.request cannot unwind past that _exit call.

This eliminates the child-side crash in the reported tombstone. The
parent KOReader exiting is most likely a knock-on effect of the child
tearing down GPU state shared across the fork, but that link is not
provable from the log alone — so this intentionally does not
auto-close #4165 until confirmed on an affected device.

No unit test: the fork + network path isn't reproducible in the busted
harness, consistent with the other network methods in this file.
This commit is contained in:
Huang Xin
2026-05-15 16:52:09 +08:00
committed by GitHub
parent 7716f189c3
commit e0cb433550
+34 -10
View File
@@ -592,16 +592,32 @@ function M.downloadCover(book, opts, cb)
local pid, parent_read_fd = FFIUtil.runInSubProcess(
function(_child_pid, child_write_fd)
local socket = require("socket")
local http = require("socket.http")
local socketutil = require("socketutil")
local ltn12 = require("ltn12")
-- Runs in a forked child. Two hard rules, both to keep
-- KOReader alive on Boox / Adreno devices (issue #4165):
--
-- 1. No Lua error may escape this function. An uncaught
-- error unwinds back to KOReader's android_main,
-- which terminates the child through the libc exit()
-- path — running __cxa_finalize.
-- 2. Terminate via _exit(), never exit(): __cxa_finalize
-- runs the destructor of the GL driver inherited from
-- the parent, which segfaults on Adreno and takes the
-- whole app down with it.
--
-- A network failure in http.request is exactly the kind
-- of error rule 1 guards against, so wrap the body.
local result
local f, ferr = io.open(dst, "wb")
if not f then
result = "error:open:" .. tostring(ferr)
else
local ok, err = pcall(function()
local socket = require("socket")
local http = require("socket.http")
local socketutil = require("socketutil")
local ltn12 = require("ltn12")
local f, ferr = io.open(dst, "wb")
if not f then
result = "error:open:" .. tostring(ferr)
return
end
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
local code = socket.skip(1, http.request{
url = url,
@@ -616,8 +632,16 @@ function M.downloadCover(book, opts, cb)
else
result = "error:http:" .. tostring(code)
end
end)
if not ok then
result = "error:exception:" .. tostring(err)
end
FFIUtil.writeToFD(child_write_fd, result, true)
pcall(FFIUtil.writeToFD, child_write_fd, result or "error:unknown", true)
-- Hard exit, bypassing libc atexit handlers (rule 2).
local ffi = require("ffi")
pcall(ffi.cdef, "void _exit(int status);")
ffi.C._exit(0)
end,
true) -- with_pipe = true