Conversation
Every QSslConfiguration built in this codebase (licenseagent.cpp's license/registration traffic, downloader.cpp's firmware info/file download) explicitly set QSslSocket::VerifyNone, disabling server certificate verification. A network man-in-the-middle could forge the firmware INFO/VERSION/LINK response and firmware image the app downloads and offers to flash to the connected analyzer, and could read the user's name/email sent during registration. Remove the overrides entirely so requests use Qt's default (VerifyPeer).
hidRead() reads a device-supplied length byte (readBuff[1], 0-255) as the loop bound for copying out of a fixed 64-byte stack buffer, with no check against either the buffer size or how many bytes hid_read() actually returned. A device (or firmware bug) reporting a length over 62 causes an over-read of up to ~192 bytes of adjacent stack memory, which then gets appended into the parsed measurement stream. Clamp the copy length to both the buffer size and the actual read count, and zero-initialize the buffer. Same bug, same fix, in both hidanalyzer.cpp and its hid_analyzer.cpp twin (built on different platforms per AntScope.pro).
dataReceived() passed every incoming BLE notification straight to checkCRC()/returnCRC(), both of which index data[BLE_PACKET_SIZE-1] (byte 19) with no length check. In a release build (QByteArray::at()'s bounds assert compiles out under QT_NO_DEBUG) a malformed or malicious BLE peripheral sending a notification shorter than 20 bytes causes an out-of-bounds read. Reject short packets before they reach any of the parsing paths.
The probe loop reading the AA-230's firmware-info response gives up after 10 timeouts and falls through regardless of how many bytes actually arrived; only arr.isEmpty() was checked before memcpy'ing sizeof(FirmwareInfo) bytes out of it. A slow, short, or malicious response shorter than the struct causes an out-of-bounds heap read. Check the actual length instead.
memcpy((char*)&info, &buff[6], 60) on a 65-byte buff reads buff[65], one byte past the end. Clamp the copy length to both the buffer size and the number of bytes actually received by hid_read_timeout().
archlinux-github
pushed a commit
to archlinux/aur
that referenced
this pull request
Aug 31, 2026
Upstream has moved from 1.2.6 (Qt5) to 2.0.3 (Qt6, Bluetooth support,
UI redesign) with no tags or releases for Linux, so the head commit is
pinned as of packaging (bc7b74d). Reworks the package around that:
- depends on qt6-base/qt6-serialport/qt6-connectivity/libusb instead
of qt5-base/qt5-serialport/glibc/gcc-libs
- license now lists both MIT (upstream's own code) and
GPL-3.0-or-later (the vendored qcustomplot.cpp), previously just MIT
- install layout simplified to a normal /usr/bin/AntScope2 binary
instead of /usr/share/antscope2/AntScope2 plus a symlink
- fixes real bugs in the old PKGBUILD: LICENSE.txt installed to a
nonstandard location, a broken .icns icon reference in the desktop
file (Linux icon themes don't resolve macOS icon files), a udev rule
numbered after 73-seat-late.rules so its TAG+="uaccess" never
actually took effect, and a self-conflicting conflicts=('antscope2'
'antscope2-git') plus a typo'd provide=() that pacman silently
ignored
- adds the two known upstream Linux defects as patches: a build-
breaking reference to a nonexistent screeninfo.h, and
Settings::localDataPath() resolving data files relative to the
binary's own directory rather than a real install path
Also adds 13 patches for a flickering-popups/title-bar bug (a
WindowActivate/WindowDeactivate feedback loop against certain Wayland
compositors) and its underlying defects, plus five out-of-bounds
reads in device/network parsing and disabled TLS certificate
verification - found and fixed while chasing the flicker bug, and
submitted upstream as:
rigexpert/AntScope2#29
rigexpert/AntScope2#30
Not yet merged as of packaging; applied here in the meantime.
… forever
update() sends a RESET command and then waits for m_bootMode to become
true via an unbounded while(1) { ...; QCoreApplication::processEvents(); }
loop, with no way out but that flag becoming true. m_bootMode is only
ever set by the device re-arrival detection path, which does not
currently work (this firmware-update feature is incomplete and
unreachable from the UI - the app never re-opens the device once it
re-enumerates in bootloader mode). So today, any call into this path
spins forever instead of ever reaching the existing failure handling
immediately below it ('Can't enter to boot mode!').
This does not make firmware update work - completing that needs a real
device-re-detection implementation, verified against actual hardware
or a protocol reference, which this change does not attempt. It only
ensures that if this path is ever reached, it fails cleanly with a
bounded timeout instead of hanging indefinitely. Same bug, same fix, in
both hidanalyzer.cpp and its hid_analyzer.cpp twin.
… HidAnalyzer::update() Two defects documented in docs/firmware-update-protocol-findings.md, fixed together since the second follows naturally from properly finishing the first: 1. After sending RESET, the device re-enumerates under RE_BOOT_VID: RE_BOOT_PID with the same USB serial number (verified against a real capture of a successful update performed by the genuine vendor Windows client). Nothing previously reopened the device there - the app's normal hot-plug detection (searchAnalyzer()/m_devices) is gated behind g_usbOnly and, even when reachable, only ever looks for the application-mode VID:PID. Added waitForBootDevice(), a bounded poll (hid_enumerate on the boot VID:PID, matched by serial number) that actually reopens the device and sets m_bootMode, replacing the previous busy-wait that could never succeed. 2. The write loop only ever read the device's response after the first chunk (BL_CMD_WRITE); the ~4700+ subsequent BL_CMD_DATA chunks were never read at all, so a real BL_CMD_ERROR from the device was silently ignored, and by the time the final BL_CMD_CHECK was sent the read queue held nothing but stale BL_CMD_OK reports left over from earlier chunks - the "checksum verification" was reading one of those, not a real answer, and so reported success unconditionally. Now reads and checks the response after every chunk, aborting immediately on failure; this also fixes the stale-CHECK-read issue as a direct consequence, since the read queue never has a chance to accumulate stale entries. Also resets m_bootMode in preUpdate() - previously a second update() call after one had already run would skip the boot-mode entry block and immediately try to write to the by-then-null m_hidDevice. NOT verified against real hardware. Validated so far only by static reading and cross-checking against a real captured protocol trace (see the findings doc). A software-simulated bootloader test harness is the next step before this is trusted for a real device.
setTestHidDevice(), compiled only under ANTSCOPE2_UNIT_TEST (undefined in the real app build), lets tests/hid_update_mock/ inject an already-open device handle and serial number directly, bypassing the normal UI-dialog- driven connection establishment (SelectDeviceDialog / AnalyzerPro:: createDevice) that update() itself has no part in. Has no effect on and is not reachable from the real application.
…Linux analyzer/usbhid/hidapi/linux/hid.c has kernel-driver-detachment code gated behind #ifdef DETACH_KERNEL_DRIVER, which AntScope.pro never defined. On Linux, the kernel's usbhid driver auto-binds normal-mode HID interfaces; without detaching it first, libusb_claim_interface() can still nominally succeed, but a subsequent interrupt-OUT hid_write() to that interface can fail outright. Observed directly: HidAnalyzer::update()'s RESET report (needed to enter the DFU bootloader for a firmware update) returned -1 from hid_write() on a real device whose normal-mode interface the kernel had already claimed, even though hid_open() for the same device succeeded moments earlier. Defining DETACH_KERNEL_DRIVER resolved it - verified on real hardware (a RigExpert Stick230), both for the RESET report specifically and for a subsequent full firmware write completing successfully afterward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013EAiXVpAbBuCAxWNA3mzJY
Drives the real, unmodified HidAnalyzer::update() against a software- simulated bootloader (same command bytes, chunk size, and ACK value as a real captured update session), using the existing ANTSCOPE2_UNIT_TEST-gated setTestHidDevice() seam. No real hardware or USB stack involved. Four scenarios, 20 assertions: - a clean update (all chunks written, CHECK and START both sent, nothing ever left unread before the next write); - a BL_CMD_ERROR injected mid-stream (update stops immediately, CHECK/START never sent); - a failing BL_CMD_CHECK (all chunks still sent, but START is correctly never sent - this is the exact stale-read defect the fix in the previous commits closes: a stale queued OK previously made this check always pass regardless of the device's real answer); - a firmware size that is not a multiple of 48 bytes (the short-final-chunk path, previously untested since the one available real firmware sample happened to be an exact multiple). The harness's sensitivity was verified, not just its result: with the per-chunk read/check deliberately reverted to the original "only read after the first chunk" bug, the same test correctly fails. Build and run: qmake6 tests/hid_update_mock/hid_update_mock.pro && make && ./hid_update_mock Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013EAiXVpAbBuCAxWNA3mzJY
Windows still said Qt5, and Linux said "to do" despite qmake6 building this cleanly on Linux and two AUR packages (antscope2, antscope2-git) existing and being actively maintained. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013EAiXVpAbBuCAxWNA3mzJY
dc0sk
force-pushed
the
fix/security-hardening
branch
from
September 2, 2026 12:57
8a24f18 to
89d223e
Compare
This was referenced Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two groups of fixes, both found while auditing the codebase for a UI bug (see #29):
All are built and verified compiling. The HID read-path fixes and the firmware-update fix were verified against real hardware, not just compiled — see "Test plan" below.
Changes: security hardening
Enforce TLS certificate verification on all network requests.
Every
QSslConfigurationbuilt in this codebase (licenseagent.cpp's license/registration traffic,analyzer/updater/downloader.cpp's firmware info/file download) explicitly setQSslSocket::VerifyNone, disabling server certificate verification entirely. A network man-in-the-middle could forge the firmwareINFO/VERSION/LINKresponse and the firmware image the app downloads and offers to flash to the connected analyzer, and could read the user's name/email sent during registration. Removed the overrides so requests use Qt's default (VerifyPeer).Out-of-bounds stack read of device-supplied HID length.
hidRead()reads a device-supplied length byte (readBuff[1], 0-255) as the loop bound for copying out of a fixed 64-byte stack buffer, with no check against either the buffer size or how many byteshid_read()actually returned. A device (or firmware bug) reporting a length over 62 causes an over-read of up to ~192 bytes of adjacent stack memory, appended straight into the parsed measurement stream. Clamped the copy length to both the buffer size and the actual read count, and zero-initialized the buffer. Same bug, same fix, in bothhidanalyzer.cppand itshid_analyzer.cpptwin (built on different platforms perAntScope.pro).Reject BLE notifications shorter than a full packet before parsing.
dataReceived()passed every incoming BLE notification straight tocheckCRC()/returnCRC(), both of which indexdata[BLE_PACKET_SIZE-1](byte 19) with no length check. In a release build (QByteArray::at()'s bounds assert compiles out underQT_NO_DEBUG) a malformed or malicious BLE peripheral sending a notification shorter than 20 bytes causes an out-of-bounds read. Rejected short packets before they reach any parsing path.Out-of-bounds read of a short firmware-info response (AA-230).
The probe loop reading the AA-230's firmware-info response gives up after 10 timeouts and falls through regardless of how many bytes actually arrived; only
arr.isEmpty()was checked beforememcpy'ingsizeof(FirmwareInfo)bytes out of it. A slow, short, or malicious response shorter than the struct causes an out-of-bounds heap read. Now checks the actual length.One-byte out-of-bounds read in HID firmware-info parsing.
memcpy((char*)&info, &buff[6], 60)on a 65-bytebuffreadsbuff[65], one byte past the end. Clamped the copy length to both the buffer size and the number of bytes actually received byhid_read_timeout().Changes: firmware-update completion
HidAnalyzer::update()(the HID-based firmware-update path used by the Stick family and others) had three compounding defects that together made the feature both unusable and silently unsafe:1. Infinite hang entering the bootloader. After sending the RESET command to reboot the device into its DFU bootloader, the app's normal hot-plug detection (
searchAnalyzer()/m_devices) is gated behindg_usbOnlyand only ever looks for the application-mode VID:PID anyway — it can never find the device in bootloader mode. Nothing else waited for or detected the re-enumeration, so the UI just hung. AddedHidAnalyzer::waitForBootDevice(qint64 timeoutMs): a bounded poll ofhid_enumerate(RE_BOOT_VID, RE_BOOT_PID), matched by USB serial number (verified to persist across the mode switch against a real capture), that reopens the device and setsm_bootMode.2. Firmware verification silently always "passed". The write loop only ever read a response after the first chunk (
BL_CMD_WRITE); every subsequentBL_CMD_DATAchunk's response was left unread. HIDAPI's background read thread queues unread reports, capped at 30, discarding the oldest — so by the time the finalBL_CMD_CHECKwas sent, the read queue held nothing but staleOKreports from recent chunks. The "verification" step was reading one of those, not the bootloader's real answer toCHECK, so it reported success unconditionally, andBL_CMD_START(boot the new image) was issued regardless of whether the flash actually verified. Fixed by reading and checking the response after every chunk, aborting immediately onBL_CMD_ERROR— this also fixes the stale-read issue as a direct consequence, since the queue no longer has a chance to accumulate unread entries.3.
hid_write()failing outright for the RESET report on Linux.analyzer/usbhid/hidapi/linux/hid.c's kernel-driver-detachment code is gated behind#ifdef DETACH_KERNEL_DRIVER, whichAntScope.pronever defined. The kernel'susbhiddriver auto-binds normal-mode HID interfaces on Linux; without detaching it first,libusb_claim_interface()can still nominally succeed, but a subsequent interrupt-OUThid_write()— such as the RESET report needed to enter the bootloader — can fail outright. DefiningDETACH_KERNEL_DRIVERresolved this, confirmed on real hardware.A new unit test harness (
tests/hid_update_mock/) drives the real, unmodifiedupdate()against a software-simulated bootloader modeling the real protocol (same command bytes, chunk size, and ACK value as a genuine captured update session), using a smallANTSCOPE2_UNIT_TEST-gated seam (setTestHidDevice()) that's inert in the shipped app. Four scenarios, 20 assertions, covering a clean update, a mid-stream device error, a failing integrity check (the exact defect (2) closes), and a firmware size that isn't a multiple of the 48-byte chunk size. The harness's own sensitivity was verified by deliberately reverting the fix and confirming the same test then correctly fails.README update.
Windows still said Qt5 (the project moved to Qt6), and Linux said "to do" despite
qmake6building this cleanly and two AUR packages (antscope2,antscope2-git) existing and being actively maintained. Updated both, with real build instructions and links to the packages.Test plan
CHECK→START), both completing successfully with noBL_CMD_ERRORtests/hid_update_mock/: 4 scenarios, 20 assertions, all passing; sabotage-verified (reverting the fix makes the relevant assertion correctly fail)VerifyNoneoverride, restoring Qt's documented default behavior🤖 Generated with Claude Code