Package: sane-backends
Version: sane-backends
Severity: normal
Tags: patch upstream

Package: src:sane-backends
Version: 1.4.0-1
Severity: normal
Tags: patch upstream fixed-upstream

Dear Maintainer,

sanei/sanei_thread.c enables PTHREAD_CANCEL_ASYNCHRONOUS in local_thread(), so 
a reader thread cancelled through sanei_thread_kill() is unwound at an 
arbitrary instruction rather than at a defined cancellation point.  I 
discovered this problem while examining the xsane source code and attempting to 
fix issues there. This issue has existed since 1.0.13 (2003). 

Upstream fixed this in commit 8acc267d ("sanei_thread.c: Use deferred 
cancellation mode", MR !881), merged 2025-07-14. That landed seven weeks after 
the 1.4.0 tag (2025-05-24) and no release has followed, so every released 
sane-backends still carries it -- including 1.4.0-1 in unstable and 1.3.1-3 in 
trixie. A patch is attached; it is a straight cherry-pick. I would rather 
submit this now and prevent future issues vs having to be reactive towards this 
issue.

Upstream's commit message describes the Linux symptom as a hang, with this 
reproducer:

  while true; do scanimage --verbose --device=test --batch-count=2; done

"The command hangs after several iterations." Upstream cites perl-Image-Sane's 
test suite as the affected consumer.

HONESTY ABOUT REPRODUCTION ON DEBIAN/UBUNTU
I could not reproduce the hang on glibc: 40 iterations of the above on Ubuntu 
24.04 (1.2.1-7build4) completed cleanly. glibc's unwinder appears to tolerate 
asynchronous cancellation well, so on Debian this is a latent defect rather 
than one users are likely hitting today. I am reporting it because the code is 
unambiguously wrong, the fix is upstream and trivial, and the failure is a race 
-- a passing run proves nothing.

WHERE IT IS NOT LATENT
The same code is fatal on Windows, where winpthreads implements asynchronous 
cancellation by force-unwinding the target thread via setjmp/RtlUnwind. Built 
for mingw-w64 (MSYS2 ships sane-backends 1.4.0), a scan segfaults at end of 
image in roughly nine runs out of ten:

  #6  local_thread (arg=...) at sanei_thread.c:360
  #5  libwinpthread-1.dll
  #3  ucrtbase!.intrinsic_setjmpex
  #2  ntdll!RtlUnwind                       -> SIGSEGV

With 8acc267d applied, a test-backend scan driven directly through libsane went 
from about 1 run in 10 succeeding to 20 out of 20 clean, reading the full image 
every time (48 reads, 278480 bytes at 590x472 grey/150dpi).

This is mentioned only as evidence that the defect is real and that the fix 
works; I am not asking Debian to care about Windows. It does mean sanei_thread 
is shared machinery used by real hardware backends, not just the test backend, 
so the exposure is not limited to test setups.

RELATED
Upstream issue #816 ("Should APPLE/MACH be using SIGUSR2 to kill a thread?") 
touches the same function and is still open: 
https://gitlab.com/sane-project/backends/-/work_items/816

Debian #869318 ("libimage-sane-perl: FTBFS randomly: build hangs during tests", 
serious, 2017) is, I believe, the same defect seen from the other end. It is 
exactly the reproducer upstream describes -- a perl-Image-Sane test suite 
hanging intermittently -- but it was filed against the consumer, never 
reassigned here, and closed by working around it in libimage-sane-perl/5-3. Its 
log mentions neither pthread nor cancellation. It is not a duplicate of this 
report; it is evidence that the race has been costing people time for years 
while presenting as a flaky test.

I searched the BTS (src:sane-backends, 13 open bugs), Ubuntu, and upstream 
before filing; this defect does not appear to have been reported anywhere.

A 1.4.1 point release carrying 8acc267d would fix this for every distribution 
at once, which may be worth raising with upstream. In my opinion, though, If 
debian just adds this to the patch list, this will prevent future issues.

-- System Information (fill in with reportbug, or edit by hand)


-- System Information:
Debian Release: trixie/sid
  APT prefers noble-updates
  APT policy: (500, 'noble-updates'), (500, 'noble-security'), (500, 'noble'), 
(100, 'noble-proposed'), (100, 'noble-backports')
Architecture: amd64 (x86_64)
Foreign Architectures: i386

Kernel: Linux 6.17.0-1031-nvidia (SMP w/16 CPU threads; PREEMPT)
Kernel taint flags: TAINT_PROPRIETARY_MODULE, TAINT_OOT_MODULE, 
TAINT_UNSIGNED_MODULE
Locale: LANG=en_US.UTF-8, LC_CTYPE=en_US.UTF-8 (charmap=UTF-8), LANGUAGE not set
Shell: /bin/sh linked to /usr/bin/dash
Init: systemd (via /run/systemd/system)
LSM: AppArmor: enabled
From: Aaron Yourk <[email protected]>
Date: Mon, 24 Aug 2026 00:00:00 +0000
Subject: [PATCH] sanei_thread: use deferred cancellation mode

Cherry-pick of upstream commit 8acc267d (MR !881, merged 2025-07-14),
"sanei_thread.c: Use deferred cancellation mode".

local_thread() enabled PTHREAD_CANCEL_ASYNCHRONOUS, so a thread cancelled
via sanei_thread_kill() is unwound at an arbitrary instruction rather than
at a defined cancellation point. Upstream observed this as a hang under
rapid scanimage loops; on Windows/winpthreads the same code segfaults.

Deferred cancellation -- the glibc default -- still cancels, just safely.

Upstream: https://gitlab.com/sane-project/backends/-/commit/8acc267d
---
 sanei/sanei_thread.c | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

--- a/sanei/sanei_thread.c
+++ b/sanei/sanei_thread.c
@@ -333,6 +333,10 @@ local_thread( void *arg )
        static int     status;
        pThreadDataDef ltd = (pThreadDataDef)arg;
 
+       /* Set signal handlers for Mac and Mach. On other systems,
+        * thread cancellation is enabled in deferred mode,
+        * which is glibc default.
+        */
 #if defined (__APPLE__) && defined (__MACH__)
        struct sigaction act;
 
@@ -340,11 +344,6 @@ local_thread( void *arg )
        act.sa_flags   = 0;
        act.sa_handler = thread_exit_handler;
        sigaction( SIGUSR2, &act, 0 );
-#else
-       int old;
-
-       pthread_setcancelstate( PTHREAD_CANCEL_ENABLE, &old );
-       pthread_setcanceltype ( PTHREAD_CANCEL_ASYNCHRONOUS, &old );
 #endif
 
        DBG( 2, "thread started, calling func() now...\n" );

Reply via email to