Skip to content

Conversation

@fly602
Copy link
Contributor

@fly602 fly602 commented Jan 5, 2026

Previously, list.c, type.c, property.c, button_map.c, and keyboard.c each had their own local mutex, which failed to protect concurrent X11 access across different files. This caused heap corruption when multiple threads simultaneously called XOpenDisplay/XCloseDisplay, resulting in "malloc(): unaligned tcache chunk detected" errors.

Log:

  • Add x11_mutex.h/c with global x11_global_mutex
  • Replace all local mutexes with x11_global_mutex
  • Add _unlocked internal versions of query_device_type and is_property_exist to avoid deadlock when called from within locked contexts
  • Ensure all top-level functions (called from Go) acquire the lock
  • Internal helper functions assume caller holds the lock

Influence: This ensures thread-safe X11 access while avoiding deadlock through proper lock hierarchy.


修复(dxinput): 使用全局互斥锁防止 X11 线程竞争

之前 list.c、type.c、property.c、button_map.c 和 keyboard.c 各自 使用独立的局部互斥锁,无法保护跨文件的并发 X11 访问。这导致多个线程
同时调用 XOpenDisplay/XCloseDisplay 时发生堆损坏,触发 "malloc(): unaligned tcache chunk detected" 错误。

Log:

  • 新增 x11_mutex.h/c,定义全局 x11_global_mutex
  • 将所有局部互斥锁替换为 x11_global_mutex
  • 新增 query_device_type 和 is_property_exist 的 _unlocked 内部版本, 避免在已持锁的上下文中调用时发生死锁
  • 确保所有顶层函数(被 Go 调用)获取锁
  • 内部辅助函数假设调用者已持有锁

Influence: 通过正确的锁层次结构,确保线程安全的 X11 访问,同时避免死锁。

Summary by Sourcery

Introduce a global X11 mutex to serialize all X11 operations across dxinput utilities and prevent cross-file race conditions.

Bug Fixes:

  • Fix potential heap corruption and race conditions caused by concurrent X11 access from multiple threads using separate local mutexes.

Enhancements:

  • Add unlocked internal helpers for device type and property checks and adjust call sites to rely on the global X11 lock where appropriate.
  • Improve Go wrapper memory-safety checks and pointer arithmetic when handling X11 property data.

Previously, list.c, type.c, property.c, button_map.c, and keyboard.c
each had their own local mutex, which failed to protect concurrent X11
access across different files. This caused heap corruption when multiple
threads simultaneously called XOpenDisplay/XCloseDisplay, resulting in
"malloc(): unaligned tcache chunk detected" errors.

Log:
- Add x11_mutex.h/c with global x11_global_mutex
- Replace all local mutexes with x11_global_mutex
- Add _unlocked internal versions of query_device_type and
  is_property_exist to avoid deadlock when called from within
  locked contexts
- Ensure all top-level functions (called from Go) acquire the lock
- Internal helper functions assume caller holds the lock

Influence: This ensures thread-safe X11 access while avoiding deadlock through
proper lock hierarchy.

---

修复(dxinput): 使用全局互斥锁防止 X11 线程竞争

之前 list.c、type.c、property.c、button_map.c 和 keyboard.c 各自
使用独立的局部互斥锁,无法保护跨文件的并发 X11 访问。这导致多个线程
同时调用 XOpenDisplay/XCloseDisplay 时发生堆损坏,触发 "malloc():
unaligned tcache chunk detected" 错误。

Log:
- 新增 x11_mutex.h/c,定义全局 x11_global_mutex
- 将所有局部互斥锁替换为 x11_global_mutex
- 新增 query_device_type 和 is_property_exist 的 _unlocked 内部版本,
  避免在已持锁的上下文中调用时发生死锁
- 确保所有顶层函数(被 Go 调用)获取锁
- 内部辅助函数假设调用者已持有锁

Influence: 通过正确的锁层次结构,确保线程安全的 X11 访问,同时避免死锁。
@sourcery-ai
Copy link

sourcery-ai bot commented Jan 5, 2026

Reviewer's Guide

Introduces a single global X11 mutex shared across all dxinput C modules, replaces local per-file mutexes, and refactors selected APIs into locked/unlocked variants to enforce a consistent lock hierarchy and prevent cross-file X11 race conditions, plus minor safety tweaks in the Go wrapper.

Sequence diagram for Go property query with global X11 mutex

sequenceDiagram
    actor GoGoroutine
    participant GoUtils as Go_utils.wrapper
    participant CUtils as C_dxinput_utils
    participant Mutex as x11_global_mutex
    participant X11 as X11_server

    GoGoroutine->>GoUtils: IsPropertyExist(id, prop)
    GoUtils->>CUtils: is_property_exist(id, prop)
    activate CUtils
    CUtils->>Mutex: pthread_mutex_lock(x11_global_mutex)
    activate Mutex
    Mutex-->>CUtils: lock_acquired
    deactivate Mutex

    CUtils->>CUtils: is_property_exist_unlocked(id, prop)
    CUtils->>X11: XOpenDisplay()
    activate X11
    X11-->>CUtils: Display*
    CUtils->>X11: XIListProperties(deviceid)
    X11-->>CUtils: props[], nprops
    loop iterate_properties
        CUtils->>X11: XGetAtomName(disp, atom)
        X11-->>CUtils: name
        CUtils->>CUtils: strcmp(name, prop)
        CUtils->>X11: XFree(name)
    end
    CUtils->>X11: XCloseDisplay(disp)
    deactivate X11

    CUtils->>Mutex: pthread_mutex_unlock(x11_global_mutex)
    Mutex-->>CUtils: lock_released

    CUtils-->>GoUtils: bool_exist
    deactivate CUtils
    GoUtils-->>GoGoroutine: bool_exist
Loading

Class diagram for dxinput X11 mutex and utility modules

classDiagram
    class X11MutexModule {
        +pthread_mutex_t x11_global_mutex
    }

    class TypeModule {
        +int query_device_type(int deviceid)
        +int query_device_type_unlocked(int deviceid)
        +int is_property_exist(int deviceid, const char* prop)
        -int is_property_exist_unlocked(int deviceid, const char* prop)
        -int is_mouse_device(int deviceid)
        -int is_touchpad_device(int deviceid)
        -int is_touchscreen_device(int deviceid)
        -int is_wacom_device(int deviceid)
        -int is_keyboard_device(int deviceid)
    }

    class PropertyModule {
        +unsigned char* get_prop(int id, const char* prop, int* nitems)
        +int set_prop_int(int id, const char* prop, unsigned char* data, int nitems, int bit)
        +int set_prop_float(int id, const char* prop, unsigned char* data, int nitems)
        +int set_prop(int id, const char* prop, unsigned char* data, int nitems, Atom type, int format)
    }

    class ButtonMapModule {
        +unsigned char* get_button_map(unsigned long xid, const char* name, int* nbuttons)
        +int set_button_map(unsigned long xid, const char* name, unsigned char* map, int nbuttons)
        -int get_button_number(Display* disp, const char* name)
        -int get_device_button_number(const XDeviceInfo* dev)
        -unsigned char* do_get_button_map(Display* disp, unsigned long xid, int nbuttons)
        -const XDeviceInfo* find_device_by_name(const XDeviceInfo* devs, int ndevs, const char* name)
    }

    class ListModule {
        +DeviceInfo* list_device(int* num)
        -int append_device(DeviceInfo** devs, XIDeviceInfo* xinfo, int idx)
        -void free_device_info(DeviceInfo* dev)
    }

    class KeyboardModule {
        +int set_keyboard_repeat(int repeated, unsigned int delay, unsigned int interval)
    }

    class GoWrapper {
        +bool IsPropertyExist(int32 id, string prop)
        +[]byte GetProperty(int32 id, string prop)
        +error SetInt8Prop(int32 id, string prop, []int8 values)
        +error SetInt16Prop(int32 id, string prop, []int16 values)
        +error SetInt32Prop(int32 id, string prop, []int32 values)
        +error SetFloat32Prop(int32 id, string prop, []float32 values)
        +[]byte ucharArrayToByte(*C.uchar cData, int length)
    }

    TypeModule ..> X11MutexModule : uses
    PropertyModule ..> X11MutexModule : uses
    ButtonMapModule ..> X11MutexModule : uses
    ListModule ..> X11MutexModule : uses
    KeyboardModule ..> X11MutexModule : uses

    ListModule ..> TypeModule : calls_query_device_type_unlocked
    GoWrapper ..> TypeModule : calls_query_device_type
    GoWrapper ..> PropertyModule : calls_get_set_prop
    GoWrapper ..> ButtonMapModule : calls_button_map
    GoWrapper ..> ListModule : calls_list_device
    GoWrapper ..> KeyboardModule : calls_set_keyboard_repeat
Loading

File-Level Changes

Change Details Files
Centralize X11 synchronization via a global mutex and refactor APIs into locked/unlocked variants to avoid deadlocks while ensuring thread-safe access.
  • Add x11_mutex.h/.c defining a global pthread_mutex_t x11_global_mutex for all X11 operations in dxinput
  • Replace local static pthread_mutex_t instances in list.c, type.c, property.c, button_map.c, and keyboard.c with x11_global_mutex around XOpenDisplay/XCloseDisplay and related X11 calls
  • Introduce query_device_type_unlocked and is_property_exist_unlocked that assume the caller holds x11_global_mutex, and adjust internal call sites to use *_unlocked while public entry points perform locking
  • Ensure list_device, get_prop/set_prop/set_prop_float, get_button_map/set_button_map, and set_keyboard_repeat acquire x11_global_mutex for their full X11 interaction scope
dxinput/utils/x11_mutex.h
dxinput/utils/x11_mutex.c
dxinput/utils/list.c
dxinput/utils/type.c
dxinput/utils/property.c
dxinput/utils/button_map.c
dxinput/utils/keyboard.c
Tighten memory-safety and edge-case handling in property checks and Go wrapper utilities.
  • Guard XGetAtomName results in is_property_exist_unlocked against NULL before strcmp/XFree to avoid potential crashes
  • Update Go wrapper GetProperty to treat zero-length results as nil/0 and clean up the pointer arithmetic in ucharArrayToByte for clarity and correctness
  • Normalize Go wrapper defer blocks that free C strings, ensuring they only free non-nil pointers
dxinput/utils/type.c
dxinput/utils/wrapper.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In GetProperty (wrapper.go), the new early return when cdatas == nil || nitems == 0 skips freeing cdatas, which can leak the X11-allocated buffer when cdatas != nil but nitems == 0; consider freeing cdatas before returning in that case to match the other paths.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `GetProperty` (wrapper.go), the new early return when `cdatas == nil || nitems == 0` skips freeing `cdatas`, which can leak the X11-allocated buffer when `cdatas != nil` but `nitems == 0`; consider freeing `cdatas` before returning in that case to match the other paths.

## Individual Comments

### Comment 1
<location> `dxinput/utils/wrapper.go:209` </location>
<code_context>
 func ucharArrayToByte(cData *C.uchar, length int) []byte {
</code_context>

<issue_to_address>
**suggestion:** The nil check on cdata inside ucharArrayToByte is redundant

Since cdata is derived from cData via pointer arithmetic, it cannot be nil for 0 <= i < length if cData itself is non-nil. The `if cdata == nil { break }` branch is therefore dead code. Consider removing it, or instead validating that cData (the argument) is non-nil upfront if that’s the intent.
</issue_to_address>

### Comment 2
<location> `dxinput/utils/wrapper.go:114` </location>
<code_context>
 	}()

 	nitems := C.int(0)
 	cdatas := C.get_prop(C.int(id), cprop, &nitems)
-	if cdatas == nil {
+	if cdatas == nil || nitems == 0 {
</code_context>

<issue_to_address>
**question (bug_risk):** Treating nitems == 0 as an error changes the semantics of GetProperty for empty properties

This change makes `GetProperty` return `(nil, 0)` when the property exists but is empty (non-null data pointer, `nitems == 0`), removing the distinction between “absent” (nil) and “empty” (zero-length slice). If callers rely on that distinction, consider only treating `cdatas == nil` as an error and returning an explicit empty slice (e.g. `make([]byte, 0)`) when `nitems == 0` but `cdatas != nil`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

cdata := (*C.uchar)(unsafe.Pointer(uintptr(unsafe.Pointer(cData)) + uintptr(i)*cItemSize))
offset := uintptr(i) * cItemSize
addr := uintptr(unsafe.Pointer(cData)) + offset
cdata := (*C.uchar)(unsafe.Pointer(addr))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The nil check on cdata inside ucharArrayToByte is redundant

Since cdata is derived from cData via pointer arithmetic, it cannot be nil for 0 <= i < length if cData itself is non-nil. The if cdata == nil { break } branch is therefore dead code. Consider removing it, or instead validating that cData (the argument) is non-nil upfront if that’s the intent.


nitems := C.int(0)
cdatas := C.get_prop(C.int(id), cprop, &nitems)
if cdatas == nil {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (bug_risk): Treating nitems == 0 as an error changes the semantics of GetProperty for empty properties

This change makes GetProperty return (nil, 0) when the property exists but is empty (non-null data pointer, nitems == 0), removing the distinction between “absent” (nil) and “empty” (zero-length slice). If callers rely on that distinction, consider only treating cdatas == nil as an error and returning an explicit empty slice (e.g. make([]byte, 0)) when nitems == 0 but cdatas != nil.

@deepin-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: fly602, mhduiy

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@fly602 fly602 merged commit 1253d38 into linuxdeepin:master Jan 6, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants