pax_global_header00006660000000000000000000000064152720611310014510gustar00rootroot0000000000000052 comment=d4dde21e085bd3307b2bfa6a81858b7ac56fc0ff linux-msm-qdl-6e625aa/000077500000000000000000000000001527206113100146325ustar00rootroot00000000000000linux-msm-qdl-6e625aa/.checkpatch.conf000066400000000000000000000020061527206113100176520ustar00rootroot00000000000000--no-tree --strict --max-line-length=120 --ignore FILE_PATH_CHANGES --ignore EMAIL_SUBJECT --ignore SPLIT_STRING # NEW_TYPEDEFS reports "do not add new typedefs" # typedef struct __attribute__((__packed__)) sparse_header { --ignore NEW_TYPEDEFS # PREFER_DEFINED_ATTRIBUTE_MACRO reports this kind of messages: # WARNING: Prefer __packed over __attribute__((__packed__)) --ignore PREFER_DEFINED_ATTRIBUTE_MACRO # PREFER_KERNEL_TYPES reports this kind of messages (when using --strict): # "Prefer kernel type 'u32' over 'uint32_t'" --ignore PREFER_KERNEL_TYPES # BRACES reports this kind of messages: # braces {} are not necessary for any arm of this statement --ignore BRACES # CAMELCASE reports this kind of messages: # Avoid CamelCase: --ignore CAMELCASE # AVOID_EXTERNS reports this kind of messages: # externs should be avoided in .c files # extern const char *__progname; --ignore AVOID_EXTERNS # COMMIT_LOG_LONG_LINE reports line lengths > 75 in commit log # Lets ignore this --ignore COMMIT_LOG_LONG_LINE linux-msm-qdl-6e625aa/.github/000077500000000000000000000000001527206113100161725ustar00rootroot00000000000000linux-msm-qdl-6e625aa/.github/CODEOWNERS000066400000000000000000000000431527206113100175620ustar00rootroot00000000000000* @linux-msm/qdl-maintainers linux-msm-qdl-6e625aa/.github/workflows/000077500000000000000000000000001527206113100202275ustar00rootroot00000000000000linux-msm-qdl-6e625aa/.github/workflows/build.yml000066400000000000000000000156721527206113100220640ustar00rootroot00000000000000name: Buildtest on: pull_request: push: jobs: build-linux: strategy: fail-fast: false matrix: include: - { arch: x64, os: 26, runner: ubuntu-26.04 } - { arch: arm64, os: 26, runner: ubuntu-26.04-arm } - { arch: x64, os: 24, runner: ubuntu-24.04 } - { arch: arm64, os: 24, runner: ubuntu-24.04-arm } - { arch: x64, os: 22, runner: ubuntu-22.04 } - { arch: arm64, os: 22, runner: ubuntu-22.04-arm } runs-on: ${{ matrix.runner }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Dependencies run: | sudo apt-get update sudo apt-get install -y libcmocka-dev libxml2-dev libusb-1.0-0-dev libzip-dev help2man pipx zip pipx ensurepath pipx install meson==1.4.0 - name: Configure run: meson setup build - name: Build run: meson compile -C build - name: Run tests run: meson test -C build --print-errorlogs - name: Generate man pages run: meson compile manpages -C build - name: Package run: | set -euo pipefail mkdir dist cp ./build/qdl dist # Bundle every shared-library dependency except the C runtime and # the dynamic loader, so the binary runs without libxml2/libzip/etc. # installed on the host. ldd ./build/qdl | awk '/=> \//{print $3}' | while read -r lib; do case "$lib" in */libc.so.*|*/libm.so.*|*/libpthread.so.*|*/libdl.so.*|*/librt.so.*|*/ld-linux*|*/ld64.so.*) ;; *) cp -vL "$lib" dist/ ;; esac done chmod 0755 dist/qdl chmod 0644 dist/*.so* # RUNPATH is not inherited by transitively-loaded libraries, so point # the binary and every bundled library at their own directory. for f in dist/qdl dist/*.so*; do patchelf --set-rpath '$ORIGIN' "$f"; done # Fail the build if anything is unresolved or resolves outside dist/. if ldd dist/qdl | grep -q 'not found'; then ldd dist/qdl; echo "::error::qdl has unresolved libraries"; exit 1 fi leaked=$(ldd dist/qdl | awk '/=> \//{print $3}' \ | grep -vE '/lib(c|m|pthread|dl|rt)\.so' | grep -v "$PWD/dist/" || true) if [ -n "$leaked" ]; then echo "::error::qdl depends on unbundled libraries:"; echo "$leaked"; exit 1 fi - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: qdl-binary-ubuntu-${{matrix.os}}-${{ matrix.arch }} path: dist/* build-mac: strategy: fail-fast: false matrix: include: - { sys: macos-26, os: 26, arch: arm64 } - { sys: macos-15, os: 15, arch: arm64 } - { sys: macos-15-intel, os: 15, arch: intel } runs-on: ${{ matrix.sys }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Dependencies run: | brew install cmocka libxml2 libzip meson ninja help2man zip dylibbundler - name: Configure run: meson setup build - name: Build run: meson compile -C build - name: Generate man pages run: meson compile manpages -C build - name: Run tests run: meson test -C build --print-errorlogs - name: Package run: | set -euxo pipefail mkdir dist # Recursively copy every non-system dylib next to the binary and # rewrite the load commands to @executable_path. Apple-provided # libraries under /usr/lib and /System are left as system references. dylibbundler --overwrite-dir --bundle-deps \ --fix-file ./build/qdl --dest-dir dist --install-path @executable_path/ cp ./build/qdl dist otool -L dist/qdl dist/qdl || true # Fail the build if the binary still references Homebrew locations. if otool -L dist/qdl | awk 'NR>1{print $1}' | grep -E '/(opt/homebrew|usr/local)/'; then echo "::error::qdl still references non-bundled Homebrew libraries"; exit 1 fi - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: qdl-binary-macos-${{ matrix.os }}-${{ matrix.arch }} path: dist/* build-windows: strategy: fail-fast: false matrix: include: - { sys: windows-latest, arch: x64 } - { sys: windows-11-arm, arch: arm64 } runs-on: ${{ matrix.sys }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Setup MSYS2 id: msys2 uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2.33.0 with: msystem: MINGW64 install: > base-devel git help2man mingw-w64-x86_64-gcc mingw-w64-x86_64-meson mingw-w64-x86_64-ninja mingw-w64-x86_64-cmocka mingw-w64-x86_64-libxml2 mingw-w64-x86_64-libusb mingw-w64-x86_64-libzip mingw-w64-x86_64-xz zip - name: Configure run: meson setup build shell: msys2 {0} - name: Build run: | git config --global core.autocrlf true meson compile -C build shell: msys2 {0} - name: Generate man pages run: meson compile manpages -C build shell: msys2 {0} - name: Run tests run: meson test -C build --print-errorlogs shell: msys2 {0} - name: Package shell: pwsh env: MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }} run: | $BIN_DIR = Join-Path $env:MSYS2_LOCATION "mingw64\bin" $DistDir = "dist" New-Item -ItemType Directory -Path $DistDir | Out-Null Copy-Item (Join-Path $BIN_DIR "zlib1.dll") $DistDir Copy-Item (Join-Path $BIN_DIR "libxml2-16.dll") $DistDir Copy-Item (Join-Path $BIN_DIR "libusb-1.0.dll") $DistDir Copy-Item (Join-Path $BIN_DIR "libzip.dll") $DistDir Copy-Item (Join-Path $BIN_DIR "liblzma-5.dll") $DistDir Copy-Item (Join-Path $BIN_DIR "libiconv-2.dll") $DistDir Copy-Item (Join-Path $BIN_DIR "libbz2-1.dll") $DistDir Copy-Item (Join-Path $BIN_DIR "libzstd.dll") $DistDir Copy-Item "./build/qdl.exe" $DistDir - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: qdl-binary-windows-${{ matrix.arch }} path: dist/* linux-msm-qdl-6e625aa/.github/workflows/checkpatch.yml000066400000000000000000000010251527206113100230450ustar00rootroot00000000000000name: Checkpatch Review on: [pull_request] jobs: check-patch: name: checkpatch review runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Run checkpatch review env: CHECKPATCH_BASE: ${{ github.event.pull_request.base.sha }} CHECKPATCH_HEAD: ${{ github.event.pull_request.head.sha }} run: ./scripts/checkpatch_wrapper.sh check-range linux-msm-qdl-6e625aa/.github/workflows/codeql.yml000066400000000000000000000016541527206113100222270ustar00rootroot00000000000000name: CodeQL on: pull_request: push: jobs: codeql: permissions: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs packages: read runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Dependencies run: | sudo apt-get update sudo apt-get install -y libxml2-dev libusb-1.0-0-dev - name: CodeQL init uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: languages: c-cpp build-mode: autobuild - name: CodeQL build uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 - name: CodeQL analysis uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 linux-msm-qdl-6e625aa/.github/workflows/commitlint.yml000066400000000000000000000007231527206113100231330ustar00rootroot00000000000000name: Commit message lint on: pull_request: types: [opened, synchronize, reopened, edited] jobs: commitlint: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Lint commit messages uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6.2.1 with: configFile: commitlint.config.mjs linux-msm-qdl-6e625aa/.github/workflows/hil.yml000066400000000000000000000112641527206113100215320ustar00rootroot00000000000000name: HIL # Hardware-in-the-loop runs: self-hosted runners with a device in EDL # reach attached (for example an Arduino Uno-Q). Board specifics stay out # of the repository: each runner provides QDL_HIL_BUILD, QDL_HIL_STORAGE, # QDL_HIL_ENTER_EDL_CMD and friends through the actions-runner .env file, # and tests/test_hil.sh skips cleanly when they are absent. # # SECURITY: these runners execute the checked-out tree against real # hardware, so this workflow must never trigger on pull requests from # forks. Keep the trigger to on-demand dispatch only, so a sweep runs # when someone asks for it and a repository without HIL runners never # queues a job it cannot service. on: workflow_dispatch: inputs: boards: description: 'Linux: JSON list of board config names (~/hil/.env on the runner), or ["default"] for the runner .env' required: false default: '' windows_boards: description: 'Windows: JSON list of board config names ($HOME/hil/.env under MSYS2), or ["default"] for the runner .env' required: false default: '' # The runners drive physical boards; run one HIL sweep at a time. concurrency: group: hil cancel-in-progress: false jobs: hil-unix: strategy: fail-fast: false matrix: os: [linux] # One entry per board attached to the runner. "default" uses the # runner's .env unchanged; any other name sources ~/hil/.env # on the runner host before the suite runs. Board list comes from # the dispatch input, else the HIL_LINUX_BOARDS repo variable. board: ${{ fromJSON(inputs.boards || vars.HIL_LINUX_BOARDS || '["default"]') }} # macOS is added via include rather than the os list: crossing os # with the Linux board list would dispatch Linux board configs to # the Mac. The Mac rig has a single board via the runner's .env. include: - os: macos board: default runs-on: [self-hosted, hil, "${{ matrix.os }}"] timeout-minutes: 600 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Configure run: meson setup build - name: Build run: meson compile -C build - name: Run HIL suite env: BOARD: ${{ matrix.board }} run: | if [ "$BOARD" != "default" ]; then set -a; . "$HOME/hil/$BOARD.env"; set +a fi meson test -C build --suite "${QDL_HIL_SUITE:-hil}" --print-errorlogs - name: Upload logs uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: always() with: name: hil-logs-${{ matrix.os }}-${{ matrix.board }} path: build/meson-logs/ hil-windows: strategy: fail-fast: false matrix: # One entry per board attached to the runner; same contract as # hil-unix. Note MSYS2's $HOME is C:\msys64\home\ by # default, so board files live there, not in the Windows profile. board: ${{ fromJSON(inputs.windows_boards || vars.HIL_WINDOWS_BOARDS || '["default"]') }} runs-on: [self-hosted, hil, windows] timeout-minutes: 600 defaults: run: shell: msys2 {0} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # release: false reuses the MSYS2 installation already present on # the runner instead of downloading one per job. The package list # mirrors build.yml. - name: Setup MSYS2 uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2.33.0 with: msystem: MINGW64 release: false install: > base-devel git help2man mingw-w64-x86_64-gcc mingw-w64-x86_64-meson mingw-w64-x86_64-ninja mingw-w64-x86_64-cmocka mingw-w64-x86_64-libxml2 mingw-w64-x86_64-libusb mingw-w64-x86_64-libzip mingw-w64-x86_64-xz zip - name: Configure run: meson setup build - name: Build run: meson compile -C build - name: Run HIL suite env: BOARD: ${{ matrix.board }} run: | if [ "$BOARD" != "default" ]; then set -a; . "$HOME/hil/$BOARD.env"; set +a fi meson test -C build --suite "${QDL_HIL_SUITE:-hil}" --print-errorlogs - name: Upload logs uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: always() with: name: hil-logs-windows-${{ matrix.board }} path: build/meson-logs/ linux-msm-qdl-6e625aa/.github/workflows/markdown-lint.yml000066400000000000000000000005511527206113100235410ustar00rootroot00000000000000name: Markdown Lint on: [pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install mdl run: | sudo apt-get update sudo apt-get install -y markdownlint - name: Run markdown-lint run: ./scripts/markdownlint_wrapper.sh check linux-msm-qdl-6e625aa/.github/workflows/qcom-preflight-checks.yml000066400000000000000000000011261527206113100251310ustar00rootroot00000000000000name: QC Preflight Checks on: pull_request: push: branches: [main] workflow_dispatch: jobs: preflight: name: Run QC Preflight Checks uses: qualcomm/qcom-reusable-workflows/.github/workflows/reusable-qcom-preflight-checks-orchestrator.yml@v2 with: enable-semgrep-scan: true enable-dependency-review: true enable-repolinter-check: true enable-copyright-license-check: true enable-commit-email-check: true enable-commit-msg-check: false enable-armor-checkers: false permissions: contents: read security-events: write linux-msm-qdl-6e625aa/.gitignore000066400000000000000000000002461527206113100166240ustar00rootroot00000000000000/build/ /tests/data/*.elf /tests/data/*.bin /tests/data/*.img *.o qdl qdl-ramdump ks *.exe compile_commands.json .cache version.h .scripts .checkpatch-camelcase.git. linux-msm-qdl-6e625aa/AGENTS.md000066400000000000000000000073731527206113100161470ustar00rootroot00000000000000# Agent Guide for QDL This file guides automation agents working in the QDL repository. Human contributors should read [CONTRIBUTING.md](CONTRIBUTING.md); this document captures the same rules in a form suited to automated workflows. ## Project overview QDL is a command-line tool for flashing firmware on Qualcomm-based devices over USB in Emergency Download (EDL) mode. It uploads a flash loader to the device and drives it using the Sahara and Firehose protocols. ## Prerequisites and build ### Linux ```bash sudo apt install libxml2-dev libusb-1.0-0-dev libzip-dev meson ninja-build help2man meson setup build meson compile -C build ``` ### macOS (Homebrew) ```bash brew install libxml2 libusb libzip meson ninja help2man meson setup build meson compile -C build ``` ### macOS (MacPorts) ```bash sudo port install libxml2 libusb libzip meson ninja help2man meson setup build meson compile -C build ``` ## Before committing Every change must build cleanly and pass the style and test checks. Run: ```bash meson setup build # once, or after changing meson.build meson compile -C build # build must succeed meson test -C build # run the test suite meson compile check -C build # checkpatch (Linux kernel style) meson compile markdown-lint -C build # lint Markdown sources ``` To check only staged changes against coding style, use `meson compile check-cached -C build`. To validate a whole series the way CI does (per-commit, patch mode), use `meson compile check-range -C build`. ## Coding style - Follow the [Linux kernel coding style](https://www.kernel.org/doc/html/latest/process/coding-style.html). - Style violations are caught by the `check` / `check-cached` targets above. - Use ASCII only. Neither code, documentation, nor commit messages may contain non-ASCII characters. Replace them with ASCII alternatives, e.g. use `-` or `--` instead of en/em dashes, straight `'` and `"` instead of typographic quotes, and `...` instead of an ellipsis character. - Use appropriate SPDX license identifiers in sources files ## Commit conventions Every commit message must follow the repository convention: - **The subject must carry a subsystem tag.** Prefix it with the affected subsystem, followed by a lowercase, imperative summary, e.g. `usb: add support for listing devices`, `firehose: report progress through the skipblock fast-path`, `github: package self-contained binaries on Linux and macOS`. Common prefixes include `qdl`, `usb`, `sahara`, `firehose`, `program`, `vip`, `ramdump`, `tests`, `github`, `meson`, `README`, and `doc`. Use `github:` (not `ci:`) for CI and workflow changes. - **The commit must have a body explaining the rationale** - why the change is needed and any key design decisions - not just what changed. The subject alone is not sufficient for anything beyond a trivial fix. - Keep the subject short and specific; capture intent, not a file-by-file dump. - Use the imperative mood (Add, Update, Drop, Enable, Revert). - Wrap body lines at ~72 characters. - Do not mix unrelated changes in one commit; split them logically. - Each patch must be logically coherent, self-contained, and independently buildable. The tree must remain functional after every commit. ## Sign-off Every commit must carry a `Signed-off-by` trailer using the identity from the local git configuration, as appended by `git commit -s`: ```bash git commit -s ``` When committing programmatically, **AI agents MUST NOT add Signed-off-by tags**, only a human can certify the Developer Certificate of Origin (DCO). Do not add any other trailers (for example, co-author trailers). ## Submitting Submit patches against the `master` branch as a GitHub pull request: fork the repo, create a branch, do the work, rebase onto upstream, and open the PR. linux-msm-qdl-6e625aa/CHANGELOG.md000066400000000000000000000643471527206113100164610ustar00rootroot00000000000000# Changelog All notable changes to QDL, mirroring the [GitHub releases](https://github.com/linux-msm/qdl/releases). Each entry lists the pull requests merged for that release. This project uses `vMAJOR.MINOR` tags. Releases prior to v2.4 were tagged but not published as GitHub releases; see the [git tags](https://github.com/linux-msm/qdl/tags) for their history. ## [v2.9](https://github.com/linux-msm/qdl/releases/tag/v2.9) - 2026-11-02 ### Highlights - Standalone `qdl reset` subcommand: kicks a board out of EDL or crash mode over Sahara without uploading a programmer, falling back to a Firehose power reset when one is already running ([#307](https://github.com/linux-msm/qdl/pull/307)) - `qdl ks` no longer needs a device node: it uses the standard usb/qud backends, so kickstart works over libusb and on Windows and can replace QSaharaServer on flashless boards ([#300](https://github.com/linux-msm/qdl/pull/300)) - New `create-sahara-archive` subcommand to build a Sahara programmer archive from a `contents.xml` or a list of images ([#303](https://github.com/linux-msm/qdl/pull/303)) - UFS provisioning fixes: the commit step gets a longer timeout and a device that resets right after committing the configuration descriptor is reported as success ([#314](https://github.com/linux-msm/qdl/pull/314), [#322](https://github.com/linux-msm/qdl/pull/322)) - NAND program tags carry the partition label, so programmers that resolve partitions by name apply the bad-block adjusted offset ([#306](https://github.com/linux-msm/qdl/pull/306)) - Sahara end-of-image and done status codes are decoded into human-readable descriptions ([#336](https://github.com/linux-msm/qdl/pull/336)) - Millisecond timestamps on `--debug` diagnostics to spot stalls and correlate host and device activity ([#321](https://github.com/linux-msm/qdl/pull/321)) - Second hardening batch across parsers, backends and the build ([#302](https://github.com/linux-msm/qdl/pull/302)) - USB backend fixes: zero-length packet reads are deferred to the next real transfer, so Firehose no longer stalls on xHCI controllers that complete the speculative read early (ASMedia ASM2142/3142); `--serial` falls back to `iSerialNumber` when `iProduct` carries no serial, retries transient string descriptor reads right after enumeration and matches case-insensitively ([#346](https://github.com/linux-msm/qdl/pull/346), [#348](https://github.com/linux-msm/qdl/pull/348)) - Readable output: `--color[=auto|always|never]`, `ERR:`/`WARN:` prefixes, uppercase subsystem tags, result lines with size and rate, a single run summary line and `-q/--quiet` for CI and production lines ([#341](https://github.com/linux-msm/qdl/pull/341)) - Linux udev rules are installed with qdl, granting local users access to EDL and crashdump devices without running as root ([#342](https://github.com/linux-msm/qdl/pull/342)) - Leaner builds: zip container support is a meson feature option (`-Dzip-container=disabled`) and the meson floor is lowered to 0.56 so qdl can be packaged for EL9 ([#299](https://github.com/linux-msm/qdl/pull/299), [#333](https://github.com/linux-msm/qdl/pull/333)) - Document official packages: qdl is in Debian, Ubuntu, Fedora and EPEL, and an official [Homebrew formula](https://formulae.brew.sh/formula/qdl) is available in homebrew-core (`brew install qdl`) ([#338](https://github.com/linux-msm/qdl/pull/338)) - The HIL suite runs unattended on self-hosted Linux and macOS runners with real boards, including VIP end to end ([#296](https://github.com/linux-msm/qdl/pull/296), [#340](https://github.com/linux-msm/qdl/pull/340)) - Documentation refreshed and split into per-topic guides under `docs/` ([#329](https://github.com/linux-msm/qdl/pull/329), [#317](https://github.com/linux-msm/qdl/pull/317)) ### What's Changed - qdl: let kickstart use the standard device backends by @igoropaniuk in [#300](https://github.com/linux-msm/qdl/pull/300) - meson: make zip container support optional by @igoropaniuk in [#299](https://github.com/linux-msm/qdl/pull/299) - For linux msm/create sahara archive by @quic-bjorande in [#303](https://github.com/linux-msm/qdl/pull/303) - qdl: add a standalone reset subcommand by @igoropaniuk in [#307](https://github.com/linux-msm/qdl/pull/307) - Harden error paths across parsers, backends and the build by @igoropaniuk in [#302](https://github.com/linux-msm/qdl/pull/302) - Run the HIL suite unattended on self-hosted runners by @igoropaniuk in [#296](https://github.com/linux-msm/qdl/pull/296) - firehose: deduplicate the chunk-streaming loop by @igoropaniuk in [#308](https://github.com/linux-msm/qdl/pull/308) - ufs: replace module globals with a caller-owned context by @igoropaniuk in [#309](https://github.com/linux-msm/qdl/pull/309) - firehose: send the partition label in NAND program tags by @franciscojsfc in [#306](https://github.com/linux-msm/qdl/pull/306) - README: document installer package creation by @quic-bjorande in [#317](https://github.com/linux-msm/qdl/pull/317) - commitlint: recognize Closes as a footer trailer by @igoropaniuk in [#319](https://github.com/linux-msm/qdl/pull/319) - firehose: give the UFS provisioning commit a longer timeout by @igoropaniuk in [#314](https://github.com/linux-msm/qdl/pull/314) - qdl: decouple helper modules from the CLI front-end by @igoropaniuk in [#313](https://github.com/linux-msm/qdl/pull/313) - Add missing stdint.h include by @aekoroglu in [#324](https://github.com/linux-msm/qdl/pull/324) - github: build on ubuntu 26.04 runners by @igoropaniuk in [#327](https://github.com/linux-msm/qdl/pull/327) - github: drop macos-14 and build on macos-15 and macos-26 runners by @igoropaniuk in [#328](https://github.com/linux-msm/qdl/pull/328) - README: refresh and split into docs/ by @igoropaniuk in [#329](https://github.com/linux-msm/qdl/pull/329) - ux: add consistent timestamps to debug diagnostics by @igoropaniuk in [#321](https://github.com/linux-msm/qdl/pull/321) - meson: lower the required meson version to 0.56 by @igoropaniuk in [#333](https://github.com/linux-msm/qdl/pull/333) - firehose: treat -ENODEV on commit=1 as success after UFS provisioning by @JohnSagaQuic in [#322](https://github.com/linux-msm/qdl/pull/322) - qdl: split the flashing front-end from the CLI plumbing by @igoropaniuk in [#323](https://github.com/linux-msm/qdl/pull/323) - sahara: describe status codes in end-of-image and done packets by @bhavyaVeera in [#336](https://github.com/linux-msm/qdl/pull/336) - github: prepare CI for the move to the Qualcomm GitHub organization by @igoropaniuk in [#335](https://github.com/linux-msm/qdl/pull/335) - README: document distribution packages and Homebrew by @igoropaniuk in [#338](https://github.com/linux-msm/qdl/pull/338) - github: run the HIL suite on the macOS runner by @igoropaniuk in [#340](https://github.com/linux-msm/qdl/pull/340) - ux: colored output, level and subsystem prefixes, run summary and --quiet by @igoropaniuk in [#341](https://github.com/linux-msm/qdl/pull/341) - udev: install rules for EDL device access by @lool in [#342](https://github.com/linux-msm/qdl/pull/342) - usb: defer ZLP reads to real transfers by @agumarcolin in [#346](https://github.com/linux-msm/qdl/pull/346) - usb: fall back to iSerialNumber and retry transient string reads by @igoropaniuk in [#348](https://github.com/linux-msm/qdl/pull/348) ### New Contributors - @franciscojsfc made their first contribution in [#306](https://github.com/linux-msm/qdl/pull/306) - @aekoroglu made their first contribution in [#324](https://github.com/linux-msm/qdl/pull/324) - @JohnSagaQuic made their first contribution in [#322](https://github.com/linux-msm/qdl/pull/322) - @bhavyaVeera made their first contribution in [#336](https://github.com/linux-msm/qdl/pull/336) - @lool made their first contribution in [#342](https://github.com/linux-msm/qdl/pull/342) - @agumarcolin made their first contribution in [#346](https://github.com/linux-msm/qdl/pull/346) ### Testing credits Thanks to everyone who helped test v2.9-rc1 in [#345](https://github.com/linux-msm/qdl/pull/345): - @igoropaniuk tested on RB3Gen2 (secure) and Uno Q across Linux and Windows - @JohnSagaQuic tested on Kaanapali, RB3, Hawi and NordAU (SA8797P) **Full Changelog**: [v2.8...v2.9](https://github.com/linux-msm/qdl/compare/v2.8...v2.9) ## [v2.8](https://github.com/linux-msm/qdl/releases/tag/v2.8) - 2026-08-02 ### Highlights - One `qdl` binary: the separate `qdl-ks` and `qdl-ramdump` tools are folded in as subcommands ([#250](https://github.com/linux-msm/qdl/pull/250)) - New `chipinfo` and `reset` verbs for reading chip identity and resetting the device without flashing ([#280](https://github.com/linux-msm/qdl/pull/280), [#281](https://github.com/linux-msm/qdl/pull/281)) - nbdkit plugin exposing a device LUN as a block device ([#283](https://github.com/linux-msm/qdl/pull/283)) - Faster, leaner skipblock flashing: short/offset files can be skipped, digest memory is bounded, progress is reported ([#256](https://github.com/linux-msm/qdl/pull/256), [#264](https://github.com/linux-msm/qdl/pull/264), [#265](https://github.com/linux-msm/qdl/pull/265)) - Hardening sweep across firehose, sahara, GPT and program parsing of device-controlled data ([#284](https://github.com/linux-msm/qdl/pull/284), [#270](https://github.com/linux-msm/qdl/pull/270)) - Improved crash-mode/ramdump device detection ([#274](https://github.com/linux-msm/qdl/pull/274), [#286](https://github.com/linux-msm/qdl/pull/286)) - EDL devices are identified by their vendor-specific interface signature instead of a product-id allowlist, so the libusb and Windows QUD backends now share one notion of an EDL device ([#287](https://github.com/linux-msm/qdl/pull/287), [#289](https://github.com/linux-msm/qdl/pull/289)) ### Breaking changes - `qdl-ks` and `qdl-ramdump` no longer exist as standalone binaries; scripts and packaging must switch to `qdl ks ...` / the ramdump verb ([#250](https://github.com/linux-msm/qdl/pull/250)) - Mixing input XML files with command verbs on one invocation is now rejected instead of silently accepted ([#282](https://github.com/linux-msm/qdl/pull/282)) ### What's Changed - firehose: zero-pad local digest so short/offset files can be skipped by @igoropaniuk in [#256](https://github.com/linux-msm/qdl/pull/256) - meson: don't require git when overriding the version by @obbardc in [#257](https://github.com/linux-msm/qdl/pull/257) - firehose: report progress through the skipblock fast-path by @igoropaniuk in [#264](https://github.com/linux-msm/qdl/pull/264) - [RFC] github: request reviews from qdl-maintainers via CODEOWNERS by @igoropaniuk in [#266](https://github.com/linux-msm/qdl/pull/266) - tree: move sources under src/ and consolidate on a single qdl binary by @igoropaniuk in [#250](https://github.com/linux-msm/qdl/pull/250) - README: describe how to use QDL from WSL2 on Windows by @brgl in [#267](https://github.com/linux-msm/qdl/pull/267) - README: mark WSL2 support as unreliable by @brgl in [#273](https://github.com/linux-msm/qdl/pull/273) - src/firehose: fix type error in flash log by @shoudil in [#275](https://github.com/linux-msm/qdl/pull/275) - tests: fix test_contents_selectors link failure at -O0 by @igoropaniuk in [#272](https://github.com/linux-msm/qdl/pull/272) - tests: add a hardware-in-the-loop (HIL) test suite by @igoropaniuk in [#271](https://github.com/linux-msm/qdl/pull/271) - ramdump: create the output directory if it does not exist by @igoropaniuk in [#269](https://github.com/linux-msm/qdl/pull/269) - README: use consistent dash bullets for unordered lists by @igoropaniuk in [#276](https://github.com/linux-msm/qdl/pull/276) - firehose: tolerate zero-length packets while reading sector data by @igoropaniuk in [#270](https://github.com/linux-msm/qdl/pull/270) - For linux msm/flashmap multi layout by @quic-bjorande in [#263](https://github.com/linux-msm/qdl/pull/263) - firehose: chunk skipblock digest into bounded sub-regions by @igoropaniuk in [#265](https://github.com/linux-msm/qdl/pull/265) - github: package self-contained binaries on Linux and macOS by @igoropaniuk in [#277](https://github.com/linux-msm/qdl/pull/277) - doc: restructure and enrich the README by @igoropaniuk in [#268](https://github.com/linux-msm/qdl/pull/268) - doc: add RELEASING.md describing the release process by @igoropaniuk in [#278](https://github.com/linux-msm/qdl/pull/278) - [RFC] doc: add AGENTS.md guide for automation agents by @igoropaniuk in [#279](https://github.com/linux-msm/qdl/pull/279) - usb: recognize 0x90db as an EDL ramdump product ID by @yangh in [#274](https://github.com/linux-msm/qdl/pull/274) - qdl: Add reset verb for device reset by @edsj-sony in [#281](https://github.com/linux-msm/qdl/pull/281) - sahara: add chipinfo command to read chip identity by @igoropaniuk in [#280](https://github.com/linux-msm/qdl/pull/280) - [RFC] usb: decouple open/list paths and share EDL device identity across backends by @igoropaniuk in [#287](https://github.com/linux-msm/qdl/pull/287) - tests: extend HIL test suite by @igoropaniuk in [#285](https://github.com/linux-msm/qdl/pull/285) - Fix critical correctness issues (silent failures, crashes, OOB accesses) by @igoropaniuk in [#284](https://github.com/linux-msm/qdl/pull/284) - [RFC] qdl: reject mixing input XML files with command verbs by @igoropaniuk in [#282](https://github.com/linux-msm/qdl/pull/282) - Enhance crash mode device detection by @ykaire-qti in [#286](https://github.com/linux-msm/qdl/pull/286) - nbdkit: expose a device LUN as a block device by @igoropaniuk in [#283](https://github.com/linux-msm/qdl/pull/283) - vip: open signed tables in binary mode on Windows by @igoropaniuk in [#288](https://github.com/linux-msm/qdl/pull/288) - Lint all markdown files and extend the release documentation by @igoropaniuk in [#292](https://github.com/linux-msm/qdl/pull/292) - Address oscompat warnings on Windows by @igoropaniuk in [#291](https://github.com/linux-msm/qdl/pull/291) - qdl: drop the EDL product id allowlist and match the EDL interface signature when enumerating by @igoropaniuk in [#289](https://github.com/linux-msm/qdl/pull/289) - firehose: do not probe sector size when storage init is skipped by @igoropaniuk in [#297](https://github.com/linux-msm/qdl/pull/297) ### New Contributors - @brgl made their first contribution in [#267](https://github.com/linux-msm/qdl/pull/267) - @shoudil made their first contribution in [#275](https://github.com/linux-msm/qdl/pull/275) - @yangh made their first contribution in [#274](https://github.com/linux-msm/qdl/pull/274) - @edsj-sony made their first contribution in [#281](https://github.com/linux-msm/qdl/pull/281) ### Testing credits Thanks to everyone who helped test v2.8-rc1 in [#293](https://github.com/linux-msm/qdl/pull/293): - @igoropaniuk tested on RB3Gen2 (open and secure), Uno Q and Radxa Q8B (SC8280XP) across Linux, Windows and macOS - @Eric0039 tested on IQ-9075 - @obbardc tested on RB3Gen2 (Debian Unstable amd64) - @shawngsc tested on IQ10 RRD (Windows) - @pankaj-qcom tested on Glymur CRD (Ubuntu 24.04.4 LTS) - @WeepingClown13 tested on Hamoa CRD (Ubuntu 24.04.4 LTS) - @shoudil tested on Hamoa/Purwa IoT EVK (Ubuntu 24.04.4 LTS) **Full Changelog**: [v2.7...v2.8](https://github.com/linux-msm/qdl/compare/v2.7...v2.8) ## [v2.7](https://github.com/linux-msm/qdl/releases/tag/v2.7) - 2026-06-08 ### What's Changed - [RFC] meson: replace GNU Make-based build system with Meson by @igoropaniuk in [#150](https://github.com/linux-msm/qdl/pull/150) - tests: replace monolithic test wrapper with per-suite Meson tests by @igoropaniuk in [#208](https://github.com/linux-msm/qdl/pull/208) - meson: rename ks to qdl-ks by @obbardc in [#211](https://github.com/linux-msm/qdl/pull/211) - Update ks/ramdump description by @igoropaniuk in [#212](https://github.com/linux-msm/qdl/pull/212) - sahara: increase MAPPING_SZ to 128 by @mukeshojha-linux in [#215](https://github.com/linux-msm/qdl/pull/215) - For linux msm/flashmap support by @andersson in [#210](https://github.com/linux-msm/qdl/pull/210) - Unify markdown-lint and checkpatch checks by @igoropaniuk in [#222](https://github.com/linux-msm/qdl/pull/222) - tests: stop fixture generation from timing out CI on slow runners by @igoropaniuk in [#230](https://github.com/linux-msm/qdl/pull/230) - tests: give each integration test its own fixture directory by @igoropaniuk in [#231](https://github.com/linux-msm/qdl/pull/231) - commitlint: Recognize kernel-style trailers as footer items by @igoropaniuk in [#229](https://github.com/linux-msm/qdl/pull/229) - [RFC] ux: Route libxml2 parser diagnostics through ux_err() by @igoropaniuk in [#227](https://github.com/linux-msm/qdl/pull/227) - usb: Allow devices with no serial number to show in the usb list by @Zephyrus29 in [#213](https://github.com/linux-msm/qdl/pull/213) - qdl: fix list/ramdump subcommands ignored when flags precede them by @mukeshojha-linux in [#217](https://github.com/linux-msm/qdl/pull/217) - meson: avoid duplicate library entries on executable link lines by @igoropaniuk in [#228](https://github.com/linux-msm/qdl/pull/228) - For linux msm/program search relative xml by @quic-bjorande in [#223](https://github.com/linux-msm/qdl/pull/223) - Add --skip-reset and auto-skip Sahara when programmer is already running by @igoropaniuk in [#232](https://github.com/linux-msm/qdl/pull/232) - qdl: Drop alloca definition by @quic-bjorande in [#236](https://github.com/linux-msm/qdl/pull/236) - For linux msm/contents xml flashing by @quic-bjorande in [#225](https://github.com/linux-msm/qdl/pull/225) - qdl: add QUD backend so on Windows flashing via the official QDLoader 9008 driver works by @igoropaniuk in [#233](https://github.com/linux-msm/qdl/pull/233) - [RFC] qdl: add sha256 subcommand by @igoropaniuk in [#239](https://github.com/linux-msm/qdl/pull/239) - github: Include missing build artifacts in Windows build by @quic-bjorande in [#240](https://github.com/linux-msm/qdl/pull/240) - qdl: add --skipblock=sha256 to skip programs already on flash by @igoropaniuk in [#242](https://github.com/linux-msm/qdl/pull/242) - For linux msm/erase all by @quic-bjorande in [#244](https://github.com/linux-msm/qdl/pull/244) - contents: Look for OS-specific path tag by @quic-bjorande in [#245](https://github.com/linux-msm/qdl/pull/245) - sahara: Deal with QUD discarding the HELLO request by @quic-bjorande in [#246](https://github.com/linux-msm/qdl/pull/246) - firehose: detect VIP mode mismatch from programmer startup logs by @igoropaniuk in [#252](https://github.com/linux-msm/qdl/pull/252) - program: Cast away const when freeing start_sector in erase by @igoropaniuk in [#253](https://github.com/linux-msm/qdl/pull/253) - tree: drop unused .gitreview by @igoropaniuk in [#251](https://github.com/linux-msm/qdl/pull/251) - sahara: auto-assemble minidump ELF after ramdump download by @mukeshojha-linux in [#243](https://github.com/linux-msm/qdl/pull/243) ### New Contributors - @mukeshojha-linux made their first contribution in [#215](https://github.com/linux-msm/qdl/pull/215) - @Zephyrus29 made their first contribution in [#213](https://github.com/linux-msm/qdl/pull/213) **Full Changelog**: [v2.6...v2.7](https://github.com/linux-msm/qdl/compare/v2.6...v2.7) ## [v2.6](https://github.com/linux-msm/qdl/releases/tag/v2.6) - 2026-04-06 ### What's Changed - Documentation improvements by @igoropaniuk in [#194](https://github.com/linux-msm/qdl/pull/194) - Address VIP mode regressions by @igoropaniuk in [#189](https://github.com/linux-msm/qdl/pull/189) - A collection of bug fixes and a small refactor across vip/usb/firehose by @igoropaniuk in [#195](https://github.com/linux-msm/qdl/pull/195) - usb: reinitialise libusb on each poll iteration in usb_open by @igoropaniuk in [#199](https://github.com/linux-msm/qdl/pull/199) - A second batch of bug fixes and code quality improvements by @igoropaniuk in [#196](https://github.com/linux-msm/qdl/pull/196) - qdl: fix absolute image_path handling in sahara_config by @MrCry0 in [#198](https://github.com/linux-msm/qdl/pull/198) - program: Add erase command by @jcreedon in [#190](https://github.com/linux-msm/qdl/pull/190) - [RFC] ci: enforce commit message formatting in PRs by @igoropaniuk in [#203](https://github.com/linux-msm/qdl/pull/203) - Add qdl-ramdump usage documentation and fix segment filter matching by @igoropaniuk in [#202](https://github.com/linux-msm/qdl/pull/202) - sahara, usb: improve robustness and user feedback by @igoropaniuk in [#204](https://github.com/linux-msm/qdl/pull/204) - [RFC] sim: implement proper Firehose protocol state machine by @igoropaniuk in [#192](https://github.com/linux-msm/qdl/pull/192) - [RFC] sim: validate VIP digest tables and chunk hashes in dry-run mode by @igoropaniuk in [#205](https://github.com/linux-msm/qdl/pull/205) ### New Contributors - @MrCry0 made their first contribution in [#198](https://github.com/linux-msm/qdl/pull/198) **Full Changelog**: [v2.5...v2.6](https://github.com/linux-msm/qdl/compare/v2.5...v2.6) ## [v2.5](https://github.com/linux-msm/qdl/releases/tag/v2.5) - 2026-02-25 ### What's Changed - makefile: Allow cross-building by @obbardc in [#174](https://github.com/linux-msm/qdl/pull/174) - qdl: Allow absolute paths in Windows again by @andersson in [#179](https://github.com/linux-msm/qdl/pull/179) - sahara: Unbreak ramdump by making images optional again by @andersson in [#180](https://github.com/linux-msm/qdl/pull/180) - firehose: Increase reset delay after flashing by @ykaire-qti in [#175](https://github.com/linux-msm/qdl/pull/175) - firehose: Increase timeout for read of program-ack by @andersson in [#182](https://github.com/linux-msm/qdl/pull/182) - usb: add --list flag for list devices by @lucarin91 in [#176](https://github.com/linux-msm/qdl/pull/176) - For linux msm/drop single image hack by @andersson in [#181](https://github.com/linux-msm/qdl/pull/181) - For linux msm/ramdump subcmd by @andersson in [#183](https://github.com/linux-msm/qdl/pull/183) - qdl: Propagate the success of decode_sahara_config() by @andersson in [#185](https://github.com/linux-msm/qdl/pull/185) - firehose: ensure we properly set skip_storage_init in provision by @kcxt in [#186](https://github.com/linux-msm/qdl/pull/186) - Address memory leaks/mitigate buffer overflows by @igoropaniuk in [#161](https://github.com/linux-msm/qdl/pull/161) ### New Contributors - @lucarin91 made their first contribution in [#176](https://github.com/linux-msm/qdl/pull/176) - @kcxt made their first contribution in [#186](https://github.com/linux-msm/qdl/pull/186) **Full Changelog**: [v2.4...v2.5](https://github.com/linux-msm/qdl/compare/v2.4...v2.5) ## [v2.4](https://github.com/linux-msm/qdl/releases/tag/v2.4) - 2025-12-19 ### What's Changed - For linux msm/read write gpt by @andersson in [#130](https://github.com/linux-msm/qdl/pull/130) - For linux msm/missing files early and list h by @andersson in [#128](https://github.com/linux-msm/qdl/pull/128) - checkpatch: Skip list.h by @quic-bjorande in [#138](https://github.com/linux-msm/qdl/pull/138) - firehose: Provide progress bar on read as well by @quic-bjorande in [#142](https://github.com/linux-msm/qdl/pull/142) - For linux msm/db410c timeout and improvements by @quic-bjorande in [#139](https://github.com/linux-msm/qdl/pull/139) - sahara: Print an error when device isn't reachable by @quic-bjorande in [#140](https://github.com/linux-msm/qdl/pull/140) - patch: Don't spam the log when no patch file was loaded by @quic-bjorande in [#141](https://github.com/linux-msm/qdl/pull/141) - gpt: gpt_load_table_from_partition: fix off-by-one error for num_sectors by @mike-scott in [#143](https://github.com/linux-msm/qdl/pull/143) - Add build instructions for MacPorts by @idlethread in [#144](https://github.com/linux-msm/qdl/pull/144) - usb: increase bulk transfer timeout for firehose raw write by @loicpoulain in [#145](https://github.com/linux-msm/qdl/pull/145) - Misc fixes by @igoropaniuk in [#147](https://github.com/linux-msm/qdl/pull/147) - For linux msm/spinor by @andersson in [#146](https://github.com/linux-msm/qdl/pull/146) - github: Bump macos runner versions by @andersson in [#151](https://github.com/linux-msm/qdl/pull/151) - Add -h/--help support to ks and ramdump by @rogers0 in [#136](https://github.com/linux-msm/qdl/pull/136) - github: Two improvements for --version on CI build by @z3ntu in [#152](https://github.com/linux-msm/qdl/pull/152) - Suppress unused var warnings by @igoropaniuk in [#153](https://github.com/linux-msm/qdl/pull/153) - Move __unused macro after variable definitions by @igoropaniuk in [#155](https://github.com/linux-msm/qdl/pull/155) - Add manpages generated by help2man by @rogers0 in [#137](https://github.com/linux-msm/qdl/pull/137) - For linux msm/multi programmer by @quic-bjorande in [#154](https://github.com/linux-msm/qdl/pull/154) - README/CI updates/improvements by @igoropaniuk in [#157](https://github.com/linux-msm/qdl/pull/157) - [housekeeping] Integer-related compiler warnings by @igoropaniuk in [#159](https://github.com/linux-msm/qdl/pull/159) - Address more -Wsign-compare issues by @igoropaniuk in [#160](https://github.com/linux-msm/qdl/pull/160) - firehose: Add slot flag by @jcreedon in [#158](https://github.com/linux-msm/qdl/pull/158) - Check for empty patch filenames. by @ykaire-qti in [#156](https://github.com/linux-msm/qdl/pull/156) - firehose/usb: Explicitly handle ZLP on USB read transfers by @loicpoulain in [#162](https://github.com/linux-msm/qdl/pull/162) - github: Resolve liblzma-related failure in Windows builds by @andersson in [#165](https://github.com/linux-msm/qdl/pull/165) - firehose: Increase image write timeout to 60 seconds by @andersson in [#166](https://github.com/linux-msm/qdl/pull/166) - usb: Fix checkpatch warning about unnecessary parenthesis by @andersson in [#167](https://github.com/linux-msm/qdl/pull/167) - vip: Fix integer underflow in digest count by @jerry-skydio in [#172](https://github.com/linux-msm/qdl/pull/172) ### New Contributors - @mike-scott made their first contribution in [#143](https://github.com/linux-msm/qdl/pull/143) - @idlethread made their first contribution in [#144](https://github.com/linux-msm/qdl/pull/144) - @loicpoulain made their first contribution in [#145](https://github.com/linux-msm/qdl/pull/145) - @rogers0 made their first contribution in [#136](https://github.com/linux-msm/qdl/pull/136) - @jcreedon made their first contribution in [#158](https://github.com/linux-msm/qdl/pull/158) - @ykaire-qti made their first contribution in [#156](https://github.com/linux-msm/qdl/pull/156) - @jerry-skydio made their first contribution in [#172](https://github.com/linux-msm/qdl/pull/172) **Full Changelog**: [v2.2...v2.4](https://github.com/linux-msm/qdl/compare/v2.2...v2.4) linux-msm-qdl-6e625aa/CLAUDE.md000077700000000000000000000000001527206113100174072AGENTS.mdustar00rootroot00000000000000linux-msm-qdl-6e625aa/CONTRIBUTING.md000066400000000000000000000042111527206113100170610ustar00rootroot00000000000000# Contributing Please submit any patches to the qdl (`master` branch) by using the GitHub pull request feature. Fork the repo, create a branch, do the work, rebase with upstream, and submit the pull request. The preferred coding style for this tool is [Linux kernel coding style](https://www.kernel.org/doc/html/v6.15/process/coding-style.html). Before creating a commit, please ensure that your changes adhere to the coding style by using the `meson compile check-cached -C build` target, for example: ```bash $ git status On branch improvements Changes to be committed: (use "git restore --staged ..." to unstage) modified: qdl.c modified: qdl.h $ meson compile check-cached -C build [0/1] Running external command check-cached (wrapped by meson to set env) Running checkpatch on staged changes... ERROR: trailing whitespace #28: FILE: qdl.h:32: +^IQDL_DEVICE_USB, $ total: 1 errors, 0 warnings, 0 checks, 27 lines checked NOTE: For some of the reported defects, checkpatch may be able to mechanically convert to the typical style using --fix or --fix-inplace. NOTE: Whitespace errors detected. You may wish to use scripts/cleanpatch or scripts/cleanfile Your patch has style problems, please review. ``` To verify a series of commits the same way the CI does (per-commit, in patch mode), use the `check-range` target. It runs checkpatch on every commit in `$CHECKPATCH_BASE..$CHECKPATCH_HEAD` (defaulting to `origin/master..HEAD`): ```bash meson compile check-range -C build ``` To restrict the range explicitly, set the environment variables before invoking meson: ```bash CHECKPATCH_BASE=origin/master CHECKPATCH_HEAD=HEAD \ meson compile check-range -C build ``` The full file-mode check (run against every tracked C/H/sh source) is also available: ```bash meson compile check -C build ``` Markdown sources are linted with [mdl](https://github.com/markdownlint/markdownlint). Install it via `sudo apt install markdownlint` (or `gem install mdl`) and run: ```bash meson compile markdown-lint -C build ``` The wrapper script is invoked from the GitHub Actions workflow as well, so a green local run reflects what CI will report. linux-msm-qdl-6e625aa/LICENSE000066400000000000000000000031341527206113100156400ustar00rootroot00000000000000/* * Copyright (c) 2016-2017, Linaro Ltd. * Copyright (c) 2016, Bjorn Andersson * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ linux-msm-qdl-6e625aa/README.md000066400000000000000000000250271527206113100161170ustar00rootroot00000000000000# Qualcomm Download [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Build on push](https://github.com/linux-msm/qdl/actions/workflows/build.yml/badge.svg)](https://github.com/linux-msm/qdl/actions/workflows/build.yml) [![Latest release](https://img.shields.io/github/v/release/linux-msm/qdl?sort=semver)](https://github.com/linux-msm/qdl/releases/latest) [![Debian package](https://img.shields.io/debian/v/qdl/unstable?logo=debian&label=Debian)](https://tracker.debian.org/pkg/qdl) [![Fedora package](https://img.shields.io/fedora/v/qdl?logo=fedora&label=Fedora)](https://packages.fedoraproject.org/pkgs/qdl/qdl/) [![Homebrew formula](https://img.shields.io/homebrew/v/qdl?logo=homebrew&label=Homebrew)](https://formulae.brew.sh/formula/qdl) This tool communicates with Qualcomm EDL USB devices (Vendor ID `05c6`) to upload a flash loader and use it to flash images. Any Qualcomm device that exposes a vendor-specific EDL interface is accepted; the Product ID (commonly `9008` for Firehose and `900e` for crash dumps) is not used for matching, as new devices keep appearing with new IDs. --- ## Install QDL is packaged for several distributions. The packaged version may lag behind the latest release; see [Repology](https://repology.org/project/qdl/versions) for an overview, or build from source as described in the [Build](#build) section below. ### Debian and Ubuntu ```bash sudo apt install qdl ``` Debian stable ships an older snapshot. The latest release is available from backports: ```bash sudo apt install -t trixie-backports qdl ``` ### Fedora and EPEL ```bash sudo dnf install qdl ``` On RHEL, CentOS Stream, AlmaLinux or Rocky Linux, enable the [EPEL](https://docs.fedoraproject.org/en-US/epel/) repository first. ### MacOS (Homebrew) QDL is also available in [Homebrew](https://formulae.brew.sh/formula/qdl). ```bash brew update brew install qdl ``` ## Build ### Linux ```bash sudo apt install libxml2-dev libusb-1.0-0-dev libzip-dev libcmocka-dev \ meson ninja-build help2man meson setup build meson compile -C build ``` ### MacOS For Homebrew users: ```bash brew install libxml2 libusb libzip cmocka meson ninja help2man meson setup build meson compile -C build ``` For MacPorts users: ```bash sudo port install libxml2 libusb libzip cmocka meson ninja help2man meson setup build meson compile -C build ``` ### Windows First, install the [MSYS2 environment](https://www.msys2.org/). Then, run the MSYS2 MinGW64 terminal (located at `\mingw64.exe`) and install additional packages needed for QDL compilation using the `pacman` tool: ```bash pacman -S base-devel --needed pacman -S git pacman -S help2man pacman -S mingw-w64-x86_64-gcc pacman -S mingw-w64-x86_64-meson pacman -S mingw-w64-x86_64-ninja pacman -S mingw-w64-x86_64-libusb pacman -S mingw-w64-x86_64-libxml2 pacman -S mingw-w64-x86_64-libzip pacman -S mingw-w64-x86_64-cmocka ``` Then use the `meson` tool to build QDL: ```bash meson setup build meson compile -C build ``` ### Build options Optional parts of QDL are controlled by meson feature options (`enabled`, `disabled` or `auto`): - `zip-container` (default `enabled`) - flashing directly from zip archives and the `create-zip` subcommand. Requires libzip; configuring fails if it is missing. To build a leaner QDL without it, explicitly disable the feature: ```bash meson setup build -Dzip-container=disabled ``` - `tests` (default `auto`) - the cmocka-based unit test suite. When cmocka is not installed the unit tests are silently skipped; pass `-Dtests=enabled` to make a missing cmocka a configure error. - `nbdkit` (default `auto`) - the nbdkit plugin described in [docs/nbd.md](docs/nbd.md). Requires the nbdkit development files. --- ## Use QDL ### EDL mode The device intended for flashing must be booted into **Emergency Download (EDL)** mode. EDL is a special boot mode available on Qualcomm-based devices that provides low-level access for firmware flashing and recovery. It bypasses the standard boot process, allowing operations such as flashing firmware even on unresponsive devices or those with locked bootloaders. Please consult your device's documentation for instructions on how to enter EDL mode. ### Device backends QDL can reach the EDL device through two backends, selected with `--backend`: - `usb` - talks to the device directly through libusb. On Windows this requires the device to be bound to the WinUSB driver (for example with Zadig) instead of the Qualcomm driver. - `qud` - Windows only. Talks to the COM port exposed by the official Qualcomm QDLoader 9008 driver, so no driver replacement is needed. - `auto` (default) - polls both backends and uses whichever reaches an EDL device first. To see the EDL devices visible through either backend, together with their serial numbers, run: ```bash qdl list ``` ### Flash device Run QDL with the `--help` option to view detailed usage information. Below is an example of how to invoke QDL to flash a FLAT build: ```bash qdl prog_firehose_ddr.elf rawprogram*.xml patch*.xml ``` If you have multiple boards connected to the host, provide the serial number of the board to flash through the `--serial` option: ```bash qdl --serial=0AA94EFD prog_firehose_ddr.elf rawprogram*.xml patch*.xml ``` Other options that commonly matter when flashing: - `--storage=` selects the target storage type passed to the programmer, and `--slot=N` the storage slot on targets with several devices of the same type. - `--include=DIR` adds a folder to search for the images referenced by the XML files, and `--allow-missing` skips images that cannot be found instead of aborting. - `--skipblock=sha256` asks the device for a SHA256 digest of each region about to be written and skips regions whose contents already match, which makes reflashing an unchanged build much faster. - `--skip-reset` leaves the device in EDL mode after flashing instead of sending the final reset. - `--finalize-provisioning` is required, together with a matching UFS provisioning XML, to perform irreversible UFS provisioning. A few maintenance commands do not need a programmer at all and only speak Sahara to the device: `qdl chipinfo` prints the chip identification the device reports, and `qdl reset` reboots a device stuck in EDL mode. ### Output A flashing run prints one line per region and ends with a summary of what it flashed, skipped, read and erased and the patches it applied: ```text flashed "xbl_a" successfully (3.2 MiB) flashed "efi" successfully (512.0 MiB, 39.4 MiB/s) skipped "dtb_a" (8.0 MiB, sha256 match) flashed 34 partitions, skipped 1 (2.1 GiB written in 54.2 s, 40.2 MiB/s) ``` - `--quiet` prints only the summary, warnings and errors. - `--debug` prints the full protocol log. Each line is timestamped and names the part of QDL it comes from, such as `SAHARA:` or `FIREHOSE:`. - Errors start with `ERR:` and warnings with `WARN:`. - `--color=auto|always|never` controls colored output. The default, `auto`, colors only output to a terminal and honours `NO_COLOR`. ### Flashing installer packages Builds shipped as an installer package (a zip archive or an unpacked `flashmap.json`) or described by a *contents.xml* file are flashed with the *flash* subcommand instead of listing the programmer and XML files by hand: ```bash qdl flash qdl flash flashmap.json qdl flash contents.xml ``` When a package or contents file covers several storage types, layouts or flavors, a `::specifier` suffix selects what to flash. The selector syntax and the `create-zip` subcommand that produces installer packages are described in [docs/installer-packages.md](docs/installer-packages.md). ### Flash simulation (dry run) Use the `--dry-run` option to run QDL without connecting to or flashing any device. This is useful for validating your XML descriptors and programmer arguments, or for generating VIP digest tables (see [docs/vip.md](docs/vip.md)): ```bash qdl --dry-run prog_firehose_ddr.elf rawprogram*.xml patch*.xml ``` --- ## Documentation The less common workflows are described in separate guides under [docs/](docs/): - [Installer packages and contents.xml](docs/installer-packages.md) - flashing zip packages, `flashmap.json` and *contents.xml* builds, the storage, layout and flavor selectors, and creating packages with `create-zip`. - [Reading and writing raw binaries](docs/raw-io.md) - `read`, `write`, `erase` and `sha256` on physical partitions, sector ranges and named GPT partitions. - [Validated Image Programming](docs/vip.md) - generating and signing digest tables for Secure Boot targets and validating them without hardware. - [Multi-programmer targets](docs/multi-programmer.md) - targets that request several Sahara images: command-line image lists, Sahara configuration XML files and programmer archives. - [Collect crash dump](docs/ramdump.md) - collecting memory segments from a crashed target with `qdl ramdump`. - [Sahara kickstart](docs/kickstart.md) - loading firmware into flashless-boot devices such as the Cloud AI 100 with `qdl ks`. - [Flashing from WSL2](docs/wsl2.md) - forwarding the EDL device into WSL2 with usbipd-win and re-attaching it after re-enumeration. - [nbdkit plugin](docs/nbd.md) - exposing a physical partition as a block device on the host. --- ## Run tests The test suite is run with the `meson` tool: ```bash meson test -C build ``` Tests are grouped into suites, selectable with `--suite`: - `unit` - cmocka programs covering the XML, JSON, contents and archive parsers. Only built when cmocka was found at configure time (see [Build options](#build-options)). - `integration` - scripts that drive the built `qdl` binary without a device, for example VIP table and Sahara archive generation. - `hil` and `hil-vip` - hardware-in-the-loop steps that flash and read back an attached EDL device. They are skipped unless `QDL_HIL_BUILD`, `QDL_HIL_STORAGE` and friends are set; see the comments at the top of [tests/test_hil.sh](tests/test_hil.sh) for the full environment. A plain `meson test` runs the unit and integration suites. To run only one suite, for example the unit tests: ```bash meson test -C build --suite unit ``` --- ## Generate man pages Manpages can be generated using `manpages` target: ```bash meson compile manpages -C build ``` --- ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for the coding style, the checkpatch and markdown-lint targets, and how to submit pull requests. --- ## License This tool is licensed under the BSD 3-Clause license. Check out [LICENSE](LICENSE) for more details. linux-msm-qdl-6e625aa/RELEASING.md000066400000000000000000000172411527206113100164720ustar00rootroot00000000000000# Releasing QDL This document describes how a new QDL release is cut, from preparing the changelog to publishing the GitHub release with prebuilt artifacts. Releases are versioned `vMAJOR.MINOR` (for example `v2.8`). Release candidates append an `-rcN` suffix (`v2.8-rc1`, `v2.8-rc2`, ...). The process is, in short: - [1. Populate the changelog](#1-populate-the-changelog) - [2. Open the changelog PR and tag a release candidate](#2-open-the-changelog-pr-and-tag-a-release-candidate) - [3. Iterate on release candidates](#3-iterate-on-release-candidates) - [4. Merge the changelog PR and apply the release tag](#4-merge-the-changelog-pr-and-apply-the-release-tag) - [5. Create the GitHub release](#5-create-the-github-release) - [Stable branches](#stable-branches) Throughout, export the version once so the commands below can be copied verbatim: ```bash VERSION=v2.8 ``` ## 1. Populate the changelog `CHANGELOG.md` mirrors the GitHub releases: one section per version, listing the pull requests merged since the previous release. Add a new section at the top for the upcoming version. GitHub can produce the raw list of merged PRs for the range, which is the same content used in the existing entries: ```bash # Everything merged since the previous release: gh api repos/linux-msm/qdl/releases/generate-notes \ -f tag_name="${VERSION}" -f target_commitish=master \ -f previous_tag_name="$(git describe --tags --abbrev=0 master)" \ --jq .body ``` Format the output to match the existing entries (PR references as `#NNN` links, a `### What's Changed` and `### New Contributors` subsection, and a `**Full Changelog**` compare link), and prepend it to `CHANGELOG.md` under a new `## [vX.Y](...) - YYYY-MM-DD` heading. Once the changelog draft is ready, ask people to start testing the current `master` so issues are found before the tag is cut. ## 2. Open the changelog PR and tag a release candidate Open a pull request with the changelog update. With that PR up, tag the latest `master` as the first release candidate so testers have a stable reference point to build from: ```bash git fetch upstream git tag "${VERSION}-rc1" upstream/master git push upstream "${VERSION}-rc1" ``` Ask testers to exercise the hardware-in-the-loop (HIL) suite against real hardware. The suite is skipped by default and only runs when pointed at an attached EDL device: ```bash meson setup build QDL_HIL_BUILD=/path/to/build-images \ QDL_HIL_STORAGE= \ meson test -C build --suite hil --print-errorlogs ``` The release must be validated across all four supported host/driver combinations: - Windows + QUD driver: the official Qualcomm QDLoader 9008 driver (`qdl --backend=qud ...`). - Windows + WinUSB driver / libusb (`qdl --backend=usb ...`). - Linux. - macOS. In addition, validate VIP (Validated Image Programming) mode: generate the digest tables with `--create-digests`, sign them, and flash a board with `--vip-table-path` to confirm secure-boot flashing still works end to end. See the VIP section in [README.md](README.md) for the full workflow. ## 3. Iterate on release candidates Collect the testing results. If a regression is found, fix it on `master` and cut the next candidate (`-rc2`, `-rc3`, ...): ```bash git fetch upstream git tag "${VERSION}-rc2" upstream/master git push upstream "${VERSION}-rc2" ``` Repeat until a candidate passes all four configurations (and VIP validation) cleanly. That commit becomes the stable release. ## 4. Merge the changelog PR and apply the release tag Merge the changelog PR. Then tag the resulting `master` commit with the final, immutable release tag. Note on tag type: the history is mixed. `v2.4` is an annotated tag while `v2.5`, `v2.6` and `v2.7` are lightweight tags (that is what GitHub's "create release" UI produces). Prefer an annotated tag for releases: it records the tagger, date and a message, and is what `git describe` expects. ```bash git fetch upstream git tag -a "${VERSION}" -m "qdl ${VERSION}" upstream/master git push upstream "${VERSION}" ``` A tag is a ref, not a branch. `git push upstream "${VERSION}"` publishes the tag itself; it does not push to the `master` branch. Once pushed, treat the tag as immutable: never move or delete it. (If matching the recent lightweight-tag convention is preferred instead, drop the `-a`/`-m` flags: `git tag "${VERSION}" upstream/master`.) ## 5. Create the GitHub release The `Buildtest` workflow already builds and uploads per-platform binaries for every push. Reuse those artifacts for the release rather than rebuilding. Download the artifacts from the green `Buildtest` run for the tagged commit and bundle each platform into its own archive: ```bash # Find the successful Buildtest run that built the tagged commit. Filtering # with --commit happens server-side, so it is not limited to the most recent # runs (a plain `gh run list` only fetches the latest 20). SHA=$(git rev-parse "${VERSION}^{commit}") RUN_ID=$(gh run list --workflow build.yml --commit "${SHA}" --status success \ --json databaseId --jq '.[0].databaseId') # Pull every platform artifact into dist//... rm -rf dist && gh run download "${RUN_ID}" -D dist # Zip each platform directory into a single release asset ( cd dist && for d in */; do name="${d%/}" zip -r "${name}-${VERSION}.zip" "${d}" done ) ``` Extract this release's notes straight from `CHANGELOG.md` (the block between this version's heading and the next `## [` heading) and create the release, attaching the zipped artifacts: ```bash NOTES=$(awk -v v="${VERSION}" ' $0 ~ ("^## \\[" v "\\]") { found=1; next } /^## \[/ && found { exit } found { print } ' CHANGELOG.md | sed '1{/^$/d}') echo "Release notes preview:" echo "---" echo "${NOTES}" echo "---" read -r -p "Create GitHub release ${VERSION}? [y/N] " reply [ "${reply}" = "y" ] || { echo "GitHub release skipped"; exit 0; } gh release create "${VERSION}" dist/*.zip \ --title "${VERSION}" \ --notes "${NOTES}" echo "GitHub release created: $(gh release view "${VERSION}" --json url -q .url)" ``` The release name matches the existing convention (the tag itself, for example `v2.8`). After publishing, sanity-check the release page: confirm the notes rendered correctly and that every platform archive is attached. ## Stable branches After a release, critical fixes reach users through kernel-style stable branches named `qdl-X.Y.y` (for `v2.8`: `qdl-2.8.y`). A point release (`v2.8.1`, `v2.8.2`, ...) is then cut from the stable branch, so urgent fixes never have to wait for - or ship - whatever is in flight on `master`. ### Cut the branch immediately after the release Right after the normal release flow tags the release on `master` (step 4), create the stable branch from the tag: ```bash git fetch upstream git branch "qdl-${VERSION#v}.y" "${VERSION}" git push upstream "qdl-${VERSION#v}.y" ``` ### Maintain the branch (backports) When a bug affecting the release gets fixed, the fix lands on `master` first, through the normal PR flow. Then backport it, with `-x` so the commit message records which `master` commit it was cherry-picked from: ```bash git checkout "qdl-${VERSION#v}.y" git cherry-pick -x ``` Open a pull request targeting the stable branch. CI runs on it unchanged, since the workflows trigger on any pull request regardless of its base branch. ### Cut a point release when warranted When the branch has accumulated fixes worth shipping - or a security fix forces one: - Update `CHANGELOG.md` on the stable branch with a section for the point release (for example `v2.8.1`). - Tag and publish from the stable branch, following the same flow as a normal release (steps 2-5) with `VERSION=v2.8.1` and the stable branch taking the place of `master`. linux-msm-qdl-6e625aa/commitlint.config.mjs000066400000000000000000000031201527206113100207640ustar00rootroot00000000000000// Commitlint config tuned for Linux kernel-style commit messages. // // Expected format: // subsystem: short description (max 72 chars) // // Body text wrapped at 75 chars. // // Signed-off-by: Name export default { rules: { // Subject line limit (Linux kernel style uses up to 72) 'header-max-length': [2, 'always', 72], // Body and footer lines wrapped at 75 chars. // Footer limit is relaxed to 200 to accommodate long Signed-off-by // addresses without false positives. 'body-max-line-length': [2, 'always', 75], 'footer-max-line-length': [2, 'always', 200], // Require a blank line between subject and body, and before footers 'body-leading-blank': [2, 'always'], 'footer-leading-blank': [2, 'always'], // Disable Conventional Commits type rules - this project uses // Linux kernel–style "subsystem: description" subjects instead. 'type-enum': [0], 'type-case': [0], 'type-empty': [0], 'scope-empty': [0], 'subject-case': [0], }, // Teach the parser which trailers terminate the body and start the // footer. Without this, kernel-style trailers like `Fixes:` are // classified as body lines and trip body-max-line-length, while the // relaxed footer-max-line-length (200) never gets a chance to apply. parserPreset: { parserOpts: { noteKeywords: [ 'BREAKING CHANGE', 'BREAKING-CHANGE', 'Fixes', 'Closes', 'Signed-off-by', 'Co-authored-by', 'Reviewed-by', 'Tested-by', 'Acked-by', 'Reported-by', 'Suggested-by', ], }, }, }; linux-msm-qdl-6e625aa/docs/000077500000000000000000000000001527206113100155625ustar00rootroot00000000000000linux-msm-qdl-6e625aa/docs/installer-packages.md000066400000000000000000000043171527206113100216620ustar00rootroot00000000000000# Installer packages and contents.xml ## Flashing installer packages If you have an installer package instead of individual binaries and XML definitions, you can flash this using the *flash* subcommand: ```bash qdl flash ``` If the *installer package* is unpacked it can be installed as: ```bash qdl flash flashmap.json ``` These can of course be combined with e.g. *--serial*. A subset of the installer package can be selected for installation by appending a **::storage1[,storage2...]** suffix to the file name. If a flashmap contains multiple layouts, select the desired layout by appending **::layout-name**. The layout selector can be combined with storage filters using **::layout-name/storage1[,storage2...]**, for example: ```bash qdl flash installer.zip::layout1/ufs ``` ## Flashing contents.xml QDL also supports flashing builds described by *contents.xml* files: ```bash qdl flash contents.xml ``` As the contents XML can describe the content for multiple storage types and multiple flavors, it might be necessary to select which content to flash. This is done by appending the **::specifier1,specifier2...** suffix to the file name. The specifier is matched against **storage types** and **flavors**. At most one resolved specifier per storage is allowed, and only the selected parts are flashed. As an example: ```bash qdl flash contents.xml::ufs,safe_rtos ``` will flash the UFS storage with the only applicable flavor, and will flash *safe_rtos* onto the spinor. ## Creating installer packages QDL can also create installer packages from builds described by *contents.xml* files. The generated zip contains a `flashmap.json`, the selected programmer images, and the referenced rawprogram, patch, and image files: ```bash qdl create-zip installer.zip contents.xml ``` When the contents XML describes multiple storage or flavor combinations, select the desired content with the same `::specifier1,specifier2...` suffix used for flashing contents XML files: ```bash qdl create-zip installer-ufs.zip contents.xml::ufs ``` The resulting archive can be flashed later with `qdl flash installer.zip`. Creating and flashing zip archives requires QDL to be built with libzip support, which is enabled by default. linux-msm-qdl-6e625aa/docs/kickstart.md000066400000000000000000000030641527206113100201060ustar00rootroot00000000000000# Sahara kickstart for flashless-boot devices (qdl ks) The `qdl ks` ("kickstart") subcommand uses the Sahara protocol to load images from the host to the device. It targets *flashless boot* devices such as the Qualcomm Cloud AI 100, which fetch their runtime firmware from the host on every boot rather than storing it on-device. Unlike normal flashing, kickstart stops once Sahara is done: no Firehose programmer is uploaded and nothing is written to storage. One argument is required: `-s id:path` registers an image mapping. It may be specified more than once, one mapping per Sahara image id the device may request. By default the device is found through the same backends the other subcommands use, so `--backend` and `--serial` select it just as they do when flashing: ```bash qdl ks -s 13:prog_firehose_ddr.elf ``` Devices that a kernel driver exposes as a node instead - such as the MHI Sahara endpoints - are addressed with `-p`, which is driven with plain open/read/write operations rather than through a backend: ```bash qdl ks -p /dev/mhi0_QAIC_SAHARA \ -s 1:/opt/qti-aic/firmware/fw1.bin \ -s 2:/opt/qti-aic/firmware/fw2.bin ``` Because `-p` names the transport outright, it cannot be combined with `--serial` or `--backend`. The mapped files do not need to exist at invocation time. If `qdl ks` cannot open a requested file, the device decides the next action. This makes it possible to wire `qdl ks` into a single udev rule that covers multiple device configurations (for example, an optional DDR training image that is only present on some setups). linux-msm-qdl-6e625aa/docs/multi-programmer.md000066400000000000000000000042051527206113100214100ustar00rootroot00000000000000# Multi-programmer targets On some targets multiple files need to be loaded in order to reach the Firehose programmer; these targets will request multiple images over Sahara. Three mechanisms for providing these images are provided: ## Command line argument The *programmer* argument allows specifying a comma-separated list of colon-separated "id" and "filename" pairs. Each filename should refer to the Sahara image of the specified Sahara image id. ```bash qdl 13:prog_firehose_ddr.elf,42:the-answer rawprogram.xml ``` ## Sahara configuration XML file Flattened METAs include the various images that need to be loaded to enter Firehose mode, as well as a sahara_config XML file, which defines the Sahara image id for each of these images. If the specified device programmer is determined to be a Sahara configuration XML file, it will be parsed and the referenced files will be loaded and serviced to the device upon request. ```bash qdl sahara_programmer.xml rawprogram.xml ``` ## Programmer archive Directly providing a list of ids and filenames is cumbersome and error-prone, so QDL accepts a "*programmer archive*". This allows the user to use the tool in the same fashion as was done for single-programmer targets. The *programmer archive* contains the Sahara images to be loaded, identified by the Sahara *id* needed by the target. QDL can create such an archive from the same programmer descriptions accepted for flashing. To create an archive from a command-line Sahara image list: ```bash qdl create-sahara-archive programmer.bin 13:prog_firehose_ddr.elf,42:the-answer ``` To create an archive from a Sahara configuration XML file: ```bash qdl create-sahara-archive programmer.bin sahara_programmer.xml ``` To create an archive from a contents XML file: ```bash qdl create-sahara-archive programmer.bin contents.xml ``` When the contents XML describes multiple storage or flavor combinations, select exactly one with the same `::specifier` syntax used by `flash`: ```bash qdl create-sahara-archive programmer.bin contents.xml::ufs ``` *programmer.bin* can now be passed to QDL and the included images will be served in order to reach Firehose mode. linux-msm-qdl-6e625aa/docs/nbd.md000066400000000000000000000055321527206113100166540ustar00rootroot00000000000000# nbdkit plugin In addition to the `qdl` programmer, an [nbdkit](https://gitlab.com/nbdkit/nbdkit) plugin can be built. It uploads the firehose programmer and exposes a physical partition (LUN) as a block device on the host, so its partition table can be edited and its partitions mounted with ordinary tools. ## Building the plugin The plugin is built by meson only when the `nbdkit` development files are present. Install the runtime and the development package, then reconfigure: - Debian/Ubuntu: `sudo apt install nbdkit nbdkit-dev` - Fedora: `sudo dnf install nbdkit nbdkit-devel` ```bash meson setup --reconfigure build meson compile -C build ``` `meson setup` reports `Run-time dependency nbdkit found: YES` and produces `build/nbdkit-qdl-plugin.so`. Pass `-Dnbdkit=enabled` to make the plugin mandatory (the build fails if nbdkit is missing) instead of the default auto-detection. ## Plugin parameters - `programmer` - firehose programmer to upload (required) - `lun` - physical partition (LUN) to expose, defaults to `1` - `storage` - `ufs` or `emmc`, selects the firehose configuration - `debug` - set to `1` for verbose qdl logging ## Serving a LUN as a block device The device must be in EDL mode, exactly as for a normal `qdl` run. **1. Start nbdkit.** In one terminal, launch the server. `-f` keeps it in the foreground and `-v` prints a debug log: ```bash sudo nbdkit -fv ./build/nbdkit-qdl-plugin.so \ programmer=prog_firehose_ddr.elf lun=0 ``` nbdkit now listens on TCP port 10809 (`nbd://localhost`). It does not touch the device yet: the programmer is uploaded and firehose is configured lazily, the first time a client connects, and kept configured until nbdkit exits. **2. Confirm the export.** In a second terminal, query it. This triggers the one-time device setup and prints the LUN's size, so it is the quickest check that the plugin can talk to the board: ```bash nbdinfo nbd://localhost ``` **3. Attach a kernel block device.** Load the `nbd` module and bind the export to a `/dev/nbdN` node with `nbd-client`. `-N default` selects the plugin's default export, `10809` is the port: ```bash sudo modprobe nbd sudo nbd-client -N default localhost 10809 /dev/nbd0 ``` `/dev/nbd0` now mirrors the LUN. `nbd-client` prints the negotiated size on connect. **4. Read the LUN.** Query its size (in bytes) and read from it with any standard tool: ```bash sudo blockdev --getsize64 /dev/nbd0 sudo dd if=/dev/nbd0 of=/tmp/lun0.head bs=1M count=1 ``` You can partition, mount, `dd`, or image `/dev/nbd0` like any other disk. **5. Detach when done**, then stop nbdkit (Ctrl-C in its terminal): ```bash sudo nbd-client -d /dev/nbd0 ``` > [!WARNING] > Writing to `/dev/nbd0` writes straight to the device's flash. Firehose > reads are also slow - every request is a separate firehose command - so > large transfers take a while; a bigger block size (`bs=8M`) helps. linux-msm-qdl-6e625aa/docs/ramdump.md000066400000000000000000000011411527206113100175460ustar00rootroot00000000000000# Collect crash dump When a Qualcomm target crashes or is forced into crash dump mode, the bootloader re-enumerates the device over USB with Product ID `900e` and offers memory segments for collection via the Sahara protocol. A kernel crash can be triggered on the target with: ```bash echo c > /proc/sysrq-trigger ``` Use `qdl ramdump` on the host to collect the dump: ```bash qdl ramdump -o ./ramdump ``` Each offered memory segment is written to a separate file under `./ramdump`. To collect only specific segments, pass a comma-separated filter: ```bash qdl ramdump -o ./ramdump OCIMEM,CODERAM ``` linux-msm-qdl-6e625aa/docs/raw-io.md000066400000000000000000000026121527206113100173030ustar00rootroot00000000000000# Reading and writing raw binaries In addition to flashing builds using their XML-based descriptions, QDL supports reading and writing binaries directly. ```bash qdl prog_firehose_ddr.elf [read | write] [address specifier] ... qdl prog_firehose_ddr.elf [erase | sha256] [address specifier]... ``` `erase` wipes the addressed region and `sha256` prints the SHA256 digest the device computes over it, which is a quick way to verify a write. Multiple read, write, erase and sha256 commands can be specified at once. The ***address specifier*** can take the forms: - N - single number, specifies the physical partition number N, starting at sector 0. To read data, the number of sectors must be specified explicitly using the N/S+L form. - N/S - two numbers, specifies the physical partition number N and the start sector S. To read data, the number of sectors must be specified explicitly using the N/S+L form. - N/S+L - three numbers, specifies the physical partition number N, the start sector S and the number of sectors L, that ***binary*** should be written to, or which should be read into ***binary***. - partition name - a string, will match against partition names across the GPT partition tables on all physical partitions. - N/partition_name - a number followed by a string, will match against partition names of the GPT partition table in the specified physical partition N. linux-msm-qdl-6e625aa/docs/vip.md000066400000000000000000000057761527206113100167210ustar00rootroot00000000000000# Validated Image Programming (VIP) QDL supports **Validated Image Programming (VIP)** mode, which is activated when Secure Boot is enabled on the target. VIP controls which packets are allowed to be issued to the target by hashing all received data and comparing each resulting digest against the next entry in a pre-loaded digest table. If the digest matches, the packet is accepted; otherwise, the packet is rejected, and the target halts. To use VIP programming, a digest table must be generated prior to flashing the device. To generate a table of digests, run QDL with the `--create-digests` option, providing a path to store the VIP tables. Note that `--create-digests` implicitly enables dry-run mode, so no device connection is required: ```bash mkdir vip qdl --create-digests=./vip prog_firehose_ddr.elf rawprogram*.xml patch*.xml ``` As a result, three types of files are generated: - `DIGEST_TABLE.bin` - contains the SHA256 table of digests for all Firehose packets to be sent to the target. It is an intermediary table and is used only for the subsequent generation of `DigestsToSign.bin` and `ChainedTableOfDigests.bin` files, and is not used directly by QDL for VIP programming. - `DigestsToSign.bin` - first 53 digests + SHA256 hash of `ChainedTableOfDigests0.bin`. This file must be converted to MBN format and then signed with sectools: ```bash sectools mbn-tool generate --data DigestsToSign.bin --mbn-version 6 --outfile DigestsToSign.bin.mbn sectools secure-image --sign DigestsToSign.bin.mbn --image-id=VIP ``` Please check the security profile for your SoC to determine which version of the MBN format should be used. - `ChainedTableOfDigests.bin` - contains the remaining digests, split across multiple files of up to 255 digests each. Non-final files have the SHA256 hash of the next chained table appended. The final file has a trailing zero byte appended to ensure its size is not a multiple of the sector size. To flash a board using VIP mode, provide the path where the previously generated and signed tables are stored using the `--vip-table-path` option: ```bash qdl --vip-table-path=./vip prog_firehose_ddr.elf rawprogram*.xml patch*.xml ``` Note that `--vip-table-path` and `--create-digests` are mutually exclusive. ## Validating VIP tables without hardware Before flashing a real device it is possible to verify that the signed digest tables match the data that will be sent, using `--dry-run` together with `--vip-table-path`: ```bash qdl --dry-run --vip-table-path=./vip prog_firehose_ddr.elf rawprogram*.xml patch*.xml ``` QDL will simulate the full Firehose session, compute SHA256 over every packet it would send, and compare each hash against the corresponding entry in the loaded digest tables. Any mismatch is reported to stderr. All mismatches are printed before the run exits so that every problem is visible at once. This catches table/data mismatches early -- before committing to a real flash -- and is useful both as a local sanity check and as a step in CI pipelines. linux-msm-qdl-6e625aa/docs/wsl2.md000066400000000000000000000042011527206113100167700ustar00rootroot00000000000000# Flashing from WSL2 (usbipd-win) > [!WARNING] > This is unreliable with current usbipd-win. See: - - - On Windows, QDL can run inside a WSL2 distribution, but WSL2 does not see USB devices by default - they must be forwarded from the Windows host with [usbipd-win](https://github.com/dorssel/usbipd-win). Install it on the Windows side (`winget install usbipd`), then forward the EDL device into WSL. EDL devices enumerate under Vendor ID `05c6`, most commonly with Product ID `9008` (Firehose) or `900e` (crash dump). From a Windows terminal, list the connected devices and note the **BUSID** of the EDL device: ```powershell usbipd list ``` Each device must be *bound* once before it can be attached. Binding requires an **elevated (Administrator)** PowerShell, but it persists across reboots, so this is a one-time step per device: ```powershell usbipd bind --busid ``` Attaching the device to WSL does **not** require Administrator and can be run from inside WSL by calling the Windows binary: ```bash usbipd.exe attach --wsl --busid ``` Once attached, the device appears in WSL (check with `lsusb` for a `05c6:` device) and QDL can flash it as usual. ## Re-attaching after EDL re-enumeration The EDL device is a USB gadget that only exists while the board is in EDL mode. Whenever the board re-enters EDL - including after QDL finishes and the target reboots - the USB device is torn down and re-created. The `bind` persists, but the **attach is dropped**, and the host may assign a **different BUSID** than before. A QDL run that reports no device found is almost always this: the device simply needs to be re-attached. After each entry into EDL, re-detect the BUSID and attach again. This can be scripted from WSL so the BUSID does not have to be looked up by hand: ```bash BUSID=$(usbipd.exe list | grep -i '05c6:' | awk '{print $1}' | tr -d '\r') usbipd.exe attach --wsl --busid "$BUSID" ``` If you flash repeatedly, run this re-attach step before every `qdl` invocation. linux-msm-qdl-6e625aa/include/000077500000000000000000000000001527206113100162555ustar00rootroot00000000000000linux-msm-qdl-6e625aa/include/qdl.h000066400000000000000000000171641527206113100172170ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __QDL_H__ #define __QDL_H__ #ifdef _WIN32 #include #else #include #endif #include #include "patch.h" #include "program.h" #include "read.h" #include #include "ux.h" #include "vip.h" #define container_of(ptr, typecast, member) ({ \ void *_ptr = (void *)(ptr); \ ((typeof(typecast) *)(_ptr - offsetof(typecast, member))); }) #define MIN(x, y) ({ \ __typeof__(x) _x = (x); \ __typeof__(y) _y = (y); \ _x < _y ? _x : _y; \ }) #define ROUND_UP(x, a) ({ \ __typeof__(x) _x = (x); \ __typeof__(a) _a = (a); \ (_x + _a - 1) & ~(_a - 1); \ }) #define __unused __attribute__((__unused__)) #define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) #define ALIGN_UP(p, size) ({ \ __typeof__(size) _mask = (size) - 1; \ (__typeof__(p))(((uintptr_t)(p) + _mask) & ~_mask); \ }) #define MAPPING_SZ 128 #define SAHARA_ID_EHOSTDL_IMG 13 enum QDL_DEVICE_TYPE { QDL_DEVICE_USB, QDL_DEVICE_SIM, QDL_DEVICE_QUD, /* * Meta-backend: defers transport selection to the wait loop inside * auto_open(), which polls libusb and (on Windows) the QUD SetupAPI * enumeration each tick and binds whichever first reaches an EDL * device. Resolves the UX hazard of an upfront probe timeout where * the user plugs in the cable just after the grace window expires. */ QDL_DEVICE_AUTO, }; enum qdl_storage_type { QDL_STORAGE_UNKNOWN, QDL_STORAGE_EMMC, QDL_STORAGE_NAND, QDL_STORAGE_UFS, QDL_STORAGE_NVME, QDL_STORAGE_SPINOR, }; enum qdl_skipblock_mode { QDL_SKIPBLOCK_NONE, QDL_SKIPBLOCK_SHA256, }; struct qdl_device { enum QDL_DEVICE_TYPE dev_type; int fd; size_t max_payload_size; size_t sector_size; enum qdl_storage_type current_storage_type; enum qdl_skipblock_mode skipblock_mode; unsigned int slot; int (*open)(struct qdl_device *qdl, const char *serial); int (*read)(struct qdl_device *qdl, void *buf, size_t len, unsigned int timeout); int (*write)(struct qdl_device *qdl, const void *buf, size_t nbytes, unsigned int timeout); void (*close)(struct qdl_device *qdl); void (*set_out_chunk_size)(struct qdl_device *qdl, long size); void (*set_vip_transfer)(struct qdl_device *qdl, const char *signed_table, const char *chained_table); struct vip_transfer_data vip_data; /* * Pushback buffer for stream-oriented transports (Windows COM via the * QDLoader driver, virtio-console, ...). When a single read crosses a * Firehose message boundary - typically because the binary payload of * a rawmode response trails the XML envelope in the same read - the * leftover bytes are stashed here and qdl_read() returns them before * pulling more data from the transport. */ char *pending_buf; size_t pending_len; size_t pending_off; }; struct sahara_image { char *name; void *ptr; size_t len; }; struct qdl_zip; struct libusb_device_handle; struct qdl_device *qdl_init(enum QDL_DEVICE_TYPE type); void qdl_deinit(struct qdl_device *qdl); int qdl_open(struct qdl_device *qdl, const char *serial); void qdl_close(struct qdl_device *qdl); int qdl_read(struct qdl_device *qdl, void *buf, size_t len, unsigned int timeout); int qdl_push_back(struct qdl_device *qdl, const void *buf, size_t len); int qdl_write(struct qdl_device *qdl, const void *buf, size_t len, unsigned int timeout); void qdl_set_out_chunk_size(struct qdl_device *qdl, long size); int qdl_vip_transfer_enable(struct qdl_device *qdl, const char *vip_table_path); struct qdl_device *usb_init(void); struct qdl_device *sim_init(void); struct qdl_device *qud_init(void); struct qdl_device *auto_init(void); /* * usb_open_once() - single libusb scan-and-open pass; shared between * the --backend usb wait loop in usb.c and the unified auto_open() loop. * Returns 0 on success (and emits the "Flashing/Collecting device" UX * line), -ENODEV when no EDL device is visible, -EBUSY when one is * visible but cannot be opened (typically: the Qualcomm QDLoader 9008 * driver has claimed it), -EIO on a libusb failure. @visible_out, if * non-NULL, receives the count of EDL devices seen on this pass. * * qud_probe_present() returns the number of Qualcomm COM ports the QUD * backend enumerated via SetupAPI; 0 on non-Windows hosts. */ int usb_open_once(struct qdl_device *qdl, const char *serial, int *visible_out); int qud_probe_present(void); /* * EDL device identity, shared by all transport backends: Qualcomm's * vendor id. Product ids are deliberately not filtered - EDL, crash-mode * and ramdump devices enumerate with a growing set of ids that no * allowlist keeps up with. The libusb backend requires a matching * vendor-specific interface (see usb_match_edl_interface() in usb.c) * before a device is opened; on Windows the QDLoader driver binding * performs the equivalent selection before the QUD backend sees the * device. */ #define QUALCOMM_VID 0x05c6 static inline bool qdl_is_edl_device(unsigned int vid, unsigned int pid __unused) { return vid == QUALCOMM_VID; } struct qdl_device_desc { int vid; int pid; char serial[16]; }; struct qdl_device_desc *usb_list(unsigned int *devices_found); /* * QUD-side counterpart to qdl_device_desc. Serial here is the iSerial as * Windows stored it in the device-instance ID (no fixed length guarantee * across OEMs), and path is the kernel-driver-exposed handle the QUD * backend will open (e.g. "\\\\.\\COM5"). */ struct qud_device_desc { unsigned int pid; char serial[64]; char path[64]; }; struct qud_device_desc *qud_list(unsigned int *devices_found); struct ufs_provisioning; int firehose_run(struct qdl_device *qdl, struct list_head *ops); int firehose_provision(struct qdl_device *qdl, struct ufs_provisioning *ufs, bool skip_reset); int firehose_read_buf(struct qdl_device *qdl, struct firehose_op *read_op, void *out_buf, size_t out_size); /* Block-level entry points used by the nbdkit plugin */ int firehose_open(struct qdl_device *qdl, enum qdl_storage_type storage); int firehose_reset(struct qdl_device *qdl); int firehose_getsize(struct qdl_device *qdl, int lun, size_t *sector_size, size_t *num_sectors); ssize_t firehose_pread(struct qdl_device *qdl, int lun, size_t sector_offset, void *buf, size_t sector_size, size_t num_sectors); ssize_t firehose_pwrite(struct qdl_device *qdl, int lun, size_t sector_offset, const void *buf, size_t sector_size, size_t num_sectors); int sahara_run(struct qdl_device *qdl, const struct sahara_image *images, const char *ramdump_path, const char *ramdump_filter); int sahara_chipinfo(struct qdl_device *qdl); int sahara_device_reset(struct qdl_device *qdl); int load_sahara_image(struct qdl_zip *zip, const char *filename, struct sahara_image *image); void sahara_images_free(struct sahara_image *images, size_t count); void print_hex_dump(const char *prefix, const void *buf, size_t len); unsigned int attr_as_unsigned(xmlNode *node, const char *attr, int *errors); const char *attr_as_string(xmlNode *node, const char *attr, int *errors); bool attr_as_bool(xmlNode *node, const char *attr, int *errors); int parse_storage_address(const char *address, int *physical_partition, unsigned int *start_sector, unsigned int *num_sectors, char **gpt_partition); int decode_backend(const char *name, enum QDL_DEVICE_TYPE *out); char *qdl_split_specifier(const char *param, char **specifier); enum qdl_storage_type decode_storage_type(const char *storage); const char *encode_storage_type(enum qdl_storage_type storage); int decode_sahara_config(struct sahara_image *blob, struct sahara_image *images, struct contents_filter *contents_filter); extern bool qdl_debug; #endif linux-msm-qdl-6e625aa/include/version.h.in000066400000000000000000000000341527206113100205150ustar00rootroot00000000000000#define VERSION "@version@" linux-msm-qdl-6e625aa/meson.build000066400000000000000000000143731527206113100170040ustar00rootroot00000000000000project('qdl', 'c', default_options : ['warning_level=2', 'optimization=2'], license: 'BSD-3-Clause', meson_version: '>=0.56.0' ) # Use 64-bit file offsets everywhere. This must be consistent across every # translation unit: off_t appears in shared types (e.g. struct firehose_op) # and in the qdl_file_seek() ABI, so a per-file definition would give # mismatched struct layouts on 32-bit builds. add_project_arguments('-D_FILE_OFFSET_BITS=64', language : 'c') # Determine version env_v = get_option('VERSION') if env_v != '' ver = env_v else git = find_program('git', required: false) git_v = 'unknown-version' if git.found() git_check_ver = run_command(git, 'describe', '--dirty', '--always', '--tags', check: false).stdout().strip() git_v = git_check_ver != '' ? git_check_ver : git_v endif ver = git_v endif # Dependencies libusb_dep = dependency('libusb-1.0') libxml_dep = dependency('libxml-2.0') libzip_dep = dependency('libzip', required: get_option('zip-container')) help2man = find_program('help2man', required: false) cmocka_dep = dependency('cmocka', required: get_option('tests')) # Public API headers live in include/, implementation headers in src/. # '.' adds the top-level build dir, where configure_file generates # version.h, so targets in subdirs (e.g. tests/) resolve it too. inc = include_directories('include', 'src', '.') # Windows-specific link flags ws2_dep = [] setupapi_dep = [] if host_machine.system() == 'windows' ws2_dep = meson.get_compiler('c').find_library('ws2_32', required : false) # SetupAPI / cfgmgr32 are needed by the QUD backend to enumerate Qualcomm # COM ports exposed by the official QDLoader 9008 driver. setupapi_dep = [ meson.get_compiler('c').find_library('setupapi', required : false), meson.get_compiler('c').find_library('cfgmgr32', required : false), ] endif common_dep = [libusb_dep, libxml_dep, ws2_dep, setupapi_dep] compile_view_deps = [libusb_dep, libxml_dep] # Zip container support is optional; the src subdirectory swaps the # libzip-backed sources for no-zip stubs when it is disabled. if libzip_dep.found() common_dep += libzip_dep compile_view_deps += libzip_dep endif # Compile-only view of common_dep (includes + cflags, no link args). # Used on executables so their own sources see the headers, while link # args propagate exactly once through the static library below, avoiding # duplicate "-l..." entries on the link line. common_compile_dep = [] foreach d : compile_view_deps common_compile_dep += d.partial_dependency(compile_args: true, includes: true) endforeach # --- version header generation --- conf = configuration_data() conf.set('version', ver) version_h = configure_file( input: 'include/version.h.in', output: 'version.h', configuration: conf ) # Source lists (common_sources, qdl_sources and the per-test source # vars) are declared next to the sources. subdir('src') shared_lib = static_library('qdl_common', common_sources, dependencies: common_dep, include_directories: inc, ) # --- qdl executable --- qdl_exe = executable('qdl', sources : qdl_sources + [version_h], link_with: shared_lib, dependencies : common_compile_dep, include_directories : inc, install : true ) # --- udev rules --- # Prefer the rules directory advertised by udev.pc so a /usr/local prefix # still installs where udev looks; packagers can override it explicitly. if host_machine.system() == 'linux' and not get_option('udev').disabled() udevrulesdir = get_option('udevrulesdir') if udevrulesdir == '' udev_dep = dependency('udev', required : false) if udev_dep.found() udevrulesdir = udev_dep.get_variable(pkgconfig : 'udevdir') / 'rules.d' else udevrulesdir = get_option('prefix') / 'lib' / 'udev' / 'rules.d' endif endif # Device access is granted to the active local session via uaccess; an # additional group (e.g. plugdev on Debian) is opt-in as group names # differ between distributions. udev_conf = configuration_data() udev_group = get_option('udev_group') if udev_group != '' udev_conf.set('UDEV_GROUP_ATTRS', 'MODE="0660", GROUP="' + udev_group + '", ') else udev_conf.set('UDEV_GROUP_ATTRS', '') endif configure_file( input : 'udev/70-qdl.rules.in', output : '70-qdl.rules', configuration : udev_conf, install_dir : udevrulesdir ) endif # --- optional nbdkit plugin --- # nbdkit dlopens plugins, so there is no library to link against; the plugin # just needs and compiles all non-main sources into a shared # module named exactly "nbdkit-qdl-plugin.so". nbdkit_dep = dependency('nbdkit', required: get_option('nbdkit')) if nbdkit_dep.found() shared_module('nbdkit-qdl-plugin', sources : nbdkit_plugin_src + lib_sources + common_sources + [version_h], dependencies : common_dep + [nbdkit_dep], include_directories : inc, name_prefix : '', install : true, install_dir : get_option('libdir') / 'nbdkit' / 'plugins', ) endif # --- tests --- subdir('tests') # --- checkpatch targets --- check_script = join_paths(meson.project_source_root(), 'scripts', 'checkpatch_wrapper.sh') foreach checkpatchparam : ['download-checkpatch', 'check', 'check-cached', 'check-range'] run_target(checkpatchparam, command: ['bash', '-lc', check_script + ' ' + checkpatchparam] ) endforeach # --- markdown lint target --- markdown_script = join_paths(meson.project_source_root(), 'scripts', 'markdownlint_wrapper.sh') run_target('markdown-lint', command: ['bash', '-lc', markdown_script + ' check'] ) # --- man generation --- if help2man.found() # Test if we can run the built executable (might fail in cross-compilation) can_run_exe = not meson.is_cross_build() if can_run_exe manpage_targets = [] foreach prog : [ ['qdl', qdl_exe, 'Qualcomm Download'], ] manpage_targets += custom_target(prog[0] + '.1', input: prog[1], output: prog[0] + '.1', command: [help2man, '-N', '-n', prog[2], prog[1].full_path()], capture: true, install: true, install_dir: get_option('mandir') / 'man1') endforeach # Replicate the "manpages" phony target. alias_target requires at # least one dependency, so it is only declared when we have actually # produced manpage targets above. alias_target('manpages', manpage_targets) endif endif linux-msm-qdl-6e625aa/meson_options.txt000066400000000000000000000014741527206113100202750ustar00rootroot00000000000000option('VERSION', type: 'string', value: '', description: 'Project version') option('tests', type: 'feature', value: 'auto', description: 'Build unit tests (requires cmocka)') option('nbdkit', type: 'feature', value: 'auto', description: 'Build the nbdkit plugin (requires nbdkit)') option('zip-container', type: 'feature', value: 'enabled', description: 'Flash from and create zip archives (requires libzip)') option('udev', type: 'feature', value: 'auto', description: 'Install udev rules for EDL device access (Linux only)') option('udevrulesdir', type: 'string', value: '', description: 'Directory for udev rules (default: from udev.pc, else /lib/udev/rules.d)') option('udev_group', type: 'string', value: '', description: 'Group granted access to EDL devices in addition to uaccess, e.g. plugdev (default: none)') linux-msm-qdl-6e625aa/scripts/000077500000000000000000000000001527206113100163215ustar00rootroot00000000000000linux-msm-qdl-6e625aa/scripts/checkpatch_wrapper.sh000077500000000000000000000050541527206113100225210ustar00rootroot00000000000000#!/bin/bash # SPDX-License-Identifier: BSD-3-Clause set -euo pipefail ROOT_URL="https://raw.githubusercontent.com/torvalds/linux/v6.15/scripts" CHECKPATCH_URL="${ROOT_URL}/checkpatch.pl" SPELLING_URL="${ROOT_URL}/spelling.txt" ROOT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/../" CHECKPATCH_DIR="${ROOT_PATH}/.scripts" CHECKPATCH="${CHECKPATCH_DIR}/checkpatch.pl" SPELLING="${CHECKPATCH_DIR}/spelling.txt" # Just to repo root to work consistently from anywhere cd "$ROOT_PATH" # List only tracked source files (not ignored/untracked) CHECKPATCH_SOURCES=$(git ls-files | grep -E '\.(c|h|sh)$' | \ grep -vE '(^|/)sha2\.(c|h)$' | \ grep -vE 'version\.h$' | \ grep -vE 'list\.h$' ) ensure_checkpatch() { if [[ ! -x "$CHECKPATCH" ]]; then echo "Downloading checkpatch.pl and spelling.txt..." mkdir -p "$CHECKPATCH_DIR" curl -sSfL "$CHECKPATCH_URL" -o "$CHECKPATCH" curl -sSfL "$SPELLING_URL" -o "$SPELLING" chmod +x "$CHECKPATCH" fi } do_check_all() { ensure_checkpatch echo "Running checkpatch on tracked source files (excluding sha2.c, sha2.h)..." for file in ${CHECKPATCH_SOURCES}; do perl "${CHECKPATCH}" --no-tree -f "$file" || exit 1 done } do_check_cached() { ensure_checkpatch echo "Running checkpatch on staged changes..." git diff --cached -- . | perl "$CHECKPATCH" --no-tree - } do_check_range() { ensure_checkpatch local base="${CHECKPATCH_BASE:-origin/master}" local head="${CHECKPATCH_HEAD:-HEAD}" if ! git rev-parse --verify "${base}" >/dev/null 2>&1; then echo "Base ref '${base}' is not available." >&2 echo "Set CHECKPATCH_BASE to a fetched ref (e.g. origin/master)." >&2 exit 1 fi local commits commits=$(git rev-list --reverse "${base}..${head}") if [[ -z "${commits}" ]]; then echo "No commits in range ${base}..${head}; nothing to check." return 0 fi echo "Running checkpatch on commits in range ${base}..${head}..." local fail=0 for commit in ${commits}; do echo echo "--- Checking commit ${commit} ---" if ! git format-patch -1 --stdout "${commit}" | perl "${CHECKPATCH}" --no-tree -; then fail=1 fi done return $fail } usage() { echo "Usage: $0 {download-checkpatch|check|check-cached|check-range}" echo echo " check-range Run checkpatch per-commit on the range" echo " \$CHECKPATCH_BASE..\$CHECKPATCH_HEAD" echo " (defaults: origin/master..HEAD)" exit 2 } case "${1:-}" in download) ensure_checkpatch ;; check) do_check_all ;; check-cached) do_check_cached ;; check-range) do_check_range ;; *) usage ;; esac linux-msm-qdl-6e625aa/scripts/markdownlint_wrapper.sh000077500000000000000000000016061527206113100231340ustar00rootroot00000000000000#!/bin/bash # SPDX-License-Identifier: BSD-3-Clause set -euo pipefail ROOT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/../" # Run consistently from the repo root regardless of where the script is called cd "$ROOT_PATH" # Lint every markdown file tracked in git, except: # - CHANGELOG.md: generated by the release process; long release-note # URLs and per-release repeated section headers are part of the format # - CLAUDE.md: a symlink to AGENTS.md, which is already linted mapfile -t MARKDOWN_FILES < <(git ls-files '*.md' ':!CHANGELOG.md' ':!CLAUDE.md') run_lint() { if ! command -v mdl >/dev/null 2>&1; then echo "mdl not found." >&2 echo "Install it with: sudo apt install markdownlint (or 'gem install mdl')" >&2 exit 1 fi mdl "${MARKDOWN_FILES[@]}" } usage() { echo "Usage: $0 {check}" exit 2 } case "${1:-}" in check) run_lint ;; *) usage ;; esac linux-msm-qdl-6e625aa/src/000077500000000000000000000000001527206113100154215ustar00rootroot00000000000000linux-msm-qdl-6e625aa/src/auto.c000066400000000000000000000071701527206113100165420ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. * * QDL_DEVICE_AUTO: meta-backend that defers transport selection to the * wait loop. Each 250 ms tick it runs both the libusb open attempt and * (on Windows) a QUD SetupAPI probe; whichever first reaches an EDL * device wins, and its concrete qdl_device is bound as the inner. All * subsequent qdl_read/write/close calls on the outer forward to the * inner. */ #include #include #include #include #include #include "qdl.h" struct qdl_device_auto { struct qdl_device base; struct qdl_device *inner; long pending_chunk_size; bool chunk_size_set; }; static struct qdl_device_auto *to_auto(struct qdl_device *qdl) { return container_of(qdl, struct qdl_device_auto, base); } static void auto_bind_inner(struct qdl_device_auto *wrap, struct qdl_device *inner) { wrap->inner = inner; wrap->base.max_payload_size = inner->max_payload_size; if (wrap->chunk_size_set) inner->set_out_chunk_size(inner, wrap->pending_chunk_size); } static int auto_open(struct qdl_device *qdl, const char *serial) { struct qdl_device_auto *wrap = to_auto(qdl); struct qdl_device *usb_dev; #ifdef _WIN32 struct qdl_device *qud_dev; int qud_count; #endif int visible_prev = -1; int visible; int ret; usb_dev = usb_init(); if (!usb_dev) return -1; #ifdef _WIN32 qud_dev = qud_init(); if (!qud_dev) { qdl_deinit(usb_dev); return -1; } #endif for (;;) { ret = usb_open_once(usb_dev, serial, &visible); if (ret == 0) { #ifdef _WIN32 qdl_deinit(qud_dev); #endif auto_bind_inner(wrap, usb_dev); return 0; } if (ret == -EIO) goto fail; #ifdef _WIN32 qud_count = qud_probe_present(); if (qud_count > 0 || ret == -EBUSY) { if (qud_dev->open(qud_dev, serial) == 0) { qdl_deinit(usb_dev); auto_bind_inner(wrap, qud_dev); return 0; } } visible += qud_count; #endif if (visible != visible_prev) { if (visible == 0) ux_info("Waiting for EDL device\n"); else if (serial) ux_info("%d EDL device(s) visible, none match serial \"%s\"\n", visible, serial); else ux_info("%d EDL device(s) visible, none could be opened\n", visible); visible_prev = visible; } usleep(250000); } fail: qdl_deinit(usb_dev); #ifdef _WIN32 qdl_deinit(qud_dev); #endif return -1; } static int auto_read(struct qdl_device *qdl, void *buf, size_t len, unsigned int timeout) { struct qdl_device *inner = to_auto(qdl)->inner; return inner->read(inner, buf, len, timeout); } static int auto_write(struct qdl_device *qdl, const void *buf, size_t len, unsigned int timeout) { struct qdl_device *inner = to_auto(qdl)->inner; return inner->write(inner, buf, len, timeout); } static void auto_close(struct qdl_device *qdl) { struct qdl_device_auto *wrap = to_auto(qdl); if (!wrap->inner) return; wrap->inner->close(wrap->inner); qdl_deinit(wrap->inner); wrap->inner = NULL; } static void auto_set_out_chunk_size(struct qdl_device *qdl, long size) { struct qdl_device_auto *wrap = to_auto(qdl); if (wrap->inner) { wrap->inner->set_out_chunk_size(wrap->inner, size); return; } wrap->pending_chunk_size = size; wrap->chunk_size_set = true; } struct qdl_device *auto_init(void) { struct qdl_device_auto *wrap = calloc(1, sizeof(*wrap)); if (!wrap) return NULL; wrap->base.dev_type = QDL_DEVICE_AUTO; wrap->base.open = auto_open; wrap->base.read = auto_read; wrap->base.write = auto_write; wrap->base.close = auto_close; wrap->base.set_out_chunk_size = auto_set_out_chunk_size; wrap->base.max_payload_size = 1048576; return &wrap->base; } linux-msm-qdl-6e625aa/src/cli.c000066400000000000000000000234471527206113100163460ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* Command-line front-end helpers: usage text and flashing-run options. */ #include #include #include #include #include #include "qdl.h" #include "oscompat.h" #include "cli.h" #include "version.h" /* Print one option or argument description, highlighting its @name */ static void usage_opt(FILE *out, const char *indent, const char *name, const char *desc) { fprintf(out, "%s%s%s%s%s\n", indent, ux_style(out, UX_STYLE_OPTION), name, ux_style(out, UX_STYLE_RESET), desc); } void print_usage(FILE *out) { fprintf(out, "%sUsage:%s %s [options] ( | | )...\n", ux_style(out, UX_STYLE_HEADING), ux_style(out, UX_STYLE_RESET), ux_progname()); fprintf(out, " %s [options] ((read | write)
)...\n", ux_progname()); fprintf(out, " %s [options] (erase
)...\n", ux_progname()); fprintf(out, " %s [options] (sha256
)...\n", ux_progname()); fprintf(out, " %s [options] (reset)\n", ux_progname()); fprintf(out, " %s list\n", ux_progname()); fprintf(out, " %s chipinfo\n", ux_progname()); fprintf(out, " %s reset\n", ux_progname()); fprintf(out, " %s ramdump [--debug] [-o ] [,...]\n", ux_progname()); fprintf(out, " %s ks [-p | --serial=T] -s ...\n", ux_progname()); fprintf(out, " %s flash ([::specifier] | [::])\n", ux_progname()); fprintf(out, " %s create-zip [::]\n", ux_progname()); fprintf(out, " %s create-sahara-archive " "([,...] | | [::])\n", ux_progname()); usage_opt(out, " ", "-d, --debug", "\t\t\tPrint detailed debug info"); usage_opt(out, " ", "-q, --quiet", "\t\t\tPrint only the summary, warnings and errors"); usage_opt(out, " ", "--color[=W]", "\t\tColorize output W: (default: auto)"); usage_opt(out, " ", "-v, --version", "\t\t\tPrint the current version and exit"); usage_opt(out, " ", "-n, --dry-run", "\t\t\tDry run execution, no device reading or flashing"); usage_opt(out, " ", "-f, --allow-missing", "\t\tAllow skipping of missing files during flashing"); usage_opt(out, " ", "-s, --storage=T", "\t\tSet target storage type T: "); usage_opt(out, " ", "-l, --finalize-provisioning", "\tProvision the target storage"); usage_opt(out, " ", "-i, --include=T", "\t\tSet an optional folder T to search for files"); usage_opt(out, " ", "-S, --serial=T", "\t\t\tSelect target by serial number T (e.g. <0AA94EFD>, case-insensitive)"); usage_opt(out, " ", "-u, --out-chunk-size=T", "\t\tOverride chunk size for transaction with T"); usage_opt(out, " ", "-t, --create-digests=T", "\t\tGenerate table of digests in the T folder"); usage_opt(out, " ", "-T, --slot=T", "\t\t\tSet slot number T for multiple storage devices"); usage_opt(out, " ", "-D, --vip-table-path=T", "\t\tUse digest tables in the T folder for VIP"); usage_opt(out, " ", "-R, --skip-reset", "\t\tDo not send the reset command after flashing completes"); usage_opt(out, " ", "--backend=B", "\t\tSelect device backend B: (default: auto)"); usage_opt(out, " ", "--skipblock=M", "\t\tUse readback mechanism M to skip entries already on flash;"); fprintf(out, " \t\tM: (default: none)\n"); usage_opt(out, " ", "-h, --help", "\t\t\tPrint this usage info"); usage_opt(out, " ", "", "\t\txml file containing or directives"); usage_opt(out, " ", "", "\t\txml file containing directives"); usage_opt(out, " ", "", "\t\txml file containing directives"); usage_opt(out, " ", "
", "\t\tdisk address specifier, can be one of

,

,

, , or"); fprintf(out, " \t\t

, to specify a physical partition number P, a starting sector\n"); fprintf(out, " \t\tnumber S, the number of sectors to follow L, or partition by \"name\"\n"); usage_opt(out, " ", "", "\t\tpath where ramdump should stored"); usage_opt(out, " ", "", "\toptional glob-pattern to select which segments to ramdump"); usage_opt(out, " ", "", "\tSahara device node, e.g. /dev/mhi0_QAIC_SAHARA (ks);"); fprintf(out, " \tomit to use the selected device backend (ks)\n"); usage_opt(out, " ", "", "\t\tmap a Sahara image id to a host file, repeatable (ks)"); usage_opt(out, " ", "", "\tflashmap JSON file, or ZIP archive with flashmap.json"); usage_opt(out, " ", "", "\tcontents XML file"); usage_opt(out, " ", "", "\tSahara programmer archive to create"); usage_opt(out, " ", "", "\tcomma-separated list of specifiers, such as storage type, layout, and flavors"); fprintf(out, "\n"); fprintf(out, "%sExample:%s %s prog_firehose_ddr.elf rawprogram*.xml patch*.xml\n", ux_style(out, UX_STYLE_HEADING), ux_style(out, UX_STYLE_RESET), ux_progname()); fprintf(out, " %s flash contents.xml::ufs,spinor/safe_rtos\n", ux_progname()); fprintf(out, " %s flash installer.zip::layout1/ufs\n", ux_progname()); } /* * qdl_take_color_option() - strip --color[=WHEN] from @argv and apply it * * Done before dispatch, as every subcommand has its own option parser. * Arguments after "--" are left alone; the last occurrence wins. * * Return: 0, or -1 if WHEN is invalid. */ int qdl_take_color_option(int *argc, char **argv) { enum ux_color_mode mode; const char *value; int out = 1; int i; /* Keep an empty argv empty, argv[1] is past its end */ if (*argc < 1) return 0; for (i = 1; i < *argc; i++) { if (!strcmp(argv[i], "--")) { while (i < *argc) argv[out++] = argv[i++]; break; } if (!strcmp(argv[i], "--color")) { value = NULL; } else if (!strncmp(argv[i], "--color=", 8)) { value = argv[i] + 8; } else { argv[out++] = argv[i]; continue; } if (decode_color_mode(value, &mode) < 0) { ux_warnx("unknown color mode \"%s\" (expected auto|always|never)", value); return -1; } ux_set_color_mode(mode); } *argc = out; argv[out] = NULL; return 0; } void print_version(void) { fprintf(stdout, "%s version %s\n", ux_progname(), VERSION); } /* * Parse the flashing options into @opts and leave optind pointing at the * first positional argument. Fatal value errors (unknown storage/backend/ * skipblock) still ux_diex() out; the return value only distinguishes the * normal, help/version and usage-error cases. */ int qdl_parse_args(int argc, char **argv, struct qdl_opts *opts) { static struct option options[] = { {"debug", no_argument, 0, 'd'}, {"version", no_argument, 0, 'v'}, {"include", required_argument, 0, 'i'}, {"finalize-provisioning", no_argument, 0, 'l'}, {"out-chunk-size", required_argument, 0, 'u' }, {"serial", required_argument, 0, 'S'}, {"vip-table-path", required_argument, 0, 'D'}, {"storage", required_argument, 0, 's'}, {"allow-missing", no_argument, 0, 'f'}, {"allow-fusing", no_argument, 0, 'c'}, {"dry-run", no_argument, 0, 'n'}, {"create-digests", required_argument, 0, 't'}, {"slot", required_argument, 0, 'T'}, {"skip-reset", no_argument, 0, 'R'}, {"quiet", no_argument, 0, 'q'}, {"backend", required_argument, 0, OPT_BACKEND}, {"skipblock", required_argument, 0, OPT_SKIPBLOCK}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; int opt; *opts = (struct qdl_opts){ .storage_type = QDL_STORAGE_UFS, .dev_type = QDL_DEVICE_AUTO, .skipblock_mode = QDL_SKIPBLOCK_NONE, .slot = UINT_MAX, }; while ((opt = getopt_long(argc, argv, "dvi:lu:S:D:s:fcnt:T:Rqh", options, NULL)) != -1) { switch (opt) { case 'd': qdl_debug = true; break; case 'q': ux_set_quiet(true); break; case 'n': opts->dev_type = QDL_DEVICE_SIM; break; case 't': opts->vip_generate_dir = optarg; /* we also enforce dry-run mode */ opts->dev_type = QDL_DEVICE_SIM; break; case 'v': print_version(); return QDL_ARGS_DONE; case 'f': opts->allow_missing = true; break; case 'i': opts->incdir = optarg; break; case 'l': opts->finalize_provisioning = true; break; case 'c': opts->allow_fusing = true; break; case 'u': opts->out_chunk_size = strtol(optarg, NULL, 10); break; case 's': opts->storage_type = decode_storage_type(optarg); if (opts->storage_type == QDL_STORAGE_UNKNOWN) ux_diex(1, "unknown storage type \"%s\"", optarg); break; case 'S': opts->serial = optarg; break; case 'D': opts->vip_table_path = optarg; break; case 'T': opts->slot = (unsigned int)strtoul(optarg, NULL, 10); break; case 'R': opts->skip_reset = true; break; case OPT_BACKEND: /* * --dry-run / --create-digests already pinned the backend to * QDL_DEVICE_SIM; honour that and ignore --backend in that case. */ if (opts->dev_type != QDL_DEVICE_SIM && decode_backend(optarg, &opts->dev_type) < 0) ux_diex(1, "unknown backend \"%s\" (expected auto|usb|qud)", optarg); break; case OPT_SKIPBLOCK: if (!strcmp(optarg, "none")) opts->skipblock_mode = QDL_SKIPBLOCK_NONE; else if (!strcmp(optarg, "sha256")) opts->skipblock_mode = QDL_SKIPBLOCK_SHA256; else ux_diex(1, "unknown --skipblock mode \"%s\", valid options are none and sha256", optarg); break; case 'h': print_usage(stdout); return QDL_ARGS_DONE; default: print_usage(stderr); return QDL_ARGS_ERROR; } } /* at least 2 non optional args required */ if ((optind + 2) > argc) { print_usage(stderr); return QDL_ARGS_ERROR; } return QDL_ARGS_OK; } linux-msm-qdl-6e625aa/src/cli.h000066400000000000000000000021621527206113100163420ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __CLI_H__ #define __CLI_H__ #include #include /* Long-only option ids, distinct from any short option character. */ enum { OPT_BACKEND = 0x100, OPT_SKIPBLOCK, OPT_SERIAL, }; /* Command-line options and flags for a flashing run. */ struct qdl_opts { enum qdl_storage_type storage_type; enum QDL_DEVICE_TYPE dev_type; enum qdl_skipblock_mode skipblock_mode; char *incdir; char *serial; const char *vip_generate_dir; const char *vip_table_path; long out_chunk_size; unsigned int slot; bool finalize_provisioning; bool allow_fusing; bool allow_missing; bool skip_reset; }; /* Result of qdl_parse_args(). */ enum { QDL_ARGS_OK, /* options parsed, positional arguments follow */ QDL_ARGS_DONE, /* handled entirely (--help/--version); exit 0 */ QDL_ARGS_ERROR, /* usage error; exit 1 */ }; int qdl_parse_args(int argc, char **argv, struct qdl_opts *opts); int qdl_take_color_option(int *argc, char **argv); void print_usage(FILE *out); void print_version(void); #endif linux-msm-qdl-6e625aa/src/contents.c000066400000000000000000000635301527206113100174310ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #include #include #include #include #include #include #include #include #include #include "contents.h" #include "file.h" #include "firehose.h" #include "pathbuf.h" #include "qdl.h" #ifdef _WIN32 #define ROOT_PATH_TAG "windows_root_path" #else #define ROOT_PATH_TAG "linux_root_path" #endif enum contents_file_type { CONTENTS_FILE_OTHER, CONTENTS_FILE_PROGRAM, CONTENTS_FILE_PATCH, CONTENTS_FILE_DEVICE_PROGRAMMER, CONTENTS_FILE_PROGRAMMER_XML, }; enum firehose_type { FIREHOSE_TYPE_NONE, FIREHOSE_TYPE_LITE, FIREHOSE_TYPE_TRUE, }; struct contents_entry { enum contents_file_type file_type; enum qdl_storage_type storage_type; char *flavor; char *filename; struct pathbuf path; enum firehose_type firehose_type; struct list_head node; }; struct contents { struct list_head entries; struct pathbuf base_dir; char **flavors; size_t num_flavors; }; struct contents_filter { struct contents *contents; enum qdl_storage_type storage_type; const char *flavor; }; struct contents_selector { enum qdl_storage_type storage_type; const char *flavor; }; static const char *contents_storage_name(enum qdl_storage_type storage) { const char *name = encode_storage_type(storage); return name ? name : "unknown"; } static char *contents_node_get_text(xmlNode *node) { const xmlChar *start; const xmlChar *end; xmlChar *str; size_t len; char *ret; str = xmlNodeGetContent(node); if (!str) return NULL; for (start = str; *start && isspace((unsigned char)*start); start++) ; for (end = start + xmlStrlen(start); end > start && isspace((unsigned char)end[-1]); end--) ; len = end - start; ret = calloc(1, len + 1); if (!ret) goto out_free_str; memcpy(ret, start, len); out_free_str: xmlFree(str); return ret; } static bool contents_entry_is_ignored(xmlNode *node) { xmlChar *ignore; bool ret; ignore = xmlGetProp(node, (xmlChar *)"ignore"); if (!ignore) return false; ret = !xmlStrcmp(ignore, (xmlChar *)"true"); xmlFree(ignore); return ret; } static int contents_parse_pf(struct contents *contents, xmlNode *node) { char **new_flavors; char *name = NULL; for (; node; node = node->next) { if (node->type == XML_ELEMENT_NODE && !xmlStrcmp(node->name, (xmlChar *)"name")) { name = contents_node_get_text(node); break; } } if (!name) return -1; new_flavors = realloc(contents->flavors, (contents->num_flavors + 1) * sizeof(*new_flavors)); if (!new_flavors) { free(name); return -1; } contents->flavors = new_flavors; contents->flavors[contents->num_flavors++] = name; return 0; } static int contents_parse_pfs(struct contents *contents, xmlNode *node) { int ret; for (; node; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (!xmlStrcmp(node->name, (xmlChar *)"pf")) { ret = contents_parse_pf(contents, node->children); if (ret < 0) { ux_err("failed to parse product flavor definition\n"); return -1; } } } return 0; } static enum contents_file_type contents_detect_file_type(xmlNode *node, enum firehose_type *fh_type) { enum contents_file_type type = CONTENTS_FILE_OTHER; xmlChar *firehose_type; if (!xmlStrcmp(node->name, (xmlChar *)"partition_file")) type = CONTENTS_FILE_PROGRAM; if (!xmlStrcmp(node->name, (xmlChar *)"partition_patch_file")) type = CONTENTS_FILE_PATCH; if (!xmlStrcmp(node->name, (xmlChar *)"device_programmer")) type = CONTENTS_FILE_DEVICE_PROGRAMMER; firehose_type = xmlGetProp(node, (xmlChar *)"firehose_type"); if (firehose_type) { if (!xmlStrcmp(firehose_type, (xmlChar *)"true")) { type = CONTENTS_FILE_PROGRAMMER_XML; *fh_type = FIREHOSE_TYPE_TRUE; } else if (!xmlStrcmp(firehose_type, (xmlChar *)"lite")) { *fh_type = FIREHOSE_TYPE_LITE; } else { *fh_type = FIREHOSE_TYPE_NONE; } } xmlFree(firehose_type); return type; } static enum qdl_storage_type contents_detect_storage_type(xmlNode *node) { enum qdl_storage_type type = QDL_STORAGE_UNKNOWN; xmlChar *storage; storage = xmlGetProp(node, (xmlChar *)"storage_type"); if (storage) { type = decode_storage_type((char *)storage); xmlFree(storage); } return type; } static int contents_expand_path_vars(struct pathbuf *path) { char *head = &path->buf[0]; char *tail = head; char *colon; char *end; /* * Paths can contain "${var:default}" entries, which allow overriding * portions of the path. As this is not supported, replace each with * "default". */ while (*head) { if (head[0] == '$' && head[1] == '{') { colon = strchr(head + 2, ':'); end = colon ? strchr(colon + 1, '}') : NULL; if (colon && end) { head = colon + 1; while (head < end) *tail++ = *head++; head = end + 1; continue; } } *tail++ = *head++; } *tail = '\0'; path->len = tail - path->buf; return 0; } static int contents_parse_file_names(struct contents *contents, xmlNode *node, enum contents_file_type file_type, struct pathbuf *current_path, char *path_flavor, enum qdl_storage_type storage, enum firehose_type firehose_type) { struct contents_entry *entry; struct pathbuf full_path; xmlNode *child; char *filename; int ret; for (child = node->children; child; child = child->next) { if (child->type != XML_ELEMENT_NODE) continue; if (xmlStrcmp(child->name, (xmlChar *)"file_name")) continue; filename = contents_node_get_text(child); if (!filename) return -1; /* Ignore filenames with wildcards */ if (strstr(filename, "*")) { free(filename); continue; } qdl_pathbuf_dup(&full_path, current_path); ret = qdl_pathbuf_push(&full_path, filename); if (ret < 0) { free(filename); return -1; } contents_expand_path_vars(&full_path); entry = calloc(1, sizeof(*entry)); if (!entry) { free(filename); return -1; } entry->file_type = file_type; entry->storage_type = storage; if (path_flavor) { entry->flavor = strdup(path_flavor); if (!entry->flavor) { free(entry); free(filename); return -1; } } entry->firehose_type = firehose_type; entry->filename = filename; qdl_pathbuf_dup(&entry->path, &full_path); list_append(&contents->entries, &entry->node); } return 0; } static int contents_parse_entry(struct contents *contents, xmlNode *node, struct pathbuf *build_root) { enum contents_file_type file_type; enum firehose_type firehose_type = FIREHOSE_TYPE_NONE; enum qdl_storage_type storage; struct pathbuf file_path; xmlNode *child; char *flavor; char *path; int ret; if (!build_root) { ux_err("entry has no build root path in contents.xml\n"); return -1; } if (contents_entry_is_ignored(node)) return 0; file_type = contents_detect_file_type(node, &firehose_type); storage = contents_detect_storage_type(node); for (child = node->children; child; child = child->next) { if (child->type != XML_ELEMENT_NODE) continue; if (xmlStrcmp(child->name, (xmlChar *)"file_path")) continue; path = contents_node_get_text(child); if (!path) return -1; flavor = (char *)xmlGetProp(child, (xmlChar *)"flavor"); qdl_pathbuf_dup(&file_path, build_root); ret = qdl_pathbuf_push(&file_path, path); if (ret < 0) { xmlFree(flavor); free(path); return -1; } ret = contents_parse_file_names(contents, node, file_type, &file_path, flavor, storage, firehose_type); xmlFree(flavor); free(path); if (ret < 0) return ret; } return 0; } static bool contents_is_entry_node(xmlNode *node) { return !xmlStrcmp(node->name, (xmlChar *)"download_file") || !xmlStrcmp(node->name, (xmlChar *)"file_ref") || !xmlStrcmp(node->name, (xmlChar *)"partition_file") || !xmlStrcmp(node->name, (xmlChar *)"partition_patch_file") || !xmlStrcmp(node->name, (xmlChar *)"device_programmer"); } static int contents_parse_builds(struct contents *contents, xmlNode *node, struct pathbuf *root_path) { char *root; int ret; for (; node; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (!xmlStrcmp(node->name, (xmlChar *)"build")) { struct pathbuf build_root = {0}; ret = contents_parse_builds(contents, node->children, &build_root); if (ret < 0) return ret; continue; } if (!xmlStrcmp(node->name, (xmlChar *)ROOT_PATH_TAG)) { root = contents_node_get_text(node); if (!root) return -1; if (!root_path) { ux_err("linux_root_path without active build context\n"); free(root); return -1; } qdl_pathbuf_dup(root_path, &contents->base_dir); ret = qdl_pathbuf_push(root_path, root); free(root); if (ret < 0) return -1; } else if (contents_is_entry_node(node)) { ret = contents_parse_entry(contents, node, root_path); if (ret < 0) return ret; } } return 0; } static int contents_parse_nodes(struct contents *contents, xmlNode *node) { int ret; for (; node; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (!xmlStrcmp(node->name, (xmlChar *)"product_flavors")) { ret = contents_parse_pfs(contents, node->children); if (ret < 0) return ret; } else if (!xmlStrcmp(node->name, (xmlChar *)"builds_flat")) { ret = contents_parse_builds(contents, node->children, NULL); if (ret < 0) return ret; } } return 0; } static int contents_get_base_dir(struct pathbuf *base_dir, const char *filename) { qdl_pathbuf_reset(base_dir); qdl_pathbuf_push(base_dir, filename); qdl_pathbuf_dirname(base_dir); return 0; } int contents_load_xml(struct contents *contents, const char *filename) { xmlNode *root; xmlDoc *doc; int ret; doc = xmlReadFile(filename, NULL, 0); if (!doc) { ux_err("failed to parse contents file \"%s\"\n", filename); return -1; } root = xmlDocGetRootElement(doc); if (!root || xmlStrcmp(root->name, (xmlChar *)"contents")) { ux_err("specified file \"%s\" is not a contents.xml document\n", filename); xmlFreeDoc(doc); return -1; } ret = contents_get_base_dir(&contents->base_dir, filename); if (ret < 0) goto err_free_doc; ret = contents_parse_nodes(contents, root->children); if (ret < 0) goto err_free_doc; xmlFreeDoc(doc); if (list_empty(&contents->entries)) ux_info_tag(UX_TAG_CONTENTS, "no file entries parsed from \"%s\"\n", filename); return 0; err_free_doc: xmlFreeDoc(doc); return -1; } static int contents_find_programmers(struct contents *contents, struct contents_filter *filter, struct sahara_image *images) { struct contents_filter default_filter = { .contents = contents }; struct contents_entry *entry; struct sahara_image blob; int ret; if (!filter) filter = &default_filter; list_for_each_entry(entry, &contents->entries, node) { if (entry->file_type != CONTENTS_FILE_PROGRAMMER_XML) continue; if (filter->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != filter->storage_type) continue; if (entry->flavor && filter->flavor && strcmp(entry->flavor, filter->flavor)) continue; ret = load_sahara_image(NULL, qdl_pathbuf_str(&entry->path), &blob); if (ret < 0) { ux_err("unable to open \"%s\" for reading\n", qdl_pathbuf_str(&entry->path)); continue; } ret = decode_sahara_config(&blob, images, filter); if (ret == 0) { ux_err("%s is not a programmer xml\n", qdl_pathbuf_str(&entry->path)); sahara_images_free(&blob, 1); continue; } else if (ret < 0) { ux_err("failed to parse programmer xml \"%s\"\n", qdl_pathbuf_str(&entry->path)); sahara_images_free(&blob, 1); return -1; } sahara_images_free(&blob, 1); return 0; } list_for_each_entry(entry, &contents->entries, node) { if (entry->file_type != CONTENTS_FILE_DEVICE_PROGRAMMER) continue; if (filter->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != filter->storage_type) continue; if (entry->flavor && filter->flavor && strcmp(entry->flavor, filter->flavor)) continue; if (entry->firehose_type == FIREHOSE_TYPE_LITE) continue; ret = load_sahara_image(NULL, qdl_pathbuf_str(&entry->path), &images[SAHARA_ID_EHOSTDL_IMG]); if (ret < 0) { ux_err("unable to open \"%s\" for reading\n", qdl_pathbuf_str(&entry->path)); continue; } return 0; } ux_err("no programmer definitions found\n"); return -1; } static bool contents_flavor_matches(const char *a, const char *b) { return (!a && !b) || (a && b && !strcmp(a, b)); } static bool contents_selector_is_known(struct contents_selector *selectors, size_t count, enum qdl_storage_type storage_type, const char *flavor) { size_t i; for (i = 0; i < count; i++) { if (selectors[i].storage_type == storage_type && contents_flavor_matches(selectors[i].flavor, flavor)) return true; } return false; } static bool contents_storage_is_selected(struct contents_selector *selectors, size_t count, enum qdl_storage_type storage_type) { size_t selector_idx; for (selector_idx = 0; selector_idx < count; selector_idx++) { if (selectors[selector_idx].storage_type == storage_type) return true; } return false; } static bool contents_storage_has_flavored_entries(struct contents *contents, enum qdl_storage_type storage_type) { struct contents_entry *entry; list_for_each_entry(entry, &contents->entries, node) { if (entry->file_type != CONTENTS_FILE_PROGRAM && entry->file_type != CONTENTS_FILE_PATCH) continue; if (entry->storage_type != storage_type) continue; if (entry->flavor) return true; } return false; } static size_t contents_collect_valid_selectors(struct contents *contents, struct contents_selector **contents_selectors) { struct contents_selector *new_selectors; struct contents_selector *selectors = NULL; struct contents_entry *entry; size_t count = 0; list_for_each_entry(entry, &contents->entries, node) { if (entry->file_type != CONTENTS_FILE_PROGRAM && entry->file_type != CONTENTS_FILE_PATCH) continue; if (entry->storage_type == QDL_STORAGE_UNKNOWN) continue; if (!entry->flavor && contents_storage_has_flavored_entries(contents, entry->storage_type)) continue; if (contents_selector_is_known(selectors, count, entry->storage_type, entry->flavor)) continue; new_selectors = realloc(selectors, (count + 1) * sizeof(*selectors)); if (!new_selectors) { free(selectors); return 0; } selectors = new_selectors; selectors[count].storage_type = entry->storage_type; selectors[count].flavor = entry->flavor; count++; } *contents_selectors = selectors; return count; } static void contents_print_valid_selectors(struct contents_selector *selectors, size_t count) { size_t selector_idx; ux_err("valid storage/flavor combinations:\n"); for (selector_idx = 0; selector_idx < count; selector_idx++) { if (selectors[selector_idx].flavor) ux_err(" %s/%s\n", contents_storage_name(selectors[selector_idx].storage_type), selectors[selector_idx].flavor); else ux_err(" %s\n", contents_storage_name(selectors[selector_idx].storage_type)); } } static bool contents_flavor_is_valid(struct contents *contents, const char *flavor) { size_t flavor_idx; for (flavor_idx = 0; flavor_idx < contents->num_flavors; flavor_idx++) { if (!strcmp(flavor, contents->flavors[flavor_idx])) return true; } return false; } static size_t contents_select_by_storage(struct contents_selector *valid_selectors, size_t num_valid_selectors, enum qdl_storage_type storage_type, struct contents_selector *selector) { size_t valid_idx; size_t matches = 0; for (valid_idx = 0; valid_idx < num_valid_selectors; valid_idx++) { if (valid_selectors[valid_idx].storage_type != storage_type) continue; *selector = valid_selectors[valid_idx]; matches++; } return matches; } static size_t contents_select_by_flavor(struct contents_selector *valid_selectors, size_t num_valid_selectors, const char *flavor, struct contents_selector *selector) { size_t valid_idx; size_t matches = 0; for (valid_idx = 0; valid_idx < num_valid_selectors; valid_idx++) { if (!valid_selectors[valid_idx].flavor || strcmp(valid_selectors[valid_idx].flavor, flavor)) continue; *selector = valid_selectors[valid_idx]; matches++; } return matches; } static int contents_decode_selectors(struct contents *contents, char *pattern, struct contents_selector **contents_selectors) { struct contents_selector *valid_selectors = NULL; struct contents_selector *new_selectors; struct contents_selector *selectors = NULL; struct contents_selector selector; enum qdl_storage_type storage; size_t num_valid_selectors; char *flavor; size_t count = 0; char *token; char *save; char *sep; size_t matches; num_valid_selectors = contents_collect_valid_selectors(contents, &valid_selectors); if (!num_valid_selectors) { ux_err("contents.xml does not provide any valid storage/flavor combinations\n"); return -1; } if (!pattern) { if (num_valid_selectors == 1) { *contents_selectors = valid_selectors; return 1; } if (num_valid_selectors > 1) { ux_err("contents.xml contains multiple storage/flavor combinations; select one or more with ::/\n"); contents_print_valid_selectors(valid_selectors, num_valid_selectors); free(valid_selectors); return -1; } free(valid_selectors); return -1; } if (!pattern[0]) { ux_err("empty storage/flavor selector\n"); goto err_free_valid_selectors; } for (token = strtok_r(pattern, ",", &save); token; token = strtok_r(NULL, ",", &save)) { new_selectors = realloc(selectors, (count + 1) * sizeof(*selectors)); if (!new_selectors) goto err_free_selectors; selectors = new_selectors; if (!token[0]) { ux_err("empty storage/flavor selector\n"); goto err_free_selectors; } sep = strchr(token, '/'); if (!sep) { storage = decode_storage_type(token); if (storage != QDL_STORAGE_UNKNOWN) { matches = contents_select_by_storage(valid_selectors, num_valid_selectors, storage, &selector); if (matches == 1) goto append_selector; if (!matches) ux_err("storage type \"%s\" has no valid flavor in contents.xml\n", token); else ux_err("storage type \"%s\" is ambiguous; specify a flavor\n", token); contents_print_valid_selectors(valid_selectors, num_valid_selectors); goto err_free_selectors; } matches = contents_select_by_flavor(valid_selectors, num_valid_selectors, token, &selector); if (matches == 1) goto append_selector; if (matches > 1) ux_err("flavor \"%s\" is ambiguous; specify a storage type\n", token); else if (contents_flavor_is_valid(contents, token)) ux_err("flavor \"%s\" has no valid storage type in contents.xml\n", token); else ux_err("unknown storage type or flavor \"%s\"\n", token); contents_print_valid_selectors(valid_selectors, num_valid_selectors); goto err_free_selectors; } *sep = '\0'; flavor = sep + 1; if (!token[0]) { ux_err("missing storage selector for flavor \"%s\"\n", flavor); goto err_free_selectors; } if (!flavor[0]) { ux_err("invalid flavor selection for storage \"%s\"\n", token); goto err_free_selectors; } if (strchr(flavor, '/')) { ux_err("invalid flavor selector \"%s\"\n", flavor); goto err_free_selectors; } if (!contents_flavor_is_valid(contents, flavor)) { ux_err("invalid flavor \"%s\" requested\n", flavor); ux_err("valid flavors:\n"); for (size_t flavor_idx = 0; flavor_idx < contents->num_flavors; flavor_idx++) ux_err(" %s\n", contents->flavors[flavor_idx]); goto err_free_selectors; } storage = decode_storage_type(token); if (storage == QDL_STORAGE_UNKNOWN) { ux_err("unknown storage type \"%s\"\n", token); goto err_free_selectors; } if (!contents_selector_is_known(valid_selectors, num_valid_selectors, storage, flavor)) { ux_err("storage/flavor combination \"%s/%s\" not mentioned in contents.xml\n", contents_storage_name(storage), flavor); contents_print_valid_selectors(valid_selectors, num_valid_selectors); goto err_free_selectors; } selector.storage_type = storage; selector.flavor = flavor; append_selector: if (contents_storage_is_selected(selectors, count, selector.storage_type)) { ux_err("storage type \"%s\" selected multiple times\n", contents_storage_name(selector.storage_type)); goto err_free_selectors; } selectors[count] = selector; count++; } *contents_selectors = selectors; free(valid_selectors); return count; err_free_selectors: free(selectors); err_free_valid_selectors: free(valid_selectors); return -1; } int contents_load(struct list_head *ops, const char *filename, char *specifier, struct sahara_image *images, const char *incdir) { struct contents_filter filter = {}; struct contents_entry *entry; struct contents_entry *next; struct contents contents = {}; enum qdl_storage_type storage_type; struct contents_selector *selectors = NULL; struct firehose_op *op; const char *flavor; int num_selectors; char *pattern = specifier; size_t flavor_idx; int ret; int i; list_init(&contents.entries); ret = contents_load_xml(&contents, filename); if (ret < 0) goto out_free_contents; ret = contents_decode_selectors(&contents, pattern, &selectors); if (ret < 0) goto out_free_contents; num_selectors = ret; if (num_selectors == 0) { ux_err("contents.xml does not provide any valid storage/flavor combinations\n"); ret = -1; goto out_free_contents; } filter.contents = &contents; ret = contents_find_programmers(&contents, &filter, images); if (ret < 0) goto out_free_contents; for (i = 0; i < num_selectors; i++) { storage_type = selectors[i].storage_type; flavor = selectors[i].flavor; op = firehose_alloc_op(FIREHOSE_OP_CONFIGURE); if (!op) { ret = -1; goto out_free_contents; } op->storage_type = storage_type; list_append(ops, &op->node); list_for_each_entry(entry, &contents.entries, node) { if (entry->file_type != CONTENTS_FILE_PROGRAM) continue; if (entry->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != storage_type) continue; if (entry->flavor && (!flavor || strcmp(entry->flavor, flavor))) continue; filter.contents = &contents; filter.storage_type = storage_type; filter.flavor = flavor; ret = program_load(ops, qdl_pathbuf_str(&entry->path), storage_type == QDL_STORAGE_NAND, false, &filter, incdir); if (ret < 0) { ux_err("failed to load program: %s\n", entry->filename); goto out_free_contents; } } list_for_each_entry(entry, &contents.entries, node) { if (entry->file_type != CONTENTS_FILE_PATCH) continue; if (entry->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != storage_type) continue; if (entry->flavor && (!flavor || strcmp(entry->flavor, flavor))) continue; ret = patch_load(ops, qdl_pathbuf_str(&entry->path)); if (ret < 0) { ux_err("failed to load %s\n", entry->filename); goto out_free_contents; } } } out_free_contents: for (flavor_idx = 0; flavor_idx < contents.num_flavors; flavor_idx++) free(contents.flavors[flavor_idx]); free(contents.flavors); list_for_each_entry_safe(entry, next, &contents.entries, node) { free(entry->filename); free(entry->flavor); free(entry); } free(selectors); return ret; } int contents_load_programmers(const char *filename, char *specifier, struct sahara_image *images) { struct contents_filter filter = {}; struct contents_selector *selectors = NULL; struct contents_entry *entry; struct contents_entry *next; struct contents contents = {}; size_t flavor_idx; int ret; list_init(&contents.entries); ret = contents_load_xml(&contents, filename); if (ret < 0) goto out_free_contents; ret = contents_decode_selectors(&contents, specifier, &selectors); if (ret < 0) goto out_free_contents; if (ret != 1) { ux_err("select exactly one storage/flavor combination for Sahara archive creation\n"); ret = -1; goto out_free_contents; } filter.contents = &contents; filter.storage_type = selectors[0].storage_type; filter.flavor = selectors[0].flavor; ret = contents_find_programmers(&contents, &filter, images); out_free_contents: for (flavor_idx = 0; flavor_idx < contents.num_flavors; flavor_idx++) free(contents.flavors[flavor_idx]); free(contents.flavors); list_for_each_entry_safe(entry, next, &contents.entries, node) { free(entry->filename); free(entry->flavor); free(entry); } free(selectors); return ret; } int contents_resolve_path(struct contents_filter *filter, const char *filename, struct pathbuf *path) { enum qdl_storage_type storage_type; struct contents_entry *entry; struct contents *contents; struct pathbuf probe; const char *flavor; if (!filter) return 0; contents = filter->contents; storage_type = filter->storage_type; flavor = filter->flavor; /* Look for a match */ list_for_each_entry(entry, &contents->entries, node) { if (entry->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != storage_type) { continue; } if (entry->flavor && flavor && strcmp(entry->flavor, flavor)) { continue; } if (strcmp(entry->filename, filename)) continue; qdl_pathbuf_dup(path, &entry->path); return 1; } /* * fh_loader adds all applicable to the search path, to find * files adjacent to those described in the contents.xml. So if we * didn't find an exact match, search adjacent to all other files... */ list_for_each_entry(entry, &contents->entries, node) { if (entry->storage_type != QDL_STORAGE_UNKNOWN && entry->storage_type != storage_type) { continue; } if (entry->flavor && flavor && strcmp(entry->flavor, flavor)) { continue; } qdl_pathbuf_dup(&probe, &entry->path); qdl_pathbuf_dirname(&probe); qdl_pathbuf_push(&probe, filename); if (!access(probe.buf, F_OK)) { qdl_pathbuf_dup(path, &probe); return 1; } } return 0; } linux-msm-qdl-6e625aa/src/contents.h000066400000000000000000000012041527206113100174240ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __CONTENTS_H__ #define __CONTENTS_H__ #include #include #include "list.h" struct sahara_image; struct pathbuf; struct contents_filter; int contents_load(struct list_head *ops, const char *filename, char *specifier, struct sahara_image *images, const char *incdir); int contents_load_programmers(const char *filename, char *specifier, struct sahara_image *images); int contents_resolve_path(struct contents_filter *filter, const char *filename, struct pathbuf *path); #endif linux-msm-qdl-6e625aa/src/file.c000066400000000000000000000064511527206113100165120ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #include #include #include #include "oscompat.h" #include "qdl.h" #include "file.h" int qdl_file_open(struct qdl_zip *qdl_zip, const char *filename, struct qdl_file *file) { off_t len; int fd; if (qdl_zip) return qdl_zip_file_open(qdl_zip, filename, file); fd = open(filename, O_RDONLY | O_BINARY); if (fd < 0) { ux_err("failed to open \"%s\" for reading\n", filename); return -1; } len = lseek(fd, 0, SEEK_END); if (len < 0) { ux_err("failed to find end of \"%s\"\n", filename); close(fd); return -1; } lseek(fd, 0, SEEK_SET); file->type = QDL_FILE_TYPE_POSIX; file->fd = fd; file->size = len; file->zip_file = NULL; return 0; } void *qdl_file_load(struct qdl_file *file, size_t *len) { ssize_t n; void *buf; buf = malloc(file->size); if (!buf) { ux_err("unable to allocate memory to load file\n"); return NULL; } switch (file->type) { case QDL_FILE_TYPE_UNKNOWN: ux_err("internal error: attempting to load unknown file type\n"); return NULL; case QDL_FILE_TYPE_POSIX: n = read(file->fd, buf, file->size); if ((size_t)n != file->size) { ux_err("failed to load content of file\n"); goto err_free_buf; } break; case QDL_FILE_TYPE_ZIP: n = qdl_zip_file_read(file, buf, file->size); if ((size_t)n != file->size) { ux_err("failed to load zip file member\n"); goto err_free_buf; } break; }; *len = file->size; return buf; err_free_buf: free(buf); return NULL; } void qdl_file_close(struct qdl_file *file) { switch (file->type) { case QDL_FILE_TYPE_UNKNOWN: break; case QDL_FILE_TYPE_POSIX: close(file->fd); file->fd = -1; break; case QDL_FILE_TYPE_ZIP: qdl_zip_file_close(file); break; }; file->type = QDL_FILE_TYPE_UNKNOWN; } size_t qdl_file_getsize(struct qdl_file *file) { return file->size; } ssize_t qdl_file_read(struct qdl_file *file, void *buf, size_t len) { switch (file->type) { case QDL_FILE_TYPE_UNKNOWN: break; case QDL_FILE_TYPE_POSIX: return read(file->fd, buf, len); case QDL_FILE_TYPE_ZIP: return qdl_zip_file_read(file, buf, len); }; return -1; } /* * Loop around qdl_file_read() until @len bytes have been read into * @buf. Returns the number of bytes read, which equals @len for full * reads and is less than @len only on EOF; negative on a read error. * * Wraps the underlying read() / zip_fread() so callers that don't * tolerate short reads don't have to roll their own loop. The bytes * past the returned length are left untouched - callers that need a * fixed-size buffer must zero them themselves. */ ssize_t qdl_file_read_exact(struct qdl_file *file, void *buf, size_t len) { uint8_t *p = buf; size_t got = 0; ssize_t n; while (got < len) { n = qdl_file_read(file, p + got, len - got); if (n < 0) return -1; if (n == 0) break; got += (size_t)n; } return (ssize_t)got; } off_t qdl_file_seek(struct qdl_file *file, off_t offset, int whence) { switch (file->type) { case QDL_FILE_TYPE_UNKNOWN: return -1; case QDL_FILE_TYPE_POSIX: return lseek(file->fd, offset, whence); case QDL_FILE_TYPE_ZIP: if (offset == 0 && whence == SEEK_SET) return 0; ux_err("seek not implemented for zip files\n"); return -1; }; return -1; } linux-msm-qdl-6e625aa/src/file.h000066400000000000000000000030511527206113100165100ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __QDL_FILE_H__ #define __QDL_FILE_H__ #include #include struct zip_file; enum qdl_file_type { QDL_FILE_TYPE_UNKNOWN, QDL_FILE_TYPE_POSIX, QDL_FILE_TYPE_ZIP, }; struct qdl_file { enum qdl_file_type type; size_t size; int fd; struct zip_file *zip_file; }; struct qdl_zip; int qdl_file_open(struct qdl_zip *qzip, const char *filename, struct qdl_file *file); void *qdl_file_load(struct qdl_file *file, size_t *len); void qdl_file_close(struct qdl_file *file); size_t qdl_file_getsize(struct qdl_file *file); ssize_t qdl_file_read(struct qdl_file *file, void *buf, size_t len); ssize_t qdl_file_read_exact(struct qdl_file *file, void *buf, size_t len); off_t qdl_file_seek(struct qdl_file *file, off_t offset, int whence); /* * Implemented by file_zip.c, backed by libzip. Builds without * zip-container support compile file_nozip.c instead, whose * qdl_zip_open() never produces a zip handle, leaving the * qdl_zip_file_*() stubs unreachable as no qdl_file ever becomes * QDL_FILE_TYPE_ZIP. */ bool qdl_zip_supported(void); int qdl_zip_open(const char *filename, struct qdl_zip **__qdl_zip); struct qdl_zip *qdl_zip_get(struct qdl_zip *qzip); void qdl_zip_put(struct qdl_zip *qzip); int qdl_zip_file_open(struct qdl_zip *qzip, const char *filename, struct qdl_file *file); ssize_t qdl_zip_file_read(struct qdl_file *file, void *buf, size_t len); void qdl_zip_file_close(struct qdl_file *file); #endif linux-msm-qdl-6e625aa/src/file_nozip.c000066400000000000000000000027061527206113100177300ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #include "qdl.h" #include "file.h" #include "flashmap.h" /* * Built without zip-container support: qdl_zip_open() reports "not a * zip archive" for every input, so the plain contents.xml and * flashmap.json flows work unchanged and no zip handle ever exists. * The qdl_zip_file_*() helpers below are consequently unreachable, as * no qdl_file ever becomes QDL_FILE_TYPE_ZIP. */ bool qdl_zip_supported(void) { return false; } int qdl_zip_open(const char *filename, struct qdl_zip **__qdl_zip) { (void)filename; *__qdl_zip = NULL; return 0; } struct qdl_zip *qdl_zip_get(struct qdl_zip *qdl_zip) { return qdl_zip; } void qdl_zip_put(struct qdl_zip *qdl_zip) { (void)qdl_zip; } int qdl_zip_file_open(struct qdl_zip *qdl_zip, const char *filename, struct qdl_file *file) { (void)qdl_zip; (void)filename; (void)file; return -1; } ssize_t qdl_zip_file_read(struct qdl_file *file, void *buf, size_t len) { (void)file; (void)buf; (void)len; return -1; } void qdl_zip_file_close(struct qdl_file *file) { (void)file; } /* * zipper.c is only built with zip-container support; create-zip checks * qdl_zip_supported() and reports the missing feature before getting * here. */ int zipper_write(const char *filename, struct list_head *ops, struct sahara_image *images) { (void)filename; (void)ops; (void)images; return -1; } linux-msm-qdl-6e625aa/src/file_zip.c000066400000000000000000000035001527206113100173640ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #include #include #include "qdl.h" #include "file.h" struct qdl_zip { zip_t *zip; unsigned int refcount; }; bool qdl_zip_supported(void) { return true; } int qdl_zip_file_open(struct qdl_zip *qdl_zip, const char *filename, struct qdl_file *file) { struct zip_stat st; zip_int64_t idx; zip_file_t *zf; zip_t *zip = qdl_zip->zip; idx = zip_name_locate(zip, filename, 0); if (idx < 0) { ux_err("unable to locate \"%s\" in zip archive\n", filename); return -1; } if (zip_stat_index(zip, idx, 0, &st) < 0) { ux_err("unable to stat \"%s\" in zip archive\n", filename); return -1; } zf = zip_fopen_index(zip, idx, 0); if (!zf) { ux_err("unable to open \"%s\" in zip archive\n", filename); return -1; } file->type = QDL_FILE_TYPE_ZIP; file->fd = -1; file->size = st.size; file->zip_file = zf; return 0; } ssize_t qdl_zip_file_read(struct qdl_file *file, void *buf, size_t len) { return zip_fread(file->zip_file, buf, len); } void qdl_zip_file_close(struct qdl_file *file) { zip_fclose(file->zip_file); file->zip_file = NULL; } int qdl_zip_open(const char *filename, struct qdl_zip **__qdl_zip) { struct qdl_zip *qdl_zip; zip_t *zip; zip = zip_open(filename, ZIP_RDONLY, NULL); if (!zip) { *__qdl_zip = NULL; return 0; } qdl_zip = calloc(1, sizeof(*qdl_zip)); if (!qdl_zip) { zip_close(zip); return -1; } qdl_zip->zip = zip; qdl_zip->refcount = 1; *__qdl_zip = qdl_zip; return 0; } struct qdl_zip *qdl_zip_get(struct qdl_zip *qdl_zip) { if (qdl_zip) qdl_zip->refcount++; return qdl_zip; } void qdl_zip_put(struct qdl_zip *qdl_zip) { if (!qdl_zip) return; if (--qdl_zip->refcount == 0) { zip_close(qdl_zip->zip); free(qdl_zip); } } linux-msm-qdl-6e625aa/src/firehose.c000066400000000000000000001711471527206113100174040ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2016-2017, Linaro Ltd. * Copyright (c) 2018, The Linux Foundation. All rights reserved. * All rights reserved. */ #include "list.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "qdl.h" #include "file.h" #include "firehose.h" #include "sha2.h" #include "ufs.h" #include "oscompat.h" #include "vip.h" #include "sparse.h" #include "gpt.h" #include "json.h" enum { FIREHOSE_ACK = 0, FIREHOSE_NAK, }; /* * Timeouts (in milliseconds) for the UFS provisioning tags. The common and * body tags only stage descriptors in the programmer, so a short wait is * enough. The epilogue with commit=1 makes the programmer write the UFS * configuration descriptor to the device, which can take considerably * longer than the previous 5s window - matching the write-back timeout used * by the program/erase paths avoids spuriously reporting a failure while the * device is still provisioning. */ #define FIREHOSE_UFS_TAG_TIMEOUT_MS 5000 #define FIREHOSE_UFS_COMMIT_TIMEOUT_MS 120000 /* * Substring emitted by the Firehose programmer's startup logs when VIP is * active on-device (e.g. "INFO: VIP is enabled, receiving the signed table * of size 8192"). Matching a stable prefix avoids coupling to the trailing * byte count, which varies per programmer build. */ #define VIP_PROGRAMMER_MARKER "VIP is enabled, receiving the signed table" static void firehose_check_vip_marker(struct qdl_device *qdl, xmlNode *node) { xmlChar *value; if (qdl->vip_data.programmer_requires_vip) return; if (xmlStrcmp(node->name, (xmlChar *)"log") != 0) return; value = xmlGetProp(node, (xmlChar *)"value"); if (!value) return; if (strstr((const char *)value, VIP_PROGRAMMER_MARKER)) qdl->vip_data.programmer_requires_vip = true; xmlFree(value); } static void xml_setpropf(xmlNode *node, const char *attr, const char *fmt, ...) { xmlChar buf[128]; va_list ap; va_start(ap, fmt); vsnprintf((char *)buf, sizeof(buf), fmt, ap); xmlSetProp(node, (xmlChar *)attr, buf); va_end(ap); } /* * Trace an XML message on a single debug line. libxml2 dumps documents * with a newline after the declaration and at the end, and some devices * terminate their responses the same way, which would otherwise split * the trace across lines and leave blank ones between messages. */ static void firehose_debug_xml(bool tx, const char *xml) { const char *in; char *copy; char *out; if (!qdl_debug) return; copy = strdup(xml); if (!copy) return; for (in = copy, out = copy; *in; in++) if (*in != '\n' && *in != '\r') *out++ = *in; *out = '\0'; ux_debug_tag(UX_TAG_FIREHOSE, "%s %s\n", ux_direction(tx), copy); free(copy); } static xmlNode *firehose_response_parse(const void *buf, size_t len, int *error) { xmlNode *node; xmlNode *root; xmlDoc *doc; doc = xmlReadMemory(buf, len, NULL, NULL, 0); if (!doc) { ux_err("failed to parse firehose response\n"); *error = -EINVAL; return NULL; } root = xmlDocGetRootElement(doc); for (node = root; node; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (xmlStrcmp(node->name, (xmlChar *)"data") == 0) break; } if (!node) { ux_err("firehose response without data tag\n"); *error = -EINVAL; xmlFreeDoc(doc); return NULL; } for (node = node->children; node && node->type != XML_ELEMENT_NODE; node = node->next) ; if (!node) { ux_err("empty firehose response\n"); *error = -EINVAL; } return node; } static int firehose_generic_parser(xmlNode *node, void *data __unused, bool *rawmode) { xmlChar *value; int ret = -EINVAL; value = xmlGetProp(node, (xmlChar *)"value"); if (!value) return -EINVAL; if (xmlStrcmp(node->name, (xmlChar *)"log") == 0) { ux_log_tag(UX_TAG_FIREHOSE, "LOG: %s\n", value); ret = -EAGAIN; } else if (xmlStrcmp(value, (xmlChar *)"ACK") == 0) { ret = FIREHOSE_ACK; } else if (xmlStrcmp(value, (xmlChar *)"NAK") == 0) { ret = FIREHOSE_NAK; } xmlFree(value); value = xmlGetProp(node, (xmlChar *)"rawmode"); if (value) { if (xmlStrcmp(value, (xmlChar *)"true") == 0) *rawmode = true; xmlFree(value); } return ret; } /* * Scan @str for a contiguous run of exactly SHA256_DIGEST_STRING_LENGTH-1 * (64) lowercase or uppercase hex characters bounded by a non-hex character * or string boundary. On match, decode into @digest and return true. */ static bool extract_sha256_hex(const char *str, uint8_t digest[SHA256_DIGEST_LENGTH]) { const size_t hex_len = SHA256_DIGEST_LENGTH * 2; const char *p = str; size_t i; while (*p) { size_t run = 0; const char *start; while (*p && !isxdigit((unsigned char)*p)) p++; start = p; while (isxdigit((unsigned char)*p)) { run++; p++; } if (run == hex_len) { for (i = 0; i < SHA256_DIGEST_LENGTH; i++) { char byte[3] = { start[i * 2], start[i * 2 + 1], '\0' }; digest[i] = (uint8_t)strtoul(byte, NULL, 16); } return true; } } return false; } static int firehose_sha256_parser(xmlNode *node, void *data, bool *rawmode) { struct firehose_op *op = data; xmlChar *value; int ret; if (xmlStrcmp(node->name, (xmlChar *)"log") == 0) { value = xmlGetProp(node, (xmlChar *)"value"); if (!value) return -EINVAL; if (!op->digest_valid && extract_sha256_hex((const char *)value, op->digest)) op->digest_valid = true; ux_log_tag(UX_TAG_FIREHOSE, "LOG: %s\n", value); xmlFree(value); return -EAGAIN; } ret = firehose_generic_parser(node, NULL, rawmode); /* * Some Firehose implementations attach the digest as an attribute on * the response itself rather than emitting it via . Try a few * known attribute names. */ if (!op->digest_valid) { static const char * const attrs[] = { "Digest", "SHA256", "sha256" }; size_t i; for (i = 0; i < ARRAY_SIZE(attrs); i++) { value = xmlGetProp(node, (xmlChar *)attrs[i]); if (!value) continue; if (extract_sha256_hex((const char *)value, op->digest)) op->digest_valid = true; xmlFree(value); if (op->digest_valid) break; } } return ret; } static int firehose_read(struct qdl_device *qdl, int timeout_ms, int (*response_parser)(xmlNode *node, void *data, bool *rawmode), void *data) { char buf[4096]; xmlNode *node; int error; int resp = -EIO; int ret = -EAGAIN; int n; bool rawmode = false; struct timeval timeout; struct timeval now; struct timeval delta = { .tv_sec = timeout_ms / 1000, .tv_usec = (timeout_ms % 1000) * 1000 }; gettimeofday(&now, NULL); timeradd(&now, &delta, &timeout); /* * The goal of firehose_read() is to find a response to a request among * one or more incoming messages AND to consume all incoming messages * (otherwise subsequent writes will time out). * The messages can be one of: * - * - * - * * Generally messages are coming prior to the , but * on MSM8916 (at least) it's been observed that messages can * arrive after the . * * We therefor need to consume messages until there are no more * (timeout) and we have been able to parse out a response (using * @response_parser). * * In the special case that the contain an attribute * "rawmode=true", the device signals that it has entered a mode where * it will not send/receive XML-formatted commands. So, (at least for * reads) we need to shortcircuit the logic and directly terminate the * consumption of incoming data. */ for (;;) { /* Reserve one byte for the NUL terminator written below. */ n = qdl_read(qdl, buf, sizeof(buf) - 1, 100); /* Timeout after seeing a response, we're done waiting for logs */ if (n == -ETIMEDOUT && resp >= 0) break; /* We want to return resp on error, to not lose the reset response */ else if (n == -EIO || n == -ENODEV) { if (resp < 0) resp = n; break; } if (n == -ETIMEDOUT || n == 0) { gettimeofday(&now, NULL); if (timercmp(&now, &timeout, <)) continue; return -ETIMEDOUT; } buf[n] = '\0'; firehose_debug_xml(false, buf); /* * On stream-oriented transports (Windows COM port via the * QDLoader driver, virtio-console, ...) a single read can * deliver multiple back-to-back Firehose responses * concatenated, since the driver doesn't preserve USB bulk- * transfer boundaries. Walk the buffer using the "" envelope to bound each message; the closing tag * is what really delimits the document so that any rawmode * binary payload that arrives spliced onto the same read * doesn't end up fed into libxml2 as if it were XML. * * libusb preserves transfer boundaries, so on that path each * read still contains exactly one document and the loop runs * once. */ char *cursor = buf; char *bufend = buf + n; while (cursor < bufend) { char *start = strstr(cursor, " tag. If it's * missing the message was either truncated or doesn't * fit the schema we know how to parse; hand the rest * of the buffer to libxml2 and let it error out * gracefully. */ xml_end = strstr(start, ""); if (xml_end) { xml_end += sizeof("") - 1; chunk = (size_t)(xml_end - start); } else { chunk = (size_t)(bufend - start); } node = firehose_response_parse(start, chunk, &error); if (!node) return error; firehose_check_vip_marker(qdl, node); ret = response_parser(node, data, &rawmode); xmlFreeDoc(node->doc); if (ret >= 0) resp = ret; cursor = start + chunk; /* * The response we just parsed told the host to switch * to raw mode (e.g. the ACK that precedes the binary * sectors of a ). On a stream transport the * first chunk of that binary payload can have arrived * tacked onto this same read. Push it back so the * next qdl_read() picks it up before the transport * is touched again. */ if (rawmode) { if (cursor < bufend) qdl_push_back(qdl, cursor, (size_t)(bufend - cursor)); break; } } if (rawmode) break; } return resp; } static int firehose_vip_send_table(struct qdl_device *qdl) { int ret; ret = vip_transfer_handle_tables(qdl); if (ret) { ux_err_tag(UX_TAG_VIP, "error occurred during VIP table transmission\n"); return -1; } if (!vip_transfer_status_check_needed(qdl)) return 0; ret = firehose_read(qdl, 30000, firehose_generic_parser, NULL); if (ret) { ux_err_tag(UX_TAG_VIP, "sending of digest table failed\n"); return -1; } ux_info_tag(UX_TAG_VIP, "digest table has been sent successfully\n"); vip_transfer_clear_status(qdl); return 0; } static int firehose_write(struct qdl_device *qdl, xmlDoc *doc) { xmlChar *s; int len; int ret; xmlDocDumpMemory(doc, &s, &len); ret = firehose_vip_send_table(qdl); if (ret) { xmlFree(s); return -1; } vip_gen_chunk_init(qdl); for (;;) { firehose_debug_xml(true, (const char *)s); vip_gen_chunk_update(qdl, s, len); ret = qdl_write(qdl, s, len, 1000); /* * db410c sometimes sends a followed by * entries and won't accept write commands until these are * drained, so attempt to read any pending data and then retry * the write. The qdl_write() backends report the timeout via * the return value, not errno, so key the retry on that. */ if (ret == -ETIMEDOUT) { firehose_read(qdl, 100, firehose_generic_parser, NULL); continue; } break; } xmlFree(s); vip_gen_chunk_store(qdl); return ret < 0 ? ret : 0; } /** * firehose_configure_response_parser() - parse a configure response * @node: response xmlNode * * Return: max size supported by the remote, or negative errno on failure */ static int firehose_configure_response_parser(xmlNode *node, void *data, bool *rawmode __unused) { xmlChar *payload; xmlChar *value; size_t max_size; value = xmlGetProp(node, (xmlChar *)"value"); if (!value) return -EINVAL; if (xmlStrcmp(node->name, (xmlChar *)"log") == 0) { ux_log_tag(UX_TAG_FIREHOSE, "LOG: %s\n", value); xmlFree(value); return -EAGAIN; } payload = xmlGetProp(node, (xmlChar *)"MaxPayloadSizeToTargetInBytes"); if (!payload) { xmlFree(value); return -EINVAL; } max_size = strtoul((char *)payload, NULL, 10); xmlFree(payload); /* * When receiving an ACK the remote may indicate that we should attempt * a larger payload size */ if (!xmlStrcmp(value, (xmlChar *)"ACK")) { payload = xmlGetProp(node, (xmlChar *)"MaxPayloadSizeToTargetInBytesSupported"); if (!payload) { xmlFree(value); return -EINVAL; } max_size = strtoul((char *)payload, NULL, 10); xmlFree(payload); } *(size_t *)data = max_size; xmlFree(value); return FIREHOSE_ACK; } static int firehose_send_configure(struct qdl_device *qdl, size_t payload_size, bool skip_storage_init, enum qdl_storage_type storage, size_t *max_payload_size) { const char *memory_name; xmlNode *root; xmlNode *node; xmlDoc *doc; memory_name = encode_storage_type(storage); if (!memory_name) return -EINVAL; doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"configure", NULL); xml_setpropf(node, "MemoryName", memory_name); xml_setpropf(node, "MaxPayloadSizeToTargetInBytes", "%lu", payload_size); xml_setpropf(node, "Verbose", "%d", 0); xml_setpropf(node, "ZlpAwareHost", "%d", 1); xml_setpropf(node, "SkipStorageInit", "%d", skip_storage_init); firehose_write(qdl, doc); xmlFreeDoc(doc); return firehose_read(qdl, 100, firehose_configure_response_parser, max_payload_size); } static int firehose_try_configure(struct qdl_device *qdl, bool skip_storage_init, enum qdl_storage_type storage) { size_t max_sector_size; size_t sector_sizes[] = { 512, 4096 }; struct firehose_op op; size_t size = 0; void *buf; int ret; unsigned int i; ret = firehose_send_configure(qdl, qdl->max_payload_size, skip_storage_init, storage, &size); if (ret < 0) return ret; /* Retry if remote proposed different size */ if (size != qdl->max_payload_size) { ret = firehose_send_configure(qdl, size, skip_storage_init, storage, &size); if (ret != FIREHOSE_ACK) { ux_err("configure request with updated payload size failed\n"); return -1; } qdl->max_payload_size = size; } ux_debug_tag(UX_TAG_FIREHOSE, "accepted max payload size: %zu\n", qdl->max_payload_size); /* * Skip sector size probing when VIP is active: the probe read commands * are not included in the pre-built VIP digest table (the dry-run that * builds it exits before reaching this code via the SIM early-return * above), so sending them would cause a VIP hash mismatch on the device. * * Also skip it when the caller asked for SkipStorageInit: the probe * reads force the programmer to initialize the very storage it was * told not to touch. On an unprovisioned UFS device the resulting * failed open can leave the programmer's storage stack in a state * where the provisioning commit later fails to open the device * well-known LUN. Provisioning has no use for the sector size. */ if (!skip_storage_init && storage != QDL_STORAGE_NAND && qdl->vip_data.state == VIP_DISABLED && !qdl->sector_size) { max_sector_size = sector_sizes[ARRAY_SIZE(sector_sizes) - 1]; buf = alloca(max_sector_size); memset(&op, 0, sizeof(op)); op.partition = 0; op.start_sector = "1"; op.num_sectors = 1; /* * Testing has shown that the loader will fail gracefully if a * read is issued with the wrong sector size, use this to attempt * to discover the storage device's sector size. */ for (i = 0; i < ARRAY_SIZE(sector_sizes); i++) { op.sector_size = sector_sizes[i]; ret = firehose_read_buf(qdl, &op, buf, max_sector_size); if (ret == 0) { qdl->sector_size = sector_sizes[i]; break; } } } if (qdl->sector_size) ux_debug_tag(UX_TAG_FIREHOSE, "detected sector size of: %zd\n", qdl->sector_size); return 0; } /* Totals of a firehose_run(), reported in its summary line */ static struct { unsigned int flashed; unsigned int skipped; unsigned int read; unsigned int erased; unsigned int patches; uint64_t bytes_written; } firehose_stats; static int firehose_erase(struct qdl_device *qdl, struct firehose_op *program) { unsigned int sector_size; xmlNode *root; xmlNode *node; xmlDoc *doc; int ret; sector_size = program->sector_size ? : qdl->sector_size; doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"erase", NULL); xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%d", sector_size); xml_setpropf(node, "physical_partition_number", "%d", program->partition); /* * Omitting num_sectors and start_sector attributes tells the programmer * to erase the full physical partition. */ if (program->num_sectors > 0) { xml_setpropf(node, "num_partition_sectors", "%d", program->num_sectors); xml_setpropf(node, "start_sector", "%s", program->start_sector); } if (qdl->slot != UINT_MAX) { xml_setpropf(node, "slot", "%u", qdl->slot); } if (program->is_nand) { xml_setpropf(node, "PAGES_PER_BLOCK", "%d", program->pages_per_block); } ret = firehose_write(qdl, doc); if (ret < 0) { ux_err("failed to send program request\n"); goto out; } ret = firehose_read(qdl, 30000, firehose_generic_parser, NULL); if (ret) ux_err("failed to erase %s+0x%x\n", program->start_sector, program->num_sectors); else { ux_info_tag(UX_TAG_FIREHOSE, "successfully erased %s+0x%x\n", program->start_sector, program->num_sectors); firehose_stats.erased++; } out: xmlFreeDoc(doc); return ret == FIREHOSE_ACK ? 0 : -1; } static int firehose_getsha256digest(struct qdl_device *qdl, struct firehose_op *op); /* * Skipblock compares the bytes that would be written against the digest * the device reports for the same flash region and, on a match, avoids * rewriting them. Rather than digest the whole region in one * - which on a multi-GB partition like rootfs reads the * entire region on the device and easily blows past * firehose_getsha256digest()'s read timeout - the region is walked one * bounded chunk at a time. Each chunk is hashed locally, compared against * the device digest for that sub-region, and reflashed on its own only when * it differs. * * Restricted to non-sparse, non-NAND programs with VIP disabled: * - Sparse chunks would each need their own digest; v1 keeps them on * the normal program path. * - NAND has spare/OOB bytes whose semantics differ across * programmers. * - is not part of pre-built VIP digest tables. */ #define SKIPBLOCK_CHUNK_BYTES (512ULL * 1024 * 1024) /* 512 MiB */ static bool firehose_skipblock_enabled(struct qdl_device *qdl, struct firehose_op *program) { return qdl->skipblock_mode == QDL_SKIPBLOCK_SHA256 && !program->sparse && !program->is_nand && qdl->vip_data.state == VIP_DISABLED; } /* * SHA-256 the @region_bytes of @file starting at @file_byte_off into @out. * Mirrors the program path's trailing zero-pad (see the memset() of the * residue in firehose_program_raw_region()): bytes past EOF are hashed as * zeros so a chunk that is short, or read from a non-zero file offset, * still produces a digest that can match flash. @buf is caller-owned * scratch of qdl->max_payload_size. Returns -1 on read error. */ static int firehose_region_local_digest(struct qdl_device *qdl, struct qdl_file *file, off_t file_byte_off, size_t region_bytes, void *buf, uint8_t out[SHA256_DIGEST_LENGTH]) { size_t hashed = 0; SHA2_CTX ctx; ssize_t hn; SHA256Init(&ctx); qdl_file_seek(file, file_byte_off, SEEK_SET); while (hashed < region_bytes) { size_t want = MIN(qdl->max_payload_size, region_bytes - hashed); hn = qdl_file_read_exact(file, buf, want); if (hn < 0) return -1; if (hn > 0) { SHA256Update(&ctx, buf, hn); hashed += (size_t)hn; } if ((size_t)hn < want) break; /* short read == EOF, remainder is zero-pad */ } if (hashed < region_bytes) { memset(buf, 0, qdl->max_payload_size); while (hashed < region_bytes) { size_t pad = MIN(qdl->max_payload_size, region_bytes - hashed); SHA256Update(&ctx, buf, pad); hashed += pad; } } SHA256Final(out, &ctx); return 0; } /* * Stream @num_sectors of @sector_size bytes to the device after a program * request has been accepted. When @fill is set the data is already present in * @buf (a repeating fill pattern) and reused for every chunk; otherwise each * chunk is read from @file and the trailing partial sector is zero-padded. The * per-chunk VIP digest hooks and progress reporting are handled here so the * regular, sparse and skipblock program paths share one implementation. */ static int firehose_stream_out(struct qdl_device *qdl, struct firehose_op *program, struct qdl_file *file, unsigned int sector_size, size_t num_sectors, void *buf, bool fill, unsigned int zlp_timeout) { size_t chunk_size; size_t left = num_sectors; int ret; int n; while (left > 0) { /* * The hash is computed per raw packet sent, not over the whole * binary. */ vip_gen_chunk_init(qdl); chunk_size = MIN(qdl->max_payload_size / sector_size, left); if (!fill) { n = qdl_file_read_exact(file, buf, chunk_size * sector_size); if (n < 0) { ux_err("failed to read %s\n", program->filename); return -1; } /* * qdl_file_read_exact() only returns short on true EOF. * The wire protocol expects exactly chunk_size * * sector_size bytes, so zero-pad the residue (at most the * trailing partial sector of the file). */ if ((size_t)n < chunk_size * sector_size) memset((char *)buf + n, 0, chunk_size * sector_size - n); } vip_gen_chunk_update(qdl, buf, chunk_size * sector_size); ret = firehose_vip_send_table(qdl); if (ret) return -1; n = qdl_write(qdl, buf, chunk_size * sector_size, zlp_timeout); if (n < 0) { ux_err("USB write failed for data chunk\n"); ret = firehose_read(qdl, 30000, firehose_generic_parser, NULL); if (ret) ux_err("flashing of chunk failed\n"); return -1; } if ((size_t)n != chunk_size * sector_size) { ux_err("USB write truncated\n"); return -1; } left -= chunk_size; vip_gen_chunk_store(qdl); ux_progress("%s", num_sectors - left, num_sectors, program->label); } return 0; } /* Milliseconds from an arbitrary epoch, for measuring transfer rates */ static uint64_t firehose_now_ms(void) { struct timeval tv; gettimeofday(&tv, NULL); return (uint64_t)tv.tv_sec * 1000 + tv.tv_usec / 1000; } /* Format ", " for @bytes moved in @ms, in KiB/s below 1 MiB/s */ static void firehose_format_rate(char *buf, size_t len, uint64_t bytes, uint64_t ms) { double kib = bytes / 1024.0 / (ms / 1000.0); if (kib < 1024) snprintf(buf, len, ", %.1f KiB/s", kib); else snprintf(buf, len, ", %.1f MiB/s", kib / 1024); } /* * Report the outcome of an operation on @name: its size, the rate when * measurable and an optional @note. */ static void firehose_report(enum ux_status status, const char *verb, const char *name, uint64_t bytes, uint64_t ms, const char *note) { char size[16]; char rate[32] = ""; ux_format_size(size, sizeof(size), bytes); if (ms >= 500) firehose_format_rate(rate, sizeof(rate), bytes, ms); ux_status(UX_TAG_FIREHOSE, status, verb, name, "%s (%s%s%s%s)", status == UX_STATUS_DONE ? " successfully" : "", size, rate, note ? ", " : "", note ? note : ""); } /* * Program the contiguous raw region [@start_sector, @start_sector + * @num_sectors) from the current position of @file: send the program * request, stream the data through firehose_stream_out() and consume the * final response. Used by the skipblock fast-path to reflash one * sub-region at a time; skipblock requires VIP disabled, so the VIP * hooks inside the streaming loop are no-ops on this path. @file and * @buf (scratch of qdl->max_payload_size) are owned by the caller. */ static int firehose_program_raw_region(struct qdl_device *qdl, struct firehose_op *program, struct qdl_file *file, const char *start_sector, unsigned int num_sectors, unsigned int sector_size, void *buf, unsigned int zlp_timeout) { xmlNode *root; xmlNode *node; xmlDoc *doc; int ret; doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"program", NULL); xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%d", sector_size); xml_setpropf(node, "num_partition_sectors", "%d", num_sectors); xml_setpropf(node, "physical_partition_number", "%d", program->partition); xml_setpropf(node, "start_sector", "%s", start_sector); if (qdl->slot != UINT_MAX) xml_setpropf(node, "slot", "%u", qdl->slot); if (program->filename) xml_setpropf(node, "filename", "%s", program->filename); ret = firehose_write(qdl, doc); if (ret < 0) { ux_err("failed to send program request\n"); goto out; } ret = firehose_read(qdl, 10000, firehose_generic_parser, NULL); if (ret) { ux_err("failed to setup programming\n"); ret = -1; goto out; } ret = firehose_stream_out(qdl, program, file, sector_size, num_sectors, buf, false, zlp_timeout); if (ret) goto out; ret = firehose_read(qdl, 120000, firehose_generic_parser, NULL); if (ret != FIREHOSE_ACK) { ux_err("flashing of %s failed\n", program->label); ret = -1; goto out; } ret = 0; out: xmlFreeDoc(doc); return ret; } /* * Walk the @program region in SKIPBLOCK_CHUNK_BYTES chunks. For each chunk * compare the locally computed digest against the device's digest for the * matching flash sub-region; flash just that chunk when they differ, leave * it untouched when they match. Returns 0 on success, -1 on a flashing * failure. @buf is caller-owned scratch of qdl->max_payload_size. */ static int firehose_program_skipblock(struct qdl_device *qdl, struct firehose_op *program, struct qdl_file *file, unsigned int num_sectors, unsigned int sector_size, void *buf, unsigned int zlp_timeout) { unsigned int chunk_max = SKIPBLOCK_CHUNK_BYTES / sector_size; uint64_t base = strtoull(program->start_sector, NULL, 0); unsigned int nchunks = num_sectors / chunk_max + !!(num_sectors % chunk_max); bool split = nchunks > 1; unsigned int flashed = 0; uint64_t flashed_bytes = 0; uint64_t flash_ms = 0; unsigned int idx = 0; unsigned int off; char note[48]; uint64_t t0; for (off = 0; off < num_sectors; off += chunk_max, idx++) { unsigned int chunk_sectors = MIN(chunk_max, num_sectors - off); size_t region_bytes = (size_t)chunk_sectors * sector_size; off_t file_byte_off = (off_t)(program->file_offset + off) * sector_size; uint8_t local_digest[SHA256_DIGEST_LENGTH]; struct firehose_op digest_op; const char *chunk_start; char start_sector[24]; char chunk_id[32]; bool match = false; /* * start_sector is a firehose expression that may be symbolic * (e.g. "NUM_DISK_SECTORS-33."), so the first chunk must reuse * the original string verbatim - parsing it would mis-address * the write. Only subsequent chunks of a split region need an * offset, and those regions always carry a numeric start. */ if (off == 0) { chunk_start = program->start_sector; } else { snprintf(start_sector, sizeof(start_sector), "%llu", (unsigned long long)(base + off)); chunk_start = start_sector; } /* * A region that fits in a single chunk keeps the original * unqualified wording; only split regions name the chunk. */ if (split) snprintf(chunk_id, sizeof(chunk_id), " chunk %u of %u", idx + 1, nchunks); else chunk_id[0] = '\0'; ux_progress("%s", idx, nchunks, program->label); ux_debug_tag(UX_TAG_FIREHOSE, "hashing \"%s\"%s locally (%zu KiB)...\n", program->label, chunk_id, region_bytes >> 10); if (firehose_region_local_digest(qdl, file, file_byte_off, region_bytes, buf, local_digest) == 0) { digest_op = (struct firehose_op){ .type = FIREHOSE_OP_GET_SHA256_DIGEST, .sector_size = sector_size, .num_sectors = chunk_sectors, .partition = program->partition, .start_sector = chunk_start, }; ux_debug_tag(UX_TAG_FIREHOSE, "requesting flash digest for \"%s\"%s...\n", program->label, chunk_id); if (firehose_getsha256digest(qdl, &digest_op) == 0 && !memcmp(local_digest, digest_op.digest, SHA256_DIGEST_LENGTH)) match = true; } if (match) { ux_debug_tag(UX_TAG_FIREHOSE, "sha256 match for \"%s\"%s, skipping\n", program->label, chunk_id); continue; } ux_debug_tag(UX_TAG_FIREHOSE, "sha256 mismatch for \"%s\"%s, flashing\n", program->label, chunk_id); t0 = firehose_now_ms(); qdl_file_seek(file, file_byte_off, SEEK_SET); if (firehose_program_raw_region(qdl, program, file, chunk_start, chunk_sectors, sector_size, buf, zlp_timeout) < 0) return -1; flash_ms += firehose_now_ms() - t0; flashed_bytes += region_bytes; flashed++; } /* One line per region, the per-chunk detail is in the debug log */ if (!flashed) { if (split) snprintf(note, sizeof(note), "%u chunks, sha256 match", nchunks); else snprintf(note, sizeof(note), "sha256 match"); firehose_report(UX_STATUS_SKIPPED, "skipped", program->label, (uint64_t)num_sectors * sector_size, 0, note); firehose_stats.skipped++; } else { snprintf(note, sizeof(note), "%u of %u chunks", flashed, nchunks); firehose_report(UX_STATUS_DONE, "flashed", program->label, flashed_bytes, flash_ms, split ? note : NULL); firehose_stats.flashed++; firehose_stats.bytes_written += flashed_bytes; } return 0; } static int firehose_program(struct qdl_device *qdl, struct firehose_op *program) { unsigned int num_sectors; unsigned int sector_size; unsigned int zlp_timeout = 10000; struct qdl_file file; xmlNode *root; xmlNode *node; xmlDoc *doc; void *buf; uint64_t t0; uint64_t ms; bool fill; int ret; size_t i; uint32_t fill_value; /* * ZLP has been measured to take up to 15 seconds on SPINOR devices, * let's double it to be on the safe side... */ if (qdl->current_storage_type == QDL_STORAGE_SPINOR) zlp_timeout = 60000; if (!program->filename) return 0; ret = qdl_file_open(program->zip, program->filename, &file); if (ret < 0) { ux_err("unable to open %s\n", program->filename); return -1; } num_sectors = program->num_sectors; sector_size = program->sector_size ? : qdl->sector_size; if (!sector_size) { ux_err("unable to determine sector size for %s\n", program->filename); goto err_close_fd; } if (!program->sparse) { num_sectors = (qdl_file_getsize(&file) + sector_size - 1) / sector_size; if (program->num_sectors && num_sectors > program->num_sectors) { ux_warnx("%s too big for %s, truncated to %u bytes", program->filename, program->label, program->num_sectors * sector_size); num_sectors = program->num_sectors; } } buf = malloc(qdl->max_payload_size); if (!buf) { ux_err("failed to allocate sector buffer\n"); goto err_close_fd; } if (firehose_skipblock_enabled(qdl, program)) { ret = firehose_program_skipblock(qdl, program, &file, num_sectors, sector_size, buf, zlp_timeout); free(buf); qdl_file_close(&file); return ret; } doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"program", NULL); xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%d", sector_size); xml_setpropf(node, "num_partition_sectors", "%d", num_sectors); xml_setpropf(node, "physical_partition_number", "%d", program->partition); xml_setpropf(node, "start_sector", "%s", program->start_sector); if (qdl->slot != UINT_MAX) { xml_setpropf(node, "slot", "%u", qdl->slot); } if (program->filename) xml_setpropf(node, "filename", "%s", program->filename); if (program->is_nand) { xml_setpropf(node, "PAGES_PER_BLOCK", "%d", program->pages_per_block); xml_setpropf(node, "last_sector", "%d", program->last_sector); /* * Some NAND programmers resolve the destination partition by * name to apply that partition's bad-block adjusted offset, so * start_sector alone does not describe where the data lands. * Given no label such a programmer matches no partition and * programming fails. Programmers that do not need the name * ignore the attribute. */ if (program->label) xml_setpropf(node, "label", "%s", program->label); } ret = firehose_write(qdl, doc); if (ret < 0) { ux_err("failed to send program request\n"); goto err_free_doc; } ret = firehose_read(qdl, 10000, firehose_generic_parser, NULL); if (ret) { ux_err("failed to setup programming\n"); goto err_free_doc; } t0 = firehose_now_ms(); if (!program->sparse) { qdl_file_seek(&file, (off_t)program->file_offset * sector_size, SEEK_SET); } else { switch (program->sparse_chunk_type) { case CHUNK_TYPE_RAW: qdl_file_seek(&file, program->sparse_offset, SEEK_SET); break; case CHUNK_TYPE_FILL: fill_value = program->sparse_fill_value; for (i = 0; i < qdl->max_payload_size; i += sizeof(fill_value)) memcpy(buf + i, &fill_value, sizeof(fill_value)); break; default: ux_err_tag(UX_TAG_SPARSE, "invalid chunk type\n"); goto err_free_doc; } } ux_debug_tag(UX_TAG_FIREHOSE_RAW, "%s %s, %zu bytes\n", ux_direction(true), program->filename, (size_t)sector_size * num_sectors); fill = program->sparse && program->sparse_chunk_type == CHUNK_TYPE_FILL; ret = firehose_stream_out(qdl, program, &file, sector_size, num_sectors, buf, fill, zlp_timeout); if (ret) goto err_free_doc; ret = firehose_read(qdl, 120000, firehose_generic_parser, NULL); if (ret != FIREHOSE_ACK) { ux_err("flashing of %s failed\n", program->label); ret = -1; goto err_free_doc; } ms = firehose_now_ms() - t0; firehose_report(UX_STATUS_DONE, "flashed", program->label, (uint64_t)sector_size * num_sectors, ms, NULL); firehose_stats.flashed++; firehose_stats.bytes_written += (uint64_t)sector_size * num_sectors; xmlFreeDoc(doc); free(buf); qdl_file_close(&file); return 0; err_free_doc: xmlFreeDoc(doc); free(buf); err_close_fd: qdl_file_close(&file); return -1; } static int firehose_issue_read(struct qdl_device *qdl, struct firehose_op *read_op, int fd, void *out_buf, size_t out_len, bool quiet) { unsigned int sector_size; size_t chunk_size; size_t out_offset = 0; xmlNode *root; xmlNode *node; xmlDoc *doc; void *buf; uint64_t t0; size_t left; int ret; int n; buf = malloc(qdl->max_payload_size); if (!buf) { ux_err("failed to allocate sector buffer\n"); return -1; } doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); sector_size = read_op->sector_size ? : qdl->sector_size; if (!sector_size) { ux_err("unable to determine sector size for read operation\n"); free(buf); xmlFreeDoc(doc); return -1; } node = xmlNewChild(root, NULL, (xmlChar *)"read", NULL); xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%d", sector_size); xml_setpropf(node, "num_partition_sectors", "%d", read_op->num_sectors); xml_setpropf(node, "physical_partition_number", "%d", read_op->partition); xml_setpropf(node, "start_sector", "%s", read_op->start_sector); if (qdl->slot != UINT_MAX) { xml_setpropf(node, "slot", "%u", qdl->slot); } if (read_op->filename) xml_setpropf(node, "filename", "%s", read_op->filename); ret = firehose_write(qdl, doc); if (ret < 0) { ux_err("failed to send read command\n"); goto out; } ret = firehose_read(qdl, 10000, firehose_generic_parser, NULL); if (ret) { if (!quiet) ux_err("failed to setup reading operation\n"); ret = -1; goto out; } ux_debug_tag(UX_TAG_FIREHOSE_RAW, "%s %s, %zu bytes\n", ux_direction(false), read_op->filename ? read_op->filename : "(buffer)", (size_t)sector_size * read_op->num_sectors); t0 = firehose_now_ms(); left = read_op->num_sectors; while (left > 0) { size_t want; size_t got; chunk_size = MIN(qdl->max_payload_size / sector_size, left); want = chunk_size * sector_size; /* * Accumulate the chunk across qdl_read() calls. libusb usually * delivers an entire bulk transfer in one shot, but stream * transports (QUD's Windows COM port, virtio-console, ...) can * fragment it - including the rawmode tail that firehose_read() * pushed back from the same buffer as the ACK response. */ got = 0; while (got < want) { n = qdl_read(qdl, (char *)buf + got, want - got, 30000); if (n < 0) { ux_err("failed to read sector data\n"); ret = -1; goto out; } /* * A 0-byte return is not EOF on a USB bulk-in pipe: it's * a zero-length packet. Windows libusb (WinUSB) surfaces * the ZLP that the device sends when transitioning from * the XML/ACK phase to the rawmode binary phase before * the first sector chunk of a large ; Linux's * usbfs path generally hides it. If qdl_read() really * has nothing more to deliver it will eventually return * -ETIMEDOUT, which is handled above. */ if (n == 0) continue; got += (size_t)n; } if (out_buf) { size_t copy = want; if (copy > out_len - out_offset) copy = out_len - out_offset; memcpy((char *)out_buf + out_offset, buf, copy); out_offset += copy; } else { n = write(fd, buf, want); if (n < 0 || (size_t)n != want) { ux_err("failed to write sector data\n"); ret = -1; goto out; } } left -= chunk_size; if (!quiet) ux_progress("%s", read_op->num_sectors - left, read_op->num_sectors, read_op->filename); } ret = firehose_read(qdl, 10000, firehose_generic_parser, NULL); if (ret) { ux_err("read operation failed\n"); ret = -1; goto out; } if (!quiet) { firehose_report(UX_STATUS_DONE, "read", read_op->filename, (uint64_t)sector_size * read_op->num_sectors, firehose_now_ms() - t0, NULL); firehose_stats.read++; } out: xmlFreeDoc(doc); free(buf); return ret; } int firehose_read_buf(struct qdl_device *qdl, struct firehose_op *read_op, void *out_buf, size_t out_size) { return firehose_issue_read(qdl, read_op, -1, out_buf, out_size, true); } static int firehose_read_op(struct qdl_device *qdl, struct firehose_op *op) { int ret; int fd; fd = open(op->filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0644); if (fd < 0) { ux_info_tag(UX_TAG_FIREHOSE, "unable to open %s...\n", op->filename); return -1; } ret = firehose_issue_read(qdl, op, fd, NULL, 0, false); close(fd); return ret; } /* * Issue a request and store the result on @op. On * success op->digest is populated and op->digest_valid is set; callers * that want to surface the digest (the qdl `sha256` subcommand prints * it after firehose_run() returns; the --skipblock=sha256 fast-path * compares it against a locally-computed digest) consume the field * themselves. */ static int firehose_getsha256digest(struct qdl_device *qdl, struct firehose_op *op) { unsigned int sector_size; xmlNode *root; xmlNode *node; xmlDoc *doc; int ret; sector_size = op->sector_size ? : qdl->sector_size; doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"getsha256digest", NULL); xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%d", sector_size); xml_setpropf(node, "num_partition_sectors", "%d", op->num_sectors); xml_setpropf(node, "physical_partition_number", "%d", op->partition); xml_setpropf(node, "start_sector", "%s", op->start_sector); if (qdl->slot != UINT_MAX) xml_setpropf(node, "slot", "%u", qdl->slot); op->digest_valid = false; ret = firehose_write(qdl, doc); if (ret < 0) { ux_err("failed to send getsha256digest command\n"); goto out; } ret = firehose_read(qdl, 30000, firehose_sha256_parser, op); if (ret != FIREHOSE_ACK) { ux_err("getsha256digest failed for %s+0x%x\n", op->start_sector, op->num_sectors); ret = -1; goto out; } if (!op->digest_valid) { ux_err("getsha256digest returned no digest for %s+0x%x\n", op->start_sector, op->num_sectors); ret = -1; goto out; } ret = 0; out: xmlFreeDoc(doc); return ret; } static int firehose_apply_patch(struct qdl_device *qdl, struct firehose_op *patch) { xmlNode *root; xmlNode *node; xmlDoc *doc; int ret; if (!patch->filename) return 0; if (strcmp(patch->filename, "DISK")) return 0; ux_debug_tag(UX_TAG_FIREHOSE, "applying patch \"%s\"\n", patch->what); doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"patch", NULL); xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%d", patch->sector_size); xml_setpropf(node, "byte_offset", "%d", patch->byte_offset); xml_setpropf(node, "filename", "%s", patch->filename); xml_setpropf(node, "physical_partition_number", "%d", patch->partition); xml_setpropf(node, "size_in_bytes", "%d", patch->size_in_bytes); xml_setpropf(node, "start_sector", "%s", patch->start_sector); xml_setpropf(node, "value", "%s", patch->value); if (qdl->slot != UINT_MAX) { xml_setpropf(node, "slot", "%u", qdl->slot); } ret = firehose_write(qdl, doc); if (ret < 0) goto out; ret = firehose_read(qdl, 5000, firehose_generic_parser, NULL); if (ret) ux_err("patch application failed\n"); out: xmlFreeDoc(doc); return ret == FIREHOSE_ACK ? 0 : -1; } static int firehose_send_single_tag(struct qdl_device *qdl, xmlNode *node, int timeout_ms) { xmlNode *root; xmlDoc *doc; int ret; doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); xmlAddChild(root, node); ret = firehose_write(qdl, doc); if (ret < 0) goto out; ret = firehose_read(qdl, timeout_ms, firehose_generic_parser, NULL); if (ret) { ux_err("ufs request failed\n"); ret = -EINVAL; } out: xmlFreeDoc(doc); return ret; } int firehose_apply_ufs_common(struct qdl_device *qdl, struct ufs_common *ufs) { xmlNode *node_to_send; int ret; node_to_send = xmlNewNode(NULL, (xmlChar *)"ufs"); xml_setpropf(node_to_send, "bNumberLU", "%d", ufs->bNumberLU); xml_setpropf(node_to_send, "bBootEnable", "%d", ufs->bBootEnable); xml_setpropf(node_to_send, "bDescrAccessEn", "%d", ufs->bDescrAccessEn); xml_setpropf(node_to_send, "bInitPowerMode", "%d", ufs->bInitPowerMode); xml_setpropf(node_to_send, "bHighPriorityLUN", "%d", ufs->bHighPriorityLUN); xml_setpropf(node_to_send, "bSecureRemovalType", "%d", ufs->bSecureRemovalType); xml_setpropf(node_to_send, "bInitActiveICCLevel", "%d", ufs->bInitActiveICCLevel); xml_setpropf(node_to_send, "wPeriodicRTCUpdate", "%d", ufs->wPeriodicRTCUpdate); xml_setpropf(node_to_send, "bConfigDescrLock", "%d", ufs->bConfigDescrLock); if (qdl->slot != UINT_MAX) { xml_setpropf(node_to_send, "slot", "%u", qdl->slot); } if (ufs->wb) { xml_setpropf(node_to_send, "bWriteBoosterBufferPreserveUserSpaceEn", "%d", ufs->bWriteBoosterBufferPreserveUserSpaceEn); xml_setpropf(node_to_send, "bWriteBoosterBufferType", "%d", ufs->bWriteBoosterBufferType); xml_setpropf(node_to_send, "shared_wb_buffer_size_in_kb", "%d", ufs->shared_wb_buffer_size_in_kb); } ret = firehose_send_single_tag(qdl, node_to_send, FIREHOSE_UFS_TAG_TIMEOUT_MS); if (ret) ux_err("failed to send ufs common tag\n"); return ret == FIREHOSE_ACK ? 0 : -1; } int firehose_apply_ufs_body(struct qdl_device *qdl, struct ufs_body *ufs) { xmlNode *node_to_send; int ret; node_to_send = xmlNewNode(NULL, (xmlChar *)"ufs"); xml_setpropf(node_to_send, "LUNum", "%d", ufs->LUNum); xml_setpropf(node_to_send, "bLUEnable", "%d", ufs->bLUEnable); xml_setpropf(node_to_send, "bBootLunID", "%d", ufs->bBootLunID); xml_setpropf(node_to_send, "size_in_kb", "%d", ufs->size_in_kb); xml_setpropf(node_to_send, "bDataReliability", "%d", ufs->bDataReliability); xml_setpropf(node_to_send, "bLUWriteProtect", "%d", ufs->bLUWriteProtect); xml_setpropf(node_to_send, "bMemoryType", "%d", ufs->bMemoryType); xml_setpropf(node_to_send, "bLogicalBlockSize", "%d", ufs->bLogicalBlockSize); xml_setpropf(node_to_send, "bProvisioningType", "%d", ufs->bProvisioningType); xml_setpropf(node_to_send, "wContextCapabilities", "%d", ufs->wContextCapabilities); if (qdl->slot != UINT_MAX) { xml_setpropf(node_to_send, "slot", "%u", qdl->slot); } if (ufs->desc) xml_setpropf(node_to_send, "desc", "%s", ufs->desc); ret = firehose_send_single_tag(qdl, node_to_send, FIREHOSE_UFS_TAG_TIMEOUT_MS); if (ret) ux_err("failed to apply ufs body tag\n"); return ret == FIREHOSE_ACK ? 0 : -1; } int firehose_apply_ufs_epilogue(struct qdl_device *qdl, struct ufs_epilogue *ufs, bool commit) { xmlNode *node_to_send; xmlNode *root; xmlDoc *doc; int ret; node_to_send = xmlNewNode(NULL, (xmlChar *)"ufs"); xml_setpropf(node_to_send, "LUNtoGrow", "%d", ufs->LUNtoGrow); xml_setpropf(node_to_send, "commit", "%d", commit); if (qdl->slot != UINT_MAX) { xml_setpropf(node_to_send, "slot", "%u", qdl->slot); } doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); xmlAddChild(root, node_to_send); ret = firehose_write(qdl, doc); xmlFreeDoc(doc); if (ret < 0) return -1; /* * A commit epilogue writes the UFS configuration descriptor and can * take a while; the validation pass (commit=0) is cheap. * * Some programmers reset the device immediately after committing the * UFS configuration descriptor, dropping the USB link before they can * send back an ACK. Treat that USB disconnect (-ENODEV) as a successful * completion when commit=1: if the programmer accepted all prior * descriptors without a NAK and then reset, the provisioning was * applied. A real failure (e.g. bad descriptor) would have produced a * NAK earlier in the sequence. * * Return 1 in this case to signal to the caller that the device has * already reset and no further reset should be sent. */ ret = firehose_read(qdl, commit ? FIREHOSE_UFS_COMMIT_TIMEOUT_MS : FIREHOSE_UFS_TAG_TIMEOUT_MS, firehose_generic_parser, NULL); if (ret == FIREHOSE_ACK) return 0; if (commit && ret == -ENODEV) { ux_info_tag(UX_TAG_FIREHOSE, "UFS provisioning commit: device reset without ACK, " "assuming success\n"); return 1; } ux_err("failed to apply ufs epilogue\n"); return -1; } static int firehose_set_bootable(struct qdl_device *qdl, int part) { xmlNode *root; xmlNode *node; xmlDoc *doc; int ret; doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"setbootablestoragedrive", NULL); xml_setpropf(node, "value", "%d", part); ret = firehose_write(qdl, doc); xmlFreeDoc(doc); if (ret < 0) return -1; ret = firehose_read(qdl, 5000, firehose_generic_parser, NULL); if (ret) { ux_err("failed to mark partition %d as bootable\n", part); return -1; } ux_info_tag(UX_TAG_FIREHOSE, "partition %d is now bootable\n", part); return 0; } int firehose_reset(struct qdl_device *qdl) { xmlNode *root; xmlNode *node; xmlDoc *doc; int ret; doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"power", NULL); xml_setpropf(node, "value", "reset"); xml_setpropf(node, "DelayInSeconds", "10"); // Add a delay to prevent reboot fail ret = firehose_write(qdl, doc); xmlFreeDoc(doc); if (ret < 0) return -1; ret = firehose_read(qdl, 5000, firehose_generic_parser, NULL); if (ret < 0) ux_err("failed to request device reset\n"); /* drain any remaining log messages for reset */ else firehose_read(qdl, 1000, firehose_generic_parser, NULL); return ret == FIREHOSE_ACK ? 0 : -1; } static int firehose_detect_and_configure(struct qdl_device *qdl, bool skip_storage_init, enum qdl_storage_type storage, unsigned int timeout_s) { struct timeval timeout = { .tv_sec = timeout_s }; struct timeval now; int ret; /* Track the currently configured storage type */ qdl->current_storage_type = storage; /* * The speculative retry loop below sends configure (and therefore the * VIP table) before the programmer has had a chance to start up and * request it. The VIP table can only be sent once per session * (VIP_INIT -> VIP_SEND_DATA is a one-way transition), so a premature * send leaves the programmer waiting for a table that was already * consumed, breaking the VIP session. * * When VIP is active, restore the original behaviour: drain all * startup log messages first, then do a single configure attempt. */ if (qdl->vip_data.state != VIP_DISABLED) { firehose_read(qdl, timeout_s * 1000, firehose_generic_parser, NULL); /* * The startup-log drain above is our only chance to learn * whether the programmer expects a VIP table before configure * is sent. configure goes through firehose_write(), which * unconditionally pushes the signed table when state is not * VIP_DISABLED; if the programmer isn't in VIP mode it will * try to parse those bytes as XML and reject the configure. * Demote VIP here so the table is never sent. */ if (!qdl->vip_data.programmer_requires_vip) { ux_warnx_tag(UX_TAG_VIP, "--vip-table-path was provided but programmer " "did not announce VIP; continuing without it"); qdl->vip_data.state = VIP_DISABLED; } ret = firehose_try_configure(qdl, skip_storage_init, storage); if (ret != FIREHOSE_ACK) { ux_err("configure request failed\n"); return -1; } return 0; } gettimeofday(&now, NULL); timeradd(&now, &timeout, &timeout); for (;;) { ret = firehose_try_configure(qdl, skip_storage_init, storage); /* * If the programmer's startup logs announced that VIP is * active but no --vip-table-path was provided, bail out now * rather than burning the full configure timeout waiting for * a signed table that will never be sent. */ if (qdl->vip_data.programmer_requires_vip) { ux_err_tag(UX_TAG_VIP, "programmer requires VIP, but no --vip-table-path was provided\n"); return -1; } if (ret == FIREHOSE_ACK) { break; } else if (ret != -ETIMEDOUT) { ux_err("configure request failed\n"); return -1; } gettimeofday(&now, NULL); if (timercmp(&now, &timeout, >)) { ux_err("failed to detect firehose programmer\n"); return -1; } } return 0; } int firehose_provision(struct qdl_device *qdl, struct ufs_provisioning *ufs, bool skip_reset) { int ret; ret = firehose_detect_and_configure(qdl, true, QDL_STORAGE_UFS, 5); if (ret) return ret; ret = ufs_provisioning_execute(ufs, qdl, firehose_apply_ufs_common, firehose_apply_ufs_body, firehose_apply_ufs_epilogue); if (ret > 0) { /* Epilogue signalled success + device already reset itself */ ux_info_tag(UX_TAG_FIREHOSE, "UFS provisioning succeeded\n"); return 0; } if (!ret) ux_info_tag(UX_TAG_FIREHOSE, "UFS provisioning succeeded\n"); else ux_info_tag(UX_TAG_FIREHOSE, "UFS provisioning failed\n"); if (!skip_reset) firehose_reset(qdl); return ret; } struct firehose_op *firehose_alloc_op(int type) { struct firehose_op *op; op = calloc(1, sizeof(*op)); if (!op) return NULL; op->type = type; return op; } void firehose_free_ops(struct list_head *ops) { struct firehose_op *next; struct firehose_op *op; list_for_each_entry_safe(op, next, ops, node) { list_del(&op->node); qdl_zip_put(op->zip); free((void *)op->filename); free((void *)op->label); free((void *)op->start_sector); free((void *)op->gpt_partition); free((void *)op->value); free((void *)op->what); free(op); } } static int firehose_execute_ops(struct qdl_device *qdl, struct list_head *ops) { unsigned int patch_count = 0; struct firehose_op *status_patch = NULL; struct firehose_op *tmp; struct firehose_op *op; unsigned int patch_idx = 0; int ret; list_for_each_entry(op, ops, node) { switch (op->type) { case FIREHOSE_OP_CONFIGURE: ret = firehose_detect_and_configure(qdl, false, op->storage_type, 5); if (ret) return ret; ret = gpt_resolve_deferrals(qdl, ops); if (ret) return ret; /* Update the number of patches for this storage device */ patch_count = 0; patch_idx = 0; tmp = op; list_for_each_entry_continue(tmp, ops, node) { if (tmp->type == FIREHOSE_OP_CONFIGURE) break; if (tmp->type != FIREHOSE_OP_PATCH) continue; if (tmp->filename && !strcmp(tmp->filename, "DISK")) { patch_count++; status_patch = tmp; } } break; case FIREHOSE_OP_PROGRAM: ret = firehose_program(qdl, op); if (ret < 0) return ret; break; case FIREHOSE_OP_ERASE: ret = firehose_erase(qdl, op); if (ret < 0) return ret; break; case FIREHOSE_OP_READ: ret = firehose_read_op(qdl, op); if (ret < 0) return ret; break; case FIREHOSE_OP_GET_SHA256_DIGEST: ret = firehose_getsha256digest(qdl, op); if (ret < 0) return ret; break; case FIREHOSE_OP_PATCH: ret = firehose_apply_patch(qdl, op); if (ret) return ret; if (op->filename && !strcmp(op->filename, "DISK")) { ux_progress("Applying patches", ++patch_idx, patch_count); firehose_stats.patches++; } if (op == status_patch) ux_info_tag(UX_TAG_FIREHOSE, "%d patches applied\n", patch_idx); break; case FIREHOSE_OP_SET_BOOTABLE: firehose_set_bootable(qdl, op->partition); break; case FIREHOSE_OP_RESET: ret = firehose_reset(qdl); if (ret < 0) return ret; break; default: ux_err("internal error: unknown firehose operation %d\n", op->type); return -1; } } return 0; } /* Append ", " to @buf, or just when @buf is empty */ static void firehose_append(char *buf, size_t len, const char *fmt, ...) { size_t used = strlen(buf); va_list ap; if (used && used + 2 < len) { buf[used++] = ','; buf[used++] = ' '; buf[used] = '\0'; } va_start(ap, fmt); vsnprintf(buf + used, len - used, fmt, ap); va_end(ap); } /* Print one line summing up the run, unless it did nothing */ static void firehose_summary(uint64_t ms) { char line[256] = ""; char elapsed[32]; char rate[32] = ""; char size[16]; if (firehose_stats.flashed) firehose_append(line, sizeof(line), "flashed %u partition%s", firehose_stats.flashed, firehose_stats.flashed == 1 ? "" : "s"); if (firehose_stats.skipped) firehose_append(line, sizeof(line), "skipped %u", firehose_stats.skipped); if (firehose_stats.read) firehose_append(line, sizeof(line), "read %u", firehose_stats.read); if (firehose_stats.erased) firehose_append(line, sizeof(line), "erased %u", firehose_stats.erased); if (!line[0]) return; if (firehose_stats.bytes_written) { ux_format_size(size, sizeof(size), firehose_stats.bytes_written); if (ms >= 60000) snprintf(elapsed, sizeof(elapsed), "%u min %u s", (unsigned int)(ms / 60000), (unsigned int)(ms / 1000 % 60)); else snprintf(elapsed, sizeof(elapsed), "%.1f s", ms / 1000.0); if (ms >= 500) firehose_format_rate(rate, sizeof(rate), firehose_stats.bytes_written, ms); snprintf(line + strlen(line), sizeof(line) - strlen(line), " (%s written in %s%s)", size, elapsed, rate); } if (firehose_stats.patches) firehose_append(line, sizeof(line), "applied %u patch%s", firehose_stats.patches, firehose_stats.patches == 1 ? "" : "es"); ux_result_tag(UX_TAG_FIREHOSE, "%s\n", line); } int firehose_run(struct qdl_device *qdl, struct list_head *ops) { uint64_t t0 = firehose_now_ms(); int ret; memset(&firehose_stats, 0, sizeof(firehose_stats)); ux_info_tag(UX_TAG_FIREHOSE, "waiting for Firehose programmer...\n"); ret = firehose_execute_ops(qdl, ops); if (!ret) firehose_summary(firehose_now_ms() - t0); return ret; } /* * Configure the programmer for block-level access without running any ops. * Exposed for out-of-tree drivers (the nbdkit plugin) that only need to read * and write sectors. */ int firehose_open(struct qdl_device *qdl, enum qdl_storage_type storage) { return firehose_detect_and_configure(qdl, false, storage, 5); } struct firehose_getsize_args { size_t sector_size; size_t num_sectors; }; /* * The programmer answers with the storage geometry encoded as * a JSON blob inside a value. Pick out the block size and block count. */ static int firehose_getstorageinfo_parser(xmlNode *node, void *data, bool *rawmode __unused) { struct firehose_getsize_args *args = data; struct json_value *info; struct json_value *json; double total_blocks = 0; double block_size = 0; const char *start; xmlChar *value; int ret = 0; value = xmlGetProp(node, (xmlChar *)"value"); if (!value) return -EINVAL; if (!xmlStrcmp(node->name, (xmlChar *)"response")) { ret = xmlStrcmp(value, (xmlChar *)"ACK") ? -1 : 1; xmlFree(value); return ret; } start = strchr((char *)value, '{'); if (start && strstr((char *)value, "\"total_blocks\":")) { json = json_parse_buf(start, strlen(start)); if (json) { info = json_get_child(json, "storage_info"); if (info) { json_get_number(info, "total_blocks", &total_blocks); json_get_number(info, "block_size", &block_size); args->sector_size = block_size; args->num_sectors = total_blocks; } json_free(json); } } else { ux_debug_tag(UX_TAG_FIREHOSE, "LOG: %s\n", value); } xmlFree(value); return 0; } static int firehose_getstorageinfo(struct qdl_device *qdl, int lun, struct firehose_getsize_args *args) { xmlNode *root; xmlNode *node; xmlDoc *doc; int ret; doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"getstorageinfo", NULL); xml_setpropf(node, "physical_partition_number", "%d", lun); ret = firehose_write(qdl, doc); xmlFreeDoc(doc); if (ret < 0) return ret; return firehose_read(qdl, 30000, firehose_getstorageinfo_parser, args); } /* * Query the sector size and sector count of a physical partition (LUN), used by * the nbdkit plugin to report the block device size. */ int firehose_getsize(struct qdl_device *qdl, int lun, size_t *sector_size, size_t *num_sectors) { struct firehose_getsize_args args = {}; int ret; ret = firehose_getstorageinfo(qdl, lun, &args); if (ret < 0) return ret; if (!args.sector_size || !args.num_sectors) { ux_err("device did not report storage geometry for LUN %d\n", lun); return -1; } *sector_size = args.sector_size; *num_sectors = args.num_sectors; return 0; } /* * Read @num_sectors sectors starting at @sector_offset into @buf. Reuses the * chunked read path (firehose_read_buf), so a single request is streamed in * max_payload_size units rather than one USB transfer per sector. */ ssize_t firehose_pread(struct qdl_device *qdl, int lun, size_t sector_offset, void *buf, size_t sector_size, size_t num_sectors) { char start_sector[24]; struct firehose_op op = {}; int ret; snprintf(start_sector, sizeof(start_sector), "%zu", sector_offset); op.partition = lun; op.start_sector = start_sector; op.num_sectors = num_sectors; op.sector_size = sector_size; ret = firehose_read_buf(qdl, &op, buf, num_sectors * sector_size); if (ret < 0) return ret; return num_sectors * sector_size; } /* * Write @num_sectors sectors from @buf starting at @sector_offset. The payload * is streamed in max_payload_size chunks rather than one transfer per sector. */ ssize_t firehose_pwrite(struct qdl_device *qdl, int lun, size_t sector_offset, const void *buf, size_t sector_size, size_t num_sectors) { const uint8_t *p = buf; xmlNode *root; xmlNode *node; xmlDoc *doc; size_t left; size_t chunk; int ret; doc = xmlNewDoc((xmlChar *)"1.0"); root = xmlNewNode(NULL, (xmlChar *)"data"); xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"program", NULL); xml_setpropf(node, "SECTOR_SIZE_IN_BYTES", "%zu", sector_size); xml_setpropf(node, "num_partition_sectors", "%zu", num_sectors); xml_setpropf(node, "physical_partition_number", "%d", lun); xml_setpropf(node, "start_sector", "%zu", sector_offset); ret = firehose_write(qdl, doc); xmlFreeDoc(doc); if (ret < 0) return ret; ret = firehose_read(qdl, 10000, firehose_generic_parser, NULL); if (ret < 0) { ux_err("failed to issue program request\n"); return ret; } left = num_sectors; while (left > 0) { chunk = MIN(qdl->max_payload_size / sector_size, left); ret = qdl_write(qdl, p, chunk * sector_size, 10000); if (ret < 0 || (size_t)ret != chunk * sector_size) { ux_err("USB write failed for data chunk\n"); return -1; } p += chunk * sector_size; left -= chunk; } ret = firehose_read(qdl, 120000, firehose_generic_parser, NULL); if (ret < 0) return ret; return num_sectors * sector_size; } linux-msm-qdl-6e625aa/src/firehose.h000066400000000000000000000026541527206113100174050ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __FIREHOSE_H__ #define __FIREHOSE_H__ #include #include #include #include "list.h" #include "qdl.h" #include "sha2.h" enum firehose_op_type { FIREHOSE_OP_NONE, FIREHOSE_OP_CONFIGURE, FIREHOSE_OP_PROGRAM, FIREHOSE_OP_ERASE, FIREHOSE_OP_READ, FIREHOSE_OP_PATCH, FIREHOSE_OP_SET_BOOTABLE, FIREHOSE_OP_RESET, FIREHOSE_OP_GET_SHA256_DIGEST, }; struct firehose_op { enum firehose_op_type type; struct list_head node; /* program, read, patch, set_bootable */ int partition; /* program, read, patch */ unsigned int sector_size; struct qdl_zip *zip; const char *filename; const char *start_sector; /* program, read */ unsigned int num_sectors; const char *gpt_partition; /* program, erase */ unsigned int pages_per_block; unsigned int file_offset; const char *label; bool sparse; unsigned int last_sector; bool is_nand; unsigned int sparse_chunk_type; uint32_t sparse_fill_value; off_t sparse_offset; /* patch */ unsigned int byte_offset; unsigned int size_in_bytes; const char *value; const char *what; /* configure */ enum qdl_storage_type storage_type; /* getsha256digest */ uint8_t digest[SHA256_DIGEST_LENGTH]; bool digest_valid; }; struct firehose_op *firehose_alloc_op(int type); void firehose_free_ops(struct list_head *ops); #endif linux-msm-qdl-6e625aa/src/flashmap.c000066400000000000000000000316631527206113100173710ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #include #include #include #include #include #include #include #include "file.h" #include "json.h" #include "list.h" #include "firehose.h" #include "qdl.h" enum { QDL_FILE_UNKNOWN, QDL_FILE_PATCH, QDL_FILE_PROGRAM, }; static int flashmap_resolve_path(char *path, size_t path_size, const char *filename, const char *incdir) { if (!filename) { ux_err_tag(UX_TAG_FLASHMAP, "filename is null\n"); return -1; } if (incdir) { snprintf(path, path_size, "%s/%s", incdir, filename); if (access(path, F_OK)) snprintf(path, path_size, "%s", filename); } else { snprintf(path, path_size, "%s", filename); } return 0; } static int flashmap_get_legacy_programmer(struct qdl_zip *zip, struct json_value *layout, struct sahara_image *images, const char *incdir) { struct json_value *programmers; const char *filename; char path[PATH_MAX]; int count; programmers = json_get_child(layout, "programmer"); if (!programmers) { ux_err_tag(UX_TAG_FLASHMAP, "parse error when decoding programmer\n"); return -1; } count = json_count_children(programmers); if (count != 1) { ux_err_tag(UX_TAG_FLASHMAP, "single programmer expected, found %d\n", count); return -1; } filename = json_get_element_string(programmers, 0); if (!filename) { ux_err_tag(UX_TAG_FLASHMAP, "parse error when decoding programmer\n"); return -1; } if (flashmap_resolve_path(path, sizeof(path), filename, incdir)) return -1; ux_debug_tag(UX_TAG_FLASHMAP, "selected programmer: %s\n", path); return load_sahara_image(zip, path, &images[SAHARA_ID_EHOSTDL_IMG]); } static int flashmap_get_programmer_map(struct qdl_zip *zip, struct json_value *layout, struct sahara_image *images, const char *incdir) { struct json_value *programmers; struct json_value *entry; unsigned long image_id; const char *filename; char *end; char path[PATH_MAX]; int count = 0; int ret; programmers = json_get_child(layout, "programmer"); if (!programmers || programmers->type != JSON_TYPE_OBJECT) { ux_err_tag(UX_TAG_FLASHMAP, "programmer map must be an object for version 1.2.0-qdl\n"); return -1; } for (entry = programmers->u.value; entry; entry = entry->next) { errno = 0; image_id = strtoul(entry->key, &end, 0); if (errno || end == entry->key || *end || image_id == 0 || image_id >= MAPPING_SZ) { ux_err_tag(UX_TAG_FLASHMAP, "invalid programmer image id \"%s\"\n", entry->key); return -1; } if (entry->type != JSON_TYPE_STRING || !entry->u.string) { ux_err_tag(UX_TAG_FLASHMAP, "programmer entry \"%s\" must be a filename string\n", entry->key); return -1; } filename = entry->u.string; ret = flashmap_resolve_path(path, sizeof(path), filename, incdir); if (ret) return ret; ux_debug_tag(UX_TAG_FLASHMAP, "selected programmer %lu: %s\n", image_id, path); ret = load_sahara_image(zip, path, &images[image_id]); if (ret) return ret; count++; } if (!count) { ux_err_tag(UX_TAG_FLASHMAP, "programmer map is empty\n"); return -1; } return 0; } static int flashmap_get_programmers(struct qdl_zip *zip, struct json_value *layout, struct sahara_image *images, const char *incdir, bool uses_programmer_map) { if (uses_programmer_map) return flashmap_get_programmer_map(zip, layout, images, incdir); return flashmap_get_legacy_programmer(zip, layout, images, incdir); } static int flashmap_load_xml(struct list_head *ops, struct qdl_zip *zip, const char *filename, bool is_nand, const char *incdir) { struct qdl_file file; xmlNode *node; xmlNode *root; xmlDoc *doc; size_t len; void *xml; int type = QDL_FILE_UNKNOWN; int ret; ret = qdl_file_open(zip, filename, &file); if (ret < 0) { ux_err("unable to parse XML: %s\n", filename); return -1; } xml = qdl_file_load(&file, &len); if (!xml) { ux_err("failed to load \"%s\"\n", filename); ret = -1; goto out_close_file; } if (len > INT_MAX) { ux_err("\"%s\" too large for XML parser\n", filename); ret = -1; goto out_free_xml; } doc = xmlReadMemory(xml, (int)len, filename, NULL, 0); if (!doc) { ux_err("failed to parse %s\n", filename); ret = -1; goto out_free_xml; } root = xmlDocGetRootElement(doc); if (!root) { ux_err("unable to find XML root in \"%s\"\n", filename); ret = -1; goto out_free_doc; } if (!xmlStrcmp(root->name, (xmlChar *)"patches")) { type = QDL_FILE_PATCH; } else if (!xmlStrcmp(root->name, (xmlChar *)"data")) { type = QDL_FILE_PROGRAM; for (node = root->children; node ; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (!xmlStrcmp(node->name, (xmlChar *)"program") || !xmlStrcmp(node->name, (xmlChar *)"erase")) { type = QDL_FILE_PROGRAM; break; } } } switch (type) { case QDL_FILE_PROGRAM: ret = program_load_xml(ops, doc, zip, filename, is_nand, false, NULL, incdir); break; case QDL_FILE_PATCH: ret = patch_load_xml(ops, doc, filename); break; default: ux_err("unknown file type: %s\n", filename); ret = -1; break; } out_free_doc: xmlFreeDoc(doc); out_free_xml: free(xml); out_close_file: qdl_file_close(&file); return ret; } static int flashmap_enumerate_programmables(struct json_value *list, struct list_head *ops, struct qdl_zip *zip, const char *incdir) { struct json_value *programmable; struct json_value *files; struct firehose_op *op; char path[PATH_MAX]; const char *memory; const char *file; int file_count; int file_idx; bool is_nand; double slot; int count; int ret; int i; count = json_count_children(list); if (count < 0) { ux_err_tag(UX_TAG_FLASHMAP, "programmable list is not an array\n"); return -1; } if (count == 0) { ux_err_tag(UX_TAG_FLASHMAP, "programmable list is empty\n"); return -1; } for (i = 0; i < count; i++) { programmable = json_get_element_object(list, i); memory = json_get_string(programmable, "memory"); ret = json_get_number(programmable, "slot", &slot); files = json_get_child(programmable, "files"); if (!memory || ret < 0 || !files) { ux_err("failed to parse flashmap\n"); return -1; } is_nand = !strcmp(memory, "nand"); op = firehose_alloc_op(FIREHOSE_OP_CONFIGURE); if (!op) return -1; op->storage_type = decode_storage_type(memory); list_append(ops, &op->node); file_count = json_count_children(files); if (file_count < 0) { ux_err_tag(UX_TAG_FLASHMAP, "files list is not an array\n"); return -1; } for (file_idx = 0; file_idx < file_count; file_idx++) { file = json_get_element_string(files, file_idx); if (!file) { ux_err_tag(UX_TAG_FLASHMAP, "parse error when decoding files\n"); return -1; } if (flashmap_resolve_path(path, sizeof(path), file, incdir)) return -1; ret = flashmap_load_xml(ops, zip, path, is_nand, incdir); if (ret) return ret; } } return 0; } static int flashmap_decode_storage_filter(char *filter, unsigned int *type_filter) { enum qdl_storage_type type; char *save = NULL; char *tmp; for (tmp = strtok_r(filter, ",", &save); tmp; tmp = strtok_r(NULL, ",", &save)) { type = decode_storage_type(tmp); if (type == QDL_STORAGE_UNKNOWN) { ux_err("unknown storage type \"%s\"\n", tmp); return -1; } *type_filter |= 1U << type; } return 0; } static int flashmap_decode_specifier(char *specifier, unsigned int *type_filter, char **layout_selector) { char *storage_filter; char *slash; *type_filter = 0; *layout_selector = NULL; if (!specifier) return 0; slash = strchr(specifier, '/'); if (slash) { *slash = '\0'; storage_filter = slash + 1; if (!specifier[0]) { ux_err("empty flashmap layout selector\n"); return -1; } if (!storage_filter[0]) { ux_err("empty flashmap storage selector\n"); return -1; } *layout_selector = specifier; return flashmap_decode_storage_filter(storage_filter, type_filter); } if (decode_storage_type(specifier) != QDL_STORAGE_UNKNOWN || strchr(specifier, ',')) return flashmap_decode_storage_filter(specifier, type_filter); *layout_selector = specifier; return 0; } static void flashmap_print_available_layouts(struct json_value *layouts) { struct json_value *layout; const char *name; int count; int i; count = json_count_children(layouts); if (count <= 0) return; ux_err_tag(UX_TAG_FLASHMAP, "available layouts:\n"); for (i = 0; i < count; i++) { layout = json_get_element_object(layouts, i); name = json_get_string(layout, "name"); if (name) ux_err_tag(UX_TAG_FLASHMAP, " %s\n", name); } } static struct json_value *flashmap_select_layout(struct json_value *layouts, const char *selector) { struct json_value *layout; const char *name; int count; int i; count = json_count_children(layouts); if (count < 0) { ux_err_tag(UX_TAG_FLASHMAP, "layouts is not an array\n"); return NULL; } if (!count) { ux_err_tag(UX_TAG_FLASHMAP, "layouts list is empty\n"); return NULL; } if (!selector) { if (count == 1) return json_get_element_object(layouts, 0); ux_err_tag(UX_TAG_FLASHMAP, "multiple layouts found, select one with " ":: or ::/[,...]\n"); flashmap_print_available_layouts(layouts); return NULL; } for (i = 0; i < count; i++) { layout = json_get_element_object(layouts, i); name = json_get_string(layout, "name"); if (name && !strcmp(name, selector)) { ux_debug_tag(UX_TAG_FLASHMAP, "selected layout: %s\n", name); return layout; } } ux_err_tag(UX_TAG_FLASHMAP, "layout \"%s\" not found\n", selector); flashmap_print_available_layouts(layouts); return NULL; } int flashmap_load(struct list_head *ops, const char *filename, char *specifier, struct sahara_image *images, const char *incdir) { struct list_head flashmap_ops = LIST_INIT(flashmap_ops); enum qdl_storage_type current_type = QDL_STORAGE_UNKNOWN; struct json_value *programmable; struct json_value *product; struct json_value *layout; struct firehose_op *op; struct firehose_op *next; struct qdl_file flashmap; struct json_value *json; struct json_value *obj; struct qdl_zip *zip; unsigned int type_filter = 0; unsigned int matched_ops = 0; bool uses_programmer_map = false; char *layout_selector; const char *version; const char *name; size_t json_size; void *json_blob; int count; int ret = -1; ret = flashmap_decode_specifier(specifier, &type_filter, &layout_selector); if (ret) return ret; if (!type_filter) type_filter = ~0U; ret = qdl_zip_open(filename, &zip); if (ret < 0) { ux_err("unable to create zip reference\n"); return -1; } ret = qdl_file_open(zip, zip ? "flashmap.json" : filename, &flashmap); if (ret < 0) { ux_err("failed to open flashmap\n"); goto out_put_zip; } json_blob = qdl_file_load(&flashmap, &json_size); qdl_file_close(&flashmap); if (!json_blob) { ret = -1; goto out_put_zip; } json = json_parse_buf(json_blob, json_size); if (!json) { ux_err("failed to parse flashmap json\n"); ret = -1; goto out_free_blob; } version = json_get_string(json, "version"); if (!version) { ux_err("unsupported flashmap version\n"); ret = -1; goto out_free_json; } if (!strcmp(version, "1.2.0-qdl")) uses_programmer_map = true; else if (strcmp(version, "1.1.0")) { ux_err("unsupported flashmap version\n"); ret = -1; goto out_free_json; } ux_debug("found flashmap of version: %s\n", version); obj = json_get_child(json, "products"); count = json_count_children(obj); if (count != 1) { ux_err_tag(UX_TAG_FLASHMAP, "only single product map supported, found %d\n", count); ret = -1; goto out_free_json; } product = json_get_element_object(obj, 0); name = json_get_string(product, "name"); ux_debug_tag(UX_TAG_FLASHMAP, "product: %s\n", name); obj = json_get_child(product, "layouts"); layout = flashmap_select_layout(obj, layout_selector); if (!layout) { ret = -1; goto out_free_json; } ret = flashmap_get_programmers(zip, layout, images, zip ? NULL : incdir, uses_programmer_map); if (ret) goto out_free_json; programmable = json_get_child(layout, "programmable"); if (!programmable) { ux_err_tag(UX_TAG_FLASHMAP, "parse error when decoding programmables\n"); sahara_images_free(images, MAPPING_SZ); ret = -1; goto out_free_json; } ret = flashmap_enumerate_programmables(programmable, &flashmap_ops, zip, zip ? NULL : incdir); if (ret < 0) { firehose_free_ops(&flashmap_ops); sahara_images_free(images, MAPPING_SZ); goto out_free_json; } list_for_each_entry_safe(op, next, &flashmap_ops, node) { if (op->storage_type != QDL_STORAGE_UNKNOWN) current_type = op->storage_type; if ((1U << current_type) & type_filter) { list_del(&op->node); list_append(ops, &op->node); matched_ops++; } } firehose_free_ops(&flashmap_ops); if (!matched_ops) { ux_err("loaded flashmap does not contain any operations for selected storage type\n"); ret = -1; } out_free_json: json_free(json); out_free_blob: free(json_blob); out_put_zip: qdl_zip_put(zip); return ret; } linux-msm-qdl-6e625aa/src/flashmap.h000066400000000000000000000012571527206113100173720ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __FLASHMAP_H__ #define __FLASHMAP_H__ #include #include "list.h" struct sahara_image; int flashmap_load(struct list_head *ops, const char *filename, char *specifier, struct sahara_image *images, const char *incdir); /* * Implemented in zipper.c, which is only built with zip-container * support; file_nozip.c stubs it out otherwise. Callers are expected * to check qdl_zip_supported() and report the missing feature * themselves. */ int zipper_write(const char *filename, struct list_head *ops, struct sahara_image *images); #endif linux-msm-qdl-6e625aa/src/gpt.c000066400000000000000000000176421527206113100163710ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #include "firehose.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "qdl.h" #include "gpt.h" struct gpt_guid { uint32_t data1; uint16_t data2; uint16_t data3; uint8_t data4[8]; } __attribute__((packed)); static const struct gpt_guid gpt_zero_guid = {0}; struct gpt_header { uint8_t signature[8]; uint32_t revision; uint32_t header_size; uint32_t header_crc32; uint32_t reserved; uint64_t current_lba; uint64_t backup_lba; uint64_t first_usable_lba; uint64_t last_usable_lba; struct gpt_guid disk_guid; uint64_t part_entry_lba; uint32_t num_part_entries; uint32_t part_entry_size; uint32_t part_array_crc32; uint8_t reserved2[420]; } __attribute__((packed)); struct gpt_entry { struct gpt_guid type_guid; struct gpt_guid unique_guid; uint64_t first_lba; uint64_t last_lba; uint64_t attrs; uint16_t name_utf16le[36]; } __attribute__((packed)); struct gpt_partition { const char *name; unsigned int partition; uint64_t start_sector; uint64_t num_sectors; struct gpt_partition *next; }; static struct gpt_partition *gpt_partitions; static struct gpt_partition *gpt_partitions_last; static void utf16le_to_utf8(uint16_t *in, size_t in_len, uint8_t *out, size_t out_len) { uint32_t codepoint; uint16_t high; uint16_t low; uint16_t w; size_t i; size_t j = 0; for (i = 0; i < in_len; i++) { w = in[i]; if (w >= 0xd800 && w <= 0xdbff) { high = w - 0xd800; if (i + 1 < in_len) { w = in[++i]; if (w >= 0xdc00 && w <= 0xdfff) { low = w - 0xdc00; codepoint = (((uint32_t)high << 10) | low) + 0x10000; } else { /* Surrogate without low surrogate */ codepoint = 0xfffd; } } else { /* Lone high surrogate at end of string */ codepoint = 0xfffd; } } else if (w >= 0xdc00 && w <= 0xdfff) { /* Low surrogate without high */ codepoint = 0xfffd; } else { codepoint = w; } if (codepoint == 0) break; if (codepoint <= 0x7f) { if (j + 1 >= out_len) break; out[j++] = (uint8_t)codepoint; } else if (codepoint <= 0x7ff) { if (j + 2 >= out_len) break; out[j++] = 0xc0 | ((codepoint >> 6) & 0x1f); out[j++] = 0x80 | (codepoint & 0x3f); } else if (codepoint <= 0xffff) { if (j + 3 >= out_len) break; out[j++] = 0xe0 | ((codepoint >> 12) & 0x0f); out[j++] = 0x80 | ((codepoint >> 6) & 0x3f); out[j++] = 0x80 | (codepoint & 0x3f); } else if (codepoint <= 0x10ffff) { if (j + 4 >= out_len) break; out[j++] = 0xf0 | ((codepoint >> 18) & 0x07); out[j++] = 0x80 | ((codepoint >> 12) & 0x3f); out[j++] = 0x80 | ((codepoint >> 6) & 0x3f); out[j++] = 0x80 | (codepoint & 0x3f); } } out[j] = '\0'; } static int gpt_load_table_from_partition(struct qdl_device *qdl, unsigned int phys_partition, bool *eof) { struct gpt_partition *partition; struct gpt_entry *entry; struct gpt_header gpt; uint8_t buf[4096]; struct firehose_op op; unsigned int offset; uint64_t lba; char lba_buf[21]; uint16_t name_utf16le[36]; char name[36 * 4]; int ret; unsigned int i; memset(&op, 0, sizeof(op)); op.type = FIREHOSE_OP_READ; op.sector_size = qdl->sector_size; op.start_sector = "1"; op.num_sectors = 1; op.partition = phys_partition; memset(&buf, 0, sizeof(buf)); ret = firehose_read_buf(qdl, &op, &gpt, sizeof(gpt)); if (ret) { /* Assume that we're beyond the last partition */ *eof = true; return -1; } if (memcmp(gpt.signature, "EFI PART", 8)) { ux_err("partition %d has not GPT header\n", phys_partition); return 0; } /* * The entry read loop below assumes entries never straddle a sector * boundary, which only holds when the entry size is a non-zero divisor * of the sector size and is at least as large as a GPT entry. */ if (gpt.part_entry_size < sizeof(struct gpt_entry) || gpt.part_entry_size > qdl->sector_size || qdl->sector_size % gpt.part_entry_size != 0 || gpt.num_part_entries > 1024) { ux_debug("partition %d has invalid GPT header\n", phys_partition); return -1; } ux_debug("Loading GPT table from physical partition %d\n", phys_partition); for (i = 0; i < gpt.num_part_entries; i++) { offset = (i * gpt.part_entry_size) % qdl->sector_size; if (offset == 0) { lba = gpt.part_entry_lba + i * gpt.part_entry_size / qdl->sector_size; snprintf(lba_buf, sizeof(lba_buf), "%" PRIu64, lba); op.start_sector = lba_buf; memset(buf, 0, sizeof(buf)); ret = firehose_read_buf(qdl, &op, buf, sizeof(buf)); if (ret) { ux_err("failed to read GPT partition entries from %d:%" PRIu64 "\n", phys_partition, lba); return -1; } } entry = (struct gpt_entry *)(buf + offset); if (!memcmp(&entry->type_guid, &gpt_zero_guid, sizeof(struct gpt_guid))) continue; memcpy(name_utf16le, entry->name_utf16le, sizeof(name_utf16le)); utf16le_to_utf8(name_utf16le, 36, (uint8_t *)name, sizeof(name)); partition = calloc(1, sizeof(*partition)); if (!partition) return -1; partition->name = strdup(name); partition->partition = phys_partition; partition->start_sector = entry->first_lba; /* if first_lba == last_lba there is 1 sector worth of data (IE: add 1 below) */ partition->num_sectors = entry->last_lba - entry->first_lba + 1; ux_debug(" %3d: %s start sector %" PRIu64 ", num sectors %" PRIu64 "\n", i, partition->name, partition->start_sector, partition->num_sectors); if (gpt_partitions) { gpt_partitions_last->next = partition; gpt_partitions_last = partition; } else { gpt_partitions = partition; gpt_partitions_last = partition; } } return 0; } static int gpt_load_tables(struct qdl_device *qdl) { unsigned int i; bool eof = false; int ret = 0; if (gpt_partitions) return 0; for (i = 0; ; i++) { ret = gpt_load_table_from_partition(qdl, i, &eof); if (ret) break; } return eof ? 0 : ret; } int gpt_find_by_name(struct qdl_device *qdl, const char *name, int *phys_partition, uint64_t *start_sector, uint64_t *num_sectors) { struct gpt_partition *gpt_part; bool found = false; int ret; if (qdl->dev_type == QDL_DEVICE_SIM) { *start_sector = 0; *num_sectors = 0; return 0; } ret = gpt_load_tables(qdl); if (ret < 0) return -1; for (gpt_part = gpt_partitions; gpt_part; gpt_part = gpt_part->next) { if (*phys_partition >= 0 && gpt_part->partition != (unsigned int)(*phys_partition)) continue; if (strcmp(gpt_part->name, name)) continue; if (found) { ux_err("duplicate candidates for partition \"%s\" found\n", name); return -1; } *phys_partition = gpt_part->partition; *start_sector = gpt_part->start_sector; *num_sectors = gpt_part->num_sectors; found = true; } if (!found) { if (*phys_partition >= 0) ux_err("no partition \"%s\" found on physical partition %d\n", name, *phys_partition); else ux_err("no partition \"%s\" found\n", name); return -1; } return 0; } int gpt_resolve_deferrals(struct qdl_device *qdl, struct list_head *ops) { uint64_t start_sector; uint64_t num_sectors; struct firehose_op *op; char buf[21]; int ret; list_for_each_entry(op, ops, node) { if (op->type != FIREHOSE_OP_PROGRAM && op->type != FIREHOSE_OP_ERASE && op->type != FIREHOSE_OP_READ && op->type != FIREHOSE_OP_GET_SHA256_DIGEST) continue; if (!op->gpt_partition) continue; ret = gpt_find_by_name(qdl, op->gpt_partition, &op->partition, &start_sector, &num_sectors); if (ret < 0) return -1; if (num_sectors > UINT_MAX) { ux_err("partition \"%s\" has too many sectors (%" PRIu64 ")\n", op->gpt_partition, num_sectors); return -1; } op->num_sectors = (unsigned int)num_sectors; snprintf(buf, sizeof(buf), "%" PRIu64, start_sector); op->start_sector = strdup(buf); } return 0; } linux-msm-qdl-6e625aa/src/gpt.h000066400000000000000000000007041527206113100163650ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __GPT_H__ #define __GPT_H__ #include #include "list.h" struct qdl_device; struct list_head; int gpt_find_by_name(struct qdl_device *qdl, const char *name, int *partition, uint64_t *start_sector, uint64_t *num_sectors); int gpt_resolve_deferrals(struct qdl_device *qdl, struct list_head *ops); #endif linux-msm-qdl-6e625aa/src/input_type.c000066400000000000000000000041551527206113100177720ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* * Classification of the positional inputs of the flashing flow: command * verbs, and XML input files told apart by their root and child * elements. */ #include #include #include #include #include #include #include "qdl.h" #include "input_type.h" int detect_type(const char *verb) { xmlNode *root; xmlDoc *doc; xmlNode *node; int type = QDL_FILE_UNKNOWN; if (!strcmp(verb, "read")) return QDL_CMD_READ; if (!strcmp(verb, "write")) return QDL_CMD_WRITE; if (!strcmp(verb, "erase")) return QDL_CMD_ERASE; if (!strcmp(verb, "flash")) return QDL_CMD_FLASH; if (!strcmp(verb, "sha256")) return QDL_CMD_SHA256; if (!strcmp(verb, "reset")) return QDL_CMD_RESET; if (access(verb, F_OK)) { ux_err("%s is not a verb and not a XML file\n", verb); return -EINVAL; } doc = xmlReadFile(verb, NULL, 0); if (!doc) { ux_err("failed to parse XML file \"%s\"\n", verb); return -EINVAL; } root = xmlDocGetRootElement(doc); if (!xmlStrcmp(root->name, (xmlChar *)"patches")) { type = QDL_FILE_PATCH; } else if (!xmlStrcmp(root->name, (xmlChar *)"data")) { for (node = root->children; node ; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (!xmlStrcmp(node->name, (xmlChar *)"program")) { type = QDL_FILE_PROGRAM; break; } if (!xmlStrcmp(node->name, (xmlChar *)"read")) { type = QDL_FILE_READ; break; } if (!xmlStrcmp(node->name, (xmlChar *)"ufs")) { type = QDL_FILE_UFS; break; } } } else if (!xmlStrcmp(root->name, (xmlChar *)"contents")) { type = QDL_FILE_CONTENTS; } xmlFreeDoc(doc); return type; } bool qdl_is_contents_xml(const char *filename) { xmlNode *root; xmlDoc *doc; bool ret; doc = xmlReadFile(filename, NULL, XML_PARSE_NONET | XML_PARSE_NOERROR | XML_PARSE_NOWARNING); if (!doc) return false; root = xmlDocGetRootElement(doc); ret = root && !xmlStrcmp(root->name, (xmlChar *)"contents"); xmlFreeDoc(doc); return ret; } linux-msm-qdl-6e625aa/src/input_type.h000066400000000000000000000007621527206113100177770ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __INPUT_TYPE_H__ #define __INPUT_TYPE_H__ #include enum { QDL_FILE_UNKNOWN, QDL_FILE_PATCH, QDL_FILE_PROGRAM, QDL_FILE_READ, QDL_FILE_UFS, QDL_FILE_CONTENTS, QDL_CMD_READ, QDL_CMD_WRITE, QDL_CMD_ERASE, QDL_CMD_FLASH, QDL_CMD_SHA256, QDL_CMD_RESET, }; int detect_type(const char *verb); bool qdl_is_contents_xml(const char *filename); #endif linux-msm-qdl-6e625aa/src/io.c000066400000000000000000000066641527206113100162100ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2025, Qualcomm Innovation Center, Inc. All rights reserved. */ #include #include #include #include "qdl.h" struct qdl_device *qdl_init(enum QDL_DEVICE_TYPE type) { if (type == QDL_DEVICE_USB) return usb_init(); if (type == QDL_DEVICE_SIM) return sim_init(); if (type == QDL_DEVICE_QUD) return qud_init(); if (type == QDL_DEVICE_AUTO) return auto_init(); return NULL; } void qdl_deinit(struct qdl_device *qdl) { if (qdl) { free(qdl->pending_buf); free(qdl); } } void qdl_set_out_chunk_size(struct qdl_device *qdl, long size) { qdl->set_out_chunk_size(qdl, size); } int qdl_open(struct qdl_device *qdl, const char *serial) { return qdl->open(qdl, serial); } void qdl_close(struct qdl_device *qdl) { qdl->close(qdl); } /** * qdl_read() - Read a message from the device * @qdl: device handle * @buf: buffer to write the data into * @len: maximum length of data to be read * @timeout: timeout for the read, in milliseconds * * Drains the pushback buffer (qdl_push_back()) before touching the underlying * transport, so a previous read that crossed a Firehose message boundary can * deliver the trailing bytes here. * * Returns: number of bytes read, might be zero for a ZLP * negative errno on failure (notably -ETIMEDOUT) */ int qdl_read(struct qdl_device *qdl, void *buf, size_t len, unsigned int timeout) { size_t available; size_t copy; if (qdl->pending_buf) { available = qdl->pending_len - qdl->pending_off; copy = available < len ? available : len; memcpy(buf, qdl->pending_buf + qdl->pending_off, copy); qdl->pending_off += copy; if (qdl->pending_off >= qdl->pending_len) { free(qdl->pending_buf); qdl->pending_buf = NULL; qdl->pending_len = 0; qdl->pending_off = 0; } return (int)copy; } return qdl->read(qdl, buf, len, timeout); } /** * qdl_push_back() - Stash unread bytes for a future qdl_read() * @qdl: device handle * @buf: bytes to remember * @len: number of bytes * * Concatenates @buf onto whatever is already pending (after any already-served * prefix). Used by firehose_read() when a single transport read returned more * than one Firehose message - or an XML envelope followed immediately by its * rawmode binary payload - and we need the trailing bytes to surface on the * next qdl_read() before any new transport I/O happens. * * Returns: 0 on success, negative errno on allocation failure. */ int qdl_push_back(struct qdl_device *qdl, const void *buf, size_t len) { char *grown; size_t total; if (!len) return 0; if (qdl->pending_off > 0) { size_t leftover = qdl->pending_len - qdl->pending_off; memmove(qdl->pending_buf, qdl->pending_buf + qdl->pending_off, leftover); qdl->pending_len = leftover; qdl->pending_off = 0; } total = qdl->pending_len + len; grown = realloc(qdl->pending_buf, total); if (!grown) return -ENOMEM; memcpy(grown + qdl->pending_len, buf, len); qdl->pending_buf = grown; qdl->pending_len = total; return 0; } /** * qdl_write() - Write a message from the device * @qdl: device handle * @buf: buffer with data to be written * @len: length of data to be written * @timeout: timeout for write, in milliseconds * * Returns: number of bytes read * negative errno on failure (notably -ETIMEDOUT) */ int qdl_write(struct qdl_device *qdl, const void *buf, size_t len, unsigned int timeout) { return qdl->write(qdl, buf, len, timeout); } linux-msm-qdl-6e625aa/src/json.c000066400000000000000000000433031527206113100165410ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. * Copyright (c) 2018-2019, Linaro Ltd. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include "json.h" static const char *input_buf; static int input_pos; static int input_len; static bool input_can_unput; static int nesting_depth; char json_error[JSON_ERROR_SIZE]; enum { JSON_INPUT_EOF = -1 }; static int json_parse_array(struct json_value *array); static int json_parse_object(struct json_value *object); static int json_parse_property(struct json_value *value); static int input(void); static void unput(void); static void json_set_error(const char *fmt, ...) { va_list ap; if (json_error[0]) return; va_start(ap, fmt); vsnprintf(json_error, sizeof(json_error), fmt, ap); va_end(ap); } static int json_hex_value(int ch) { if (ch >= '0' && ch <= '9') return ch - '0'; if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; return -1; } static int json_buf_append(char **buf, size_t *len, size_t *cap, unsigned char byte) { char *new_buf; if (*len + 1 >= *cap) { size_t new_cap = *cap ? (*cap * 2) : 32; new_buf = realloc(*buf, new_cap); if (!new_buf) return -1; *buf = new_buf; *cap = new_cap; } (*buf)[*len] = (char)byte; (*len)++; return 0; } static int json_buf_append_utf8(char **buf, size_t *len, size_t *cap, unsigned int cp) { if (cp <= 0x7f) return json_buf_append(buf, len, cap, (unsigned char)cp); if (cp <= 0x7ff) { if (json_buf_append(buf, len, cap, (unsigned char)(0xc0 | (cp >> 6)))) return -1; return json_buf_append(buf, len, cap, (unsigned char)(0x80 | (cp & 0x3f))); } if (cp <= 0xffff) { if (json_buf_append(buf, len, cap, (unsigned char)(0xe0 | (cp >> 12)))) return -1; if (json_buf_append(buf, len, cap, (unsigned char)(0x80 | ((cp >> 6) & 0x3f)))) return -1; return json_buf_append(buf, len, cap, (unsigned char)(0x80 | (cp & 0x3f))); } if (cp <= 0x10ffff) { if (json_buf_append(buf, len, cap, (unsigned char)(0xf0 | (cp >> 18)))) return -1; if (json_buf_append(buf, len, cap, (unsigned char)(0x80 | ((cp >> 12) & 0x3f)))) return -1; if (json_buf_append(buf, len, cap, (unsigned char)(0x80 | ((cp >> 6) & 0x3f)))) return -1; return json_buf_append(buf, len, cap, (unsigned char)(0x80 | (cp & 0x3f))); } return -1; } static int json_parse_hex4(unsigned int *out) { unsigned int code = 0; int i; for (i = 0; i < 4; i++) { int ch = input(); int digit; if (ch == JSON_INPUT_EOF) return -1; digit = json_hex_value(ch); if (digit < 0) return -1; code = (code << 4) | (unsigned int)digit; } *out = code; return 0; } static int json_parse_escape(char **buf, size_t *len, size_t *cap, int escape_pos) { unsigned int code; unsigned int low; int ch; ch = input(); if (ch == JSON_INPUT_EOF) { json_set_error("unterminated escape at offset %d", escape_pos); return -1; } switch (ch) { case '"': case '\\': case '/': if (json_buf_append(buf, len, cap, (unsigned char)ch)) { json_set_error("out of memory while parsing escape at offset %d", escape_pos); return -1; } return 0; case 'b': if (json_buf_append(buf, len, cap, '\b')) { json_set_error("out of memory while parsing escape at offset %d", escape_pos); return -1; } return 0; case 'f': if (json_buf_append(buf, len, cap, '\f')) { json_set_error("out of memory while parsing escape at offset %d", escape_pos); return -1; } return 0; case 'n': if (json_buf_append(buf, len, cap, '\n')) { json_set_error("out of memory while parsing escape at offset %d", escape_pos); return -1; } return 0; case 'r': if (json_buf_append(buf, len, cap, '\r')) { json_set_error("out of memory while parsing escape at offset %d", escape_pos); return -1; } return 0; case 't': if (json_buf_append(buf, len, cap, '\t')) { json_set_error("out of memory while parsing escape at offset %d", escape_pos); return -1; } return 0; case 'u': if (json_parse_hex4(&code)) { json_set_error("invalid unicode escape at offset %d", escape_pos); return -1; } if (code >= 0xd800 && code <= 0xdbff) { ch = input(); if (ch != '\\') { json_set_error("expected low surrogate escape at offset %d", input_pos - 1); return -1; } ch = input(); if (ch != 'u') { json_set_error("expected low surrogate escape at offset %d", input_pos - 1); return -1; } if (json_parse_hex4(&low)) { json_set_error("invalid low surrogate escape at offset %d", input_pos); return -1; } if (low < 0xdc00 || low > 0xdfff) { json_set_error("invalid low surrogate value at offset %d", input_pos); return -1; } code = 0x10000 + (((code - 0xd800) << 10) | (low - 0xdc00)); } else if (code >= 0xdc00 && code <= 0xdfff) { json_set_error("unexpected low surrogate at offset %d", escape_pos); return -1; } if (json_buf_append_utf8(buf, len, cap, code)) { json_set_error("out of memory while decoding unicode escape"); return -1; } return 0; default: json_set_error("invalid escape '\\%c' at offset %d", ch, escape_pos); return -1; } } static int json_enter_nesting(int offset) { nesting_depth++; if (nesting_depth > JSON_MAX_DEPTH) { nesting_depth--; json_set_error("maximum nesting depth %d exceeded at offset %d", JSON_MAX_DEPTH, offset); return -1; } return 0; } static void json_leave_nesting(void) { if (nesting_depth > 0) nesting_depth--; } static int input(void) { if (input_pos >= input_len) { input_can_unput = false; return JSON_INPUT_EOF; } input_can_unput = true; return (unsigned char)input_buf[input_pos++]; } static void unput(void) { if (input_can_unput && input_pos > 0) { input_pos--; input_can_unput = false; } } static void json_skip_whitespace(void) { int ch; while ((ch = input()) != JSON_INPUT_EOF && isspace((unsigned char)ch)) ; unput(); } static int json_parse_string(struct json_value *value) { size_t len = 0; size_t cap = 0; char *str; int string_start; int ch; ch = input(); if (ch != '"') { unput(); return 0; } string_start = input_pos - 1; str = NULL; while ((ch = input()) != JSON_INPUT_EOF) { if (ch == '"') break; if (ch == '\\') { if (json_parse_escape(&str, &len, &cap, input_pos - 1)) { free(str); return -1; } continue; } if (ch < 0x20) { json_set_error("unescaped control character in string at offset %d", input_pos - 1); free(str); return -1; } if (json_buf_append(&str, &len, &cap, (unsigned char)ch)) { json_set_error("out of memory while parsing string"); free(str); return -1; } } if (ch == JSON_INPUT_EOF) { json_set_error("unterminated string starting at offset %d", string_start); free(str); return -1; } if (json_buf_append(&str, &len, &cap, '\0')) { json_set_error("out of memory while finalizing string"); free(str); return -1; } value->type = JSON_TYPE_STRING; value->u.string = str; return 1; } static int json_parse_number(struct json_value *value) { char *token; char *endptr; double parsed; size_t len; int start; int end; int ch; ch = input(); if (ch != '-' && (ch == JSON_INPUT_EOF || !isdigit(ch))) { unput(); return 0; } start = input_pos - 1; if (ch == '-') { ch = input(); if (ch == JSON_INPUT_EOF || !isdigit(ch)) { json_set_error("invalid number: '-' not followed by digit at offset %d", start); return -1; } } if (ch == '0') { ch = input(); if (ch != JSON_INPUT_EOF && isdigit(ch)) { json_set_error("invalid number: leading zero at offset %d", start); return -1; } } else if (isdigit(ch)) { do { ch = input(); } while (ch != JSON_INPUT_EOF && isdigit(ch)); } else { json_set_error("invalid number at offset %d", start); return -1; } if (ch == '.') { ch = input(); if (ch == JSON_INPUT_EOF || !isdigit(ch)) { json_set_error("invalid number: fractional part missing digits at offset %d", start); return -1; } do { ch = input(); } while (ch != JSON_INPUT_EOF && isdigit(ch)); } if (ch == 'e' || ch == 'E') { ch = input(); if (ch == '+' || ch == '-') ch = input(); if (ch == JSON_INPUT_EOF || !isdigit(ch)) { json_set_error("invalid number: exponent missing digits at offset %d", start); return -1; } do { ch = input(); } while (ch != JSON_INPUT_EOF && isdigit(ch)); } if (ch != JSON_INPUT_EOF) { end = input_pos - 1; unput(); } else { end = input_pos; } len = (size_t)(end - start); token = malloc(len + 1); if (!token) { json_set_error("out of memory while parsing number"); return -1; } memcpy(token, input_buf + start, len); token[len] = '\0'; parsed = strtod(token, &endptr); /* * Reject a token that was not fully consumed, and overflow (strtod * returns +/-inf, caught by !isfinite). Underflow to a subnormal or * zero also sets ERANGE but yields a finite, valid JSON number, so it * must not be rejected. */ if (endptr != token + len || !isfinite(parsed)) { json_set_error("invalid or out-of-range number at offset %d", start); free(token); return -1; } free(token); value->type = JSON_TYPE_NUMBER; value->u.number = parsed; return 1; } static int json_parse_keyword(struct json_value *value) { const char *match; int i; int ch; ch = input(); switch (ch) { case 't': match = "true"; value->type = JSON_TYPE_TRUE; break; case 'f': match = "false"; value->type = JSON_TYPE_FALSE; break; case 'n': match = "null"; value->type = JSON_TYPE_NULL; break; default: unput(); return 0; } for (i = 1; match[i]; i++) { ch = input(); if (ch == JSON_INPUT_EOF || ch != match[i]) { json_set_error("invalid keyword at offset %d", input_pos - 1); return -1; } } return 1; } static int json_parse_value(struct json_value *value) { int ret; json_skip_whitespace(); ret = json_parse_object(value); if (ret) goto out; ret = json_parse_array(value); if (ret) goto out; ret = json_parse_string(value); if (ret) goto out; ret = json_parse_number(value); if (ret) goto out; ret = json_parse_keyword(value); if (ret) goto out; json_set_error("unable to match value at offset %d", input_pos); return -1; out: json_skip_whitespace(); return ret; } static int json_parse_array(struct json_value *array) { struct json_value *value; struct json_value *last = NULL; int ret; int ch; ch = input(); if (ch != '[') { unput(); return 0; } if (json_enter_nesting(input_pos - 1)) return -1; array->type = JSON_TYPE_ARRAY; json_skip_whitespace(); ch = input(); if (ch == ']') { json_leave_nesting(); return 1; } unput(); do { value = calloc(1, sizeof(*value)); if (!value) { json_set_error("out of memory while parsing array at offset %d", input_pos); json_leave_nesting(); return -1; } ret = json_parse_value(value); if (ret <= 0) { json_set_error("invalid array element at offset %d", input_pos); json_free(value); json_leave_nesting(); return -1; } if (!array->u.value) array->u.value = value; if (last) last->next = value; last = value; ch = input(); if (ch == ']') { json_leave_nesting(); return 1; } } while (ch == ','); json_set_error("expected ',' or ']' at offset %d", input_pos - 1); json_leave_nesting(); return -1; } static int json_parse_object(struct json_value *object) { struct json_value *value; struct json_value *last = NULL; int ret; int ch; ch = input(); if (ch != '{') { unput(); return 0; } if (json_enter_nesting(input_pos - 1)) return -1; object->type = JSON_TYPE_OBJECT; json_skip_whitespace(); ch = input(); if (ch == '}') { json_leave_nesting(); return 1; } unput(); do { value = calloc(1, sizeof(*value)); if (!value) { json_set_error("out of memory while parsing object at offset %d", input_pos); json_leave_nesting(); return -1; } ret = json_parse_property(value); if (ret <= 0) { json_set_error("invalid object property at offset %d", input_pos); json_free(value); json_leave_nesting(); return -1; } if (!object->u.value) object->u.value = value; if (last) last->next = value; last = value; ch = input(); if (ch == '}') { json_leave_nesting(); return 1; } } while (ch == ','); json_set_error("expected ',' or '}' at offset %d", input_pos - 1); json_leave_nesting(); return -1; } static int json_parse_property(struct json_value *value) { struct json_value key; int ret; int ch; json_skip_whitespace(); ret = json_parse_string(&key); if (ret <= 0) { json_set_error("expected string key at offset %d", input_pos); return -1; } value->key = key.u.string; json_skip_whitespace(); ch = input(); if (ch != ':') { json_set_error("expected ':' after key at offset %d", input_pos - 1); return -1; } ret = json_parse_value(value); if (ret <= 0) { json_set_error("invalid property value at offset %d", input_pos); return -1; } return 1; } static struct json_value *json_parse_internal(const char *json, size_t len) { struct json_value *root; int ret; json_error[0] = '\0'; if (len > INT_MAX) { json_set_error("json input too large"); return NULL; } input_buf = json; input_pos = 0; input_len = len; input_can_unput = false; nesting_depth = 0; root = calloc(1, sizeof(*root)); if (!root) { json_set_error("out of memory while allocating parse root"); return NULL; } ret = json_parse_value(root); if (ret != 1) { json_set_error("parse error near offset %d", input_pos); json_free(root); return NULL; } json_skip_whitespace(); if (input() != JSON_INPUT_EOF) { json_set_error("unexpected trailing token at offset %d", input_pos - 1); json_free(root); return NULL; } return root; } struct json_value *json_parse_buf(const char *json, size_t len) { return json_parse_internal(json, len); } struct json_value *json_get_child(struct json_value *object, const char *key) { struct json_value *it; if (!object || object->type != JSON_TYPE_OBJECT) return NULL; for (it = object->u.value; it; it = it->next) { if (!strcmp(it->key, key)) return it; } return NULL; } struct json_value *json_get_element_object(struct json_value *object, unsigned int idx) { struct json_value *it; unsigned int i; if (!object || object->type != JSON_TYPE_ARRAY) return NULL; for (it = object->u.value, i = 0; it; it = it->next, i++) { if (i == idx) return it; } return NULL; } const char *json_get_element_string(struct json_value *object, unsigned int idx) { struct json_value *element; if (!object) return NULL; element = json_get_element_object(object, idx); if (!element || element->type != JSON_TYPE_STRING) return NULL; return element->u.string; } int json_count_children(struct json_value *array) { struct json_value *it; int count = 0; if (!array || array->type != JSON_TYPE_ARRAY) return -1; for (it = array->u.value; it; it = it->next) count++; return count; } int json_get_number(struct json_value *object, const char *key, double *number) { struct json_value *it; if (!object || object->type != JSON_TYPE_OBJECT) return -1; for (it = object->u.value; it; it = it->next) { if (!strcmp(it->key, key)) { if (it->type != JSON_TYPE_NUMBER) return -1; *number = it->u.number; return 0; } } return -1; } const char *json_get_string(struct json_value *object, const char *key) { struct json_value *it; if (!object || object->type != JSON_TYPE_OBJECT) return NULL; for (it = object->u.value; it; it = it->next) { if (!strcmp(it->key, key)) { if (it->type != JSON_TYPE_STRING) return NULL; return it->u.string; } } return NULL; } void json_free(struct json_value *value) { struct json_value *stack; struct json_value *next; struct json_value *it; if (!value) return; /* Free iteratively to avoid recursion depth issues on deeply nested JSON. */ stack = value; stack->next = NULL; while (stack) { value = stack; stack = stack->next; if (value->type == JSON_TYPE_OBJECT || value->type == JSON_TYPE_ARRAY) { it = value->u.value; while (it) { next = it->next; it->next = stack; stack = it; it = next; } } free((char *)value->key); if (value->type == JSON_TYPE_STRING) free((char *)value->u.string); free(value); } } linux-msm-qdl-6e625aa/src/json.h000066400000000000000000000056361527206113100165550ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. * Copyright (c) 2019, Linaro Ltd. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #ifndef __JSON_H__ #define __JSON_H__ #include #define JSON_ERROR_SIZE 256 #define JSON_MAX_DEPTH 256 enum { JSON_TYPE_UNKNOWN, JSON_TYPE_TRUE, JSON_TYPE_FALSE, JSON_TYPE_NULL, JSON_TYPE_NUMBER, JSON_TYPE_STRING, JSON_TYPE_ARRAY, JSON_TYPE_OBJECT, }; struct json_value { const char *key; int type; union { double number; const char *string; struct json_value *value; } u; struct json_value *next; }; /* * Parser error message for the most recent json_parse_buf() call. * Empty string means no parse error was recorded. * This buffer is global and not thread-safe. */ extern char json_error[JSON_ERROR_SIZE]; struct json_value *json_parse_buf(const char *json, size_t len); int json_count_children(struct json_value *array); /* Duplicate object keys are resolved by first match in input order. */ struct json_value *json_get_child(struct json_value *object, const char *key); struct json_value *json_get_element_object(struct json_value *object, unsigned int idx); const char *json_get_element_string(struct json_value *object, unsigned int idx); int json_get_number(struct json_value *object, const char *key, double *number); const char *json_get_string(struct json_value *object, const char *key); void json_free(struct json_value *value); #endif linux-msm-qdl-6e625aa/src/list.h000066400000000000000000000041501527206113100165450ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) 2016, Linaro Ltd. */ #ifndef __LIST_H__ #define __LIST_H__ #include #include struct list_head { struct list_head *prev; struct list_head *next; }; #define LIST_INIT(list) { &(list), &(list) } static inline void list_init(struct list_head *list) { list->prev = list; list->next = list; } static inline bool list_empty(struct list_head *list) { return list->next == list; } static inline void list_prepend(struct list_head *list, struct list_head *item) { struct list_head *first = list->next; item->next = first; item->prev = list; list->next = item; first->prev = item; } static inline void list_append(struct list_head *list, struct list_head *item) { struct list_head *prev = list->prev; item->next = list; item->prev = prev; prev->next = item; list->prev = item; } static inline void list_del(struct list_head *item) { item->prev->next = item->next; item->next->prev = item->prev; } #define list_for_each(item, list) \ for (item = (list)->next; item != list; item = item->next) #define list_for_each_safe(item, tmp, list) \ for (item = (list)->next, tmp = item->next; item != list; item = tmp, tmp = item->next) #define list_entry(item, type, member) \ container_of(item, type, member) #define list_entry_first(list, type, member) \ container_of((list)->next, type, member) #define list_entry_next(item, member) \ container_of((item)->member.next, typeof(*(item)), member) #define list_for_each_entry(item, list, member) \ for (item = list_entry_first(list, typeof(*(item)), member); \ &item->member != list; \ item = list_entry_next(item, member)) #define list_for_each_entry_continue(item, list, member) \ for (item = list_entry_next(item, member); \ &item->member != list; \ item = list_entry_next(item, member)) #define list_for_each_entry_safe(item, next, list, member) \ for (item = list_entry_first(list, typeof(*(item)), member), \ next = list_entry_next(item, member); \ &item->member != list; \ item = next, \ next = list_entry_next(item, member)) \ #endif linux-msm-qdl-6e625aa/src/meson.build000066400000000000000000000031361527206113100175660ustar00rootroot00000000000000# Source lists for the qdl binaries. Paths here are relative to src/, # so this is the one file to touch when adding or removing a source. # Shared by every executable; built into the qdl_common static library. common_sources = files( 'sahara.c', 'util.c', 'ux.c', 'oscompat.c', 'file.c', ) # Everything except main(); reused by the qdl binary and the nbdkit plugin. lib_sources = files( 'auto.c', 'cli.c', 'qud.c', 'firehose.c', 'input_type.c', 'io.c', 'patch.c', 'program.c', 'programmer.c', 'read.c', 'sahara_config.c', 'sha2.c', 'sim.c', 'ufs.c', 'usb.c', 'vip.c', 'sparse.c', 'gpt.c', 'flashmap.c', 'json.c', 'contents.c', 'pathbuf.c', ) # Zip container support: reading flash bundles (file_zip.c) and the # create-zip subcommand (zipper.c) both need libzip. Without it, # file_nozip.c implements the same entry points as inert stubs. if libzip_dep.found() common_sources += files('file_zip.c') lib_sources += files('zipper.c') else common_sources += files('file_nozip.c') endif # qdl: the full flashing tool (shared sources plus the CLI front-end). qdl_sources = lib_sources + files('qdl.c') # nbdkit plugin exposing a LUN as a block device. nbdkit_plugin_src = files('nbdkit-qdl-plugin.c') # Individual sources reused by the cmocka unit tests. flashmap_src = files('flashmap.c') cli_src = files('cli.c') input_type_src = files('input_type.c') json_src = files('json.c') patch_src = files('patch.c') pathbuf_src = files('pathbuf.c') programmer_src = files('programmer.c') program_src = files('program.c') read_src = files('read.c') ufs_src = files('ufs.c') util_src = files('util.c') linux-msm-qdl-6e625aa/src/nbdkit-qdl-plugin.c000066400000000000000000000123711527206113100211160ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* * nbdkit plugin exposing a Qualcomm EDL device's storage as a block device. * * Based on the plugin from Bjorn Andersson's nbdkit branch, adapted to the * current qdl library interfaces. */ #define NBDKIT_API_VERSION 2 #include #define THREAD_MODEL NBDKIT_THREAD_MODEL_SERIALIZE_ALL_REQUESTS #include #include #include #include #include #include "qdl.h" bool qdl_debug; static const char *config_programmer; static enum qdl_storage_type config_storage = QDL_STORAGE_UFS; static int config_lun = 1; /* * An EDL device is programmed once per session: uploading the firehose * programmer is a one-shot transition and a reset reboots the device out of * firehose. nbdkit, on the other hand, calls .open/.close several times per * client (NBD_OPT_INFO to query metadata, then NBD_OPT_GO for the transfer). * * So the device is set up once, on the first open, kept configured across * connections, and only torn down - with a reset - when the plugin unloads. * THREAD_MODEL_SERIALIZE_ALL_REQUESTS makes the shared state safe. */ static struct qdl_device *dev; static size_t sector_size; static size_t num_sectors; static int qdl_plugin_config(const char *key, const char *value) { if (!strcmp(key, "programmer")) { config_programmer = nbdkit_absolute_path(value); if (!config_programmer) return -1; } else if (!strcmp(key, "storage")) { config_storage = decode_storage_type(value); if (config_storage == QDL_STORAGE_UNKNOWN) { nbdkit_error("unknown storage type '%s'", value); return -1; } } else if (!strcmp(key, "lun")) { config_lun = atoi(value); } else if (!strcmp(key, "debug")) { qdl_debug = !strcmp(value, "true") || !strcmp(value, "1"); } else { nbdkit_error("unknown parameter '%s'", key); return -1; } return 0; } static int qdl_plugin_config_complete(void) { if (!config_programmer) { nbdkit_error("the 'programmer' parameter is required"); return -1; } return 0; } /* Upload the programmer and configure firehose. Runs once, on the first open. */ static int qdl_device_setup(void) { struct sahara_image images[MAPPING_SZ] = {}; struct qdl_device *d; int ret; d = qdl_init(QDL_DEVICE_USB); if (!d) { nbdkit_error("failed to initialize USB backend"); return -1; } if (qdl_open(d, NULL)) { nbdkit_error("failed to open EDL device"); goto err_deinit; } if (load_sahara_image(NULL, config_programmer, &images[SAHARA_ID_EHOSTDL_IMG]) < 0) { nbdkit_error("failed to load programmer '%s'", config_programmer); goto err_close; } ret = sahara_run(d, images, NULL, NULL); sahara_images_free(images, MAPPING_SZ); if (ret < 0) { nbdkit_error("failed to upload programmer"); goto err_close; } if (firehose_open(d, config_storage) < 0) { nbdkit_error("failed to configure firehose programmer"); goto err_close; } if (firehose_getsize(d, config_lun, §or_size, &num_sectors) < 0) { nbdkit_error("failed to query size of LUN %d", config_lun); goto err_close; } dev = d; return 0; err_close: qdl_close(d); err_deinit: qdl_deinit(d); return -1; } static void qdl_plugin_load(void) { ux_set_progname("nbdkit-qdl-plugin"); /* Diagnostics end up in nbdkit's log, never on a terminal of ours */ ux_set_color_mode(UX_COLOR_NEVER); } static void qdl_plugin_unload(void) { if (!dev) return; firehose_reset(dev); qdl_close(dev); qdl_deinit(dev); dev = NULL; } static void *qdl_plugin_open(int readonly) { (void)readonly; if (!dev && qdl_device_setup() < 0) return NULL; /* The device is shared; the handle is only used as a non-NULL token. */ return dev; } static void qdl_plugin_close(void *handle) { /* The device persists across connections; it is torn down in unload. */ (void)handle; } static int64_t qdl_plugin_get_size(void *handle) { (void)handle; return (int64_t)sector_size * num_sectors; } static int qdl_plugin_pread(void *handle, void *buf, uint32_t count, uint64_t offset, uint32_t flags) { (void)handle; (void)flags; if (offset % sector_size || count % sector_size) { nbdkit_error("unaligned read (sector size %zu)", sector_size); errno = EINVAL; return -1; } if (firehose_pread(dev, config_lun, offset / sector_size, buf, sector_size, count / sector_size) < 0) { errno = EIO; return -1; } return 0; } static int qdl_plugin_pwrite(void *handle, const void *buf, uint32_t count, uint64_t offset, uint32_t flags) { (void)handle; (void)flags; if (offset % sector_size || count % sector_size) { nbdkit_error("unaligned write (sector size %zu)", sector_size); errno = EINVAL; return -1; } if (firehose_pwrite(dev, config_lun, offset / sector_size, buf, sector_size, count / sector_size) < 0) { errno = EIO; return -1; } return 0; } static struct nbdkit_plugin plugin = { .name = "qdl", .description = "nbdkit Qualcomm Download plugin", .load = qdl_plugin_load, .unload = qdl_plugin_unload, .config = qdl_plugin_config, .config_complete = qdl_plugin_config_complete, .open = qdl_plugin_open, .close = qdl_plugin_close, .get_size = qdl_plugin_get_size, .pread = qdl_plugin_pread, .pwrite = qdl_plugin_pwrite, }; NBDKIT_REGISTER_PLUGIN(plugin) linux-msm-qdl-6e625aa/src/oscompat.c000066400000000000000000000007651527206113100174220ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifdef _WIN32 #include #include #include #include "oscompat.h" void timeradd(const struct timeval *a, const struct timeval *b, struct timeval *result) { result->tv_sec = a->tv_sec + b->tv_sec; result->tv_usec = a->tv_usec + b->tv_usec; if (result->tv_usec >= 1000000) { result->tv_sec += 1; result->tv_usec -= 1000000; } } #endif // _WIN32 linux-msm-qdl-6e625aa/src/oscompat.h000066400000000000000000000071511527206113100174230ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __OSCOMPAT_H__ #define __OSCOMPAT_H__ #include #include #include #include #include #include #ifndef _WIN32 #include #include #include #define O_BINARY 0 #else // _WIN32 #include #include void timeradd(const struct timeval *a, const struct timeval *b, struct timeval *result); #endif /** * path_is_absolute() - check if a path is absolute * @path: path string to check * * On POSIX systems, a path starting with '/' is absolute. * On Windows, absolute paths are either drive-letter paths (e.g. "C:\...") * or UNC paths (e.g. "\\server\share"). * * Returns: true if @path is absolute, false otherwise */ static inline bool path_is_absolute(const char *path) { #ifndef _WIN32 return path[0] == '/'; #else if (path[0] == '\0') return false; return (isalpha((unsigned char)path[0]) && path[1] == ':') || (path[0] == '\\' && path[1] == '\\'); #endif } /** * qdl_open_device_node() - open a device node for binary protocol traffic * @path: device node to open, e.g. "/dev/mhi0_QAIC_SAHARA" or "/dev/ttyUSB0" * * Opens @path read-write in binary mode, so that the Windows CRT does not * rewrite 0x0a on the way out or end a read at the first 0x1a. * * Serial ports arrive in canonical mode, where the line discipline echoes * input and rewrites the stream; a protocol carrying binary payloads cannot * survive that, so a node that turns out to be a terminal is switched to raw * mode. Nodes that are not terminals - character devices such as the MHI * Sahara endpoints - pass data through untouched and need no such setup. * * Returns: an open file descriptor, or -1 with errno set on failure. */ static inline int qdl_open_device_node(const char *path) { int fd; #ifndef _WIN32 struct termios tio; int saved_errno; fd = open(path, O_RDWR | O_BINARY); if (fd < 0) return -1; if (!isatty(fd)) return fd; if (tcgetattr(fd, &tio) < 0) goto err_close; cfmakeraw(&tio); if (tcsetattr(fd, TCSANOW, &tio) < 0) goto err_close; return fd; err_close: saved_errno = errno; close(fd); errno = saved_errno; return -1; #else fd = open(path, O_RDWR | O_BINARY); if (fd < 0) return -1; return fd; #endif } /** * qdl_mkdir_p() - create @path and any missing parent directories * @path: directory path to create * * Mirrors "mkdir -p": an already existing directory is not an error. * Both '/' and (on Windows) '\\' are treated as path separators. * * Returns: 0 on success, -1 with errno set on failure. */ static inline int qdl_mkdir_p(const char *path) { char tmp[PATH_MAX]; size_t len; size_t i; len = strlen(path); if (len == 0) return 0; if (len >= sizeof(tmp)) { errno = ENAMETOOLONG; return -1; } memcpy(tmp, path, len + 1); /* Create each prefix in turn, then the full path at the terminator. */ for (i = 1; i <= len; i++) { char c = tmp[i]; char prev = tmp[i - 1]; bool sep = c == '\0' || c == '/'; bool prev_sep = prev == '/'; char saved; #ifdef _WIN32 sep = sep || c == '\\'; prev_sep = prev_sep || prev == '\\'; /* Don't try to create a bare drive prefix such as "C:". */ if (sep && prev == ':') continue; #endif /* Act only at separators, and skip empty components. */ if (!sep || prev_sep) continue; saved = tmp[i]; tmp[i] = '\0'; #ifdef _WIN32 if (_mkdir(tmp) != 0 && errno != EEXIST) #else if (mkdir(tmp, 0755) != 0 && errno != EEXIST) #endif return -1; tmp[i] = saved; } return 0; } #endif linux-msm-qdl-6e625aa/src/patch.c000066400000000000000000000040011527206113100166570ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2016-2017, Linaro Ltd. * All rights reserved. */ #include #include #include #include #include #include #include #include "patch.h" #include "firehose.h" #include "qdl.h" int patch_load_xml(struct list_head *ops, xmlDoc *doc, const char *patch_file) { struct firehose_op *patch; xmlNode *node; xmlNode *root; int errors; root = xmlDocGetRootElement(doc); for (node = root->children; node ; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (xmlStrcmp(node->name, (xmlChar *)"patch")) { ux_err("unrecognized tag \"%s\" in patch-type file, ignoring\n", node->name); continue; } errors = 0; patch = firehose_alloc_op(FIREHOSE_OP_PATCH); if (!patch) return -ENOMEM; patch->sector_size = attr_as_unsigned(node, "SECTOR_SIZE_IN_BYTES", &errors); patch->byte_offset = attr_as_unsigned(node, "byte_offset", &errors); patch->filename = attr_as_string(node, "filename", &errors); patch->partition = attr_as_unsigned(node, "physical_partition_number", &errors); patch->size_in_bytes = attr_as_unsigned(node, "size_in_bytes", &errors); patch->start_sector = attr_as_string(node, "start_sector", &errors); patch->value = attr_as_string(node, "value", &errors); patch->what = attr_as_string(node, "what", &errors); if (errors) { ux_err("errors while parsing patch-type file \"%s\"\n", patch_file); free((void *)patch->filename); free((void *)patch->start_sector); free((void *)patch->value); free((void *)patch->what); free(patch); return -EINVAL; } list_append(ops, &patch->node); } return 0; } int patch_load(struct list_head *ops, const char *patch_file) { xmlDoc *doc; int ret; doc = xmlReadFile(patch_file, NULL, 0); if (!doc) { ux_err("failed to parse patch-type file \"%s\"\n", patch_file); return -EINVAL; } ret = patch_load_xml(ops, doc, patch_file); xmlFreeDoc(doc); return ret; } linux-msm-qdl-6e625aa/src/patch.h000066400000000000000000000006171527206113100166750ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __PATCH_H__ #define __PATCH_H__ #include #include "list.h" struct qdl_device; struct firehose_op; int patch_load(struct list_head *ops, const char *patch_file); int patch_load_xml(struct list_head *ops, xmlDoc *doc, const char *patch_file); #endif linux-msm-qdl-6e625aa/src/pathbuf.c000066400000000000000000000065311527206113100172230ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #include #include #include #include "oscompat.h" #include "pathbuf.h" static bool qdl_path_is_sep(char ch) { #ifdef _WIN32 return ch == '/' || ch == '\\'; #else return ch == '/'; #endif } static char *qdl_pathbuf_last_sep(char *path) { char *last = NULL; for (; *path; path++) { if (qdl_path_is_sep(*path)) last = path; } return last; } #ifdef _WIN32 static size_t qdl_pathbuf_unc_root_len(const char *path) { size_t i; size_t start; if (!qdl_path_is_sep(path[0]) || !qdl_path_is_sep(path[1])) return 0; i = 2; while (qdl_path_is_sep(path[i])) i++; if (!path[i]) return 2; start = i; while (path[i] && !qdl_path_is_sep(path[i])) i++; if (i == start) return 2; while (qdl_path_is_sep(path[i])) i++; if (!path[i]) return 2; start = i; while (path[i] && !qdl_path_is_sep(path[i])) i++; if (i == start) return 2; return i; } #endif void qdl_pathbuf_reset(struct pathbuf *path) { if (!path) return; path->buf[0] = '\0'; path->len = 0; } void qdl_pathbuf_dup(struct pathbuf *dst, const struct pathbuf *orig) { memcpy(dst, orig, sizeof(struct pathbuf)); } int qdl_pathbuf_push(struct pathbuf *path, const char *component) { size_t component_len; size_t path_len; size_t skip = 0; size_t need_sep = 0; if (!path || !component) return -EINVAL; if (component[0] == '\0') return 0; path_len = path->len; if (path_is_absolute(component)) path_len = 0; if (component[0] == '.' && qdl_path_is_sep(component[1])) skip += 2; if (path_len > 0) need_sep = !qdl_path_is_sep(path->buf[path_len - 1]); if (path_len > 0 && !need_sep) { while (qdl_path_is_sep(component[skip])) skip++; } component_len = strlen(component + skip); if (component_len == 0) return 0; if (path_len + need_sep + component_len + 1 > sizeof(path->buf)) return -ENAMETOOLONG; if (need_sep) path->buf[path_len++] = '/'; #ifndef _WIN32 memcpy(path->buf + path_len, component + skip, component_len + 1); #else for (size_t i = 0; i <= component_len; i++) path->buf[path_len + i] = qdl_path_is_sep(component[skip + i]) ? '/' : component[skip + i]; #endif path->len = path_len + component_len; return 0; } const char *qdl_pathbuf_str(const struct pathbuf *path) { return path ? path->buf : NULL; } void qdl_pathbuf_dirname(struct pathbuf *path) { size_t root_len = 0; char *sep; if (!path || !path->buf[0]) return; if (path->len == 0) path->len = strlen(path->buf); #ifdef _WIN32 if (isalpha((unsigned char)path->buf[0]) && path->buf[1] == ':' && qdl_path_is_sep(path->buf[2])) { root_len = 3; } else { root_len = qdl_pathbuf_unc_root_len(path->buf); if (root_len == 0 && qdl_path_is_sep(path->buf[0])) root_len = 1; } #else if (path->buf[0] == '/') root_len = 1; #endif while (path->len > root_len && qdl_path_is_sep(path->buf[path->len - 1])) { path->buf[--path->len] = '\0'; } sep = qdl_pathbuf_last_sep(path->buf); if (!sep) return; if ((size_t)(sep - path->buf) < root_len) { path->buf[root_len] = '\0'; path->len = root_len; return; } *sep = '\0'; path->len = (size_t)(sep - path->buf); } bool qdl_path_exists(const struct pathbuf *path) { struct stat st; if (!path || !path->buf[0]) return false; return stat(path->buf, &st) == 0; } linux-msm-qdl-6e625aa/src/pathbuf.h000066400000000000000000000011701527206113100172220ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __PATHBUF_H__ #define __PATHBUF_H__ #include #include #include struct pathbuf { char buf[PATH_MAX]; size_t len; }; void qdl_pathbuf_reset(struct pathbuf *path); void qdl_pathbuf_dup(struct pathbuf *dst, const struct pathbuf *orig); int qdl_pathbuf_push(struct pathbuf *path, const char *component); const char *qdl_pathbuf_str(const struct pathbuf *path); void qdl_pathbuf_dirname(struct pathbuf *path); bool qdl_path_exists(const struct pathbuf *path); #endif linux-msm-qdl-6e625aa/src/program.c000066400000000000000000000340641527206113100172430ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2016-2017, Linaro Ltd. * All rights reserved. */ #include "contents.h" #include "pathbuf.h" #include #include #include #include #include #include #include #include #include #include #include #include "program.h" #include "file.h" #include "qdl.h" #include "firehose.h" #include "sparse.h" #include "gpt.h" static int load_erase_tag(struct list_head *ops, xmlNode *node, bool is_nand) { struct firehose_op *program; int errors = 0; program = firehose_alloc_op(FIREHOSE_OP_ERASE); if (!program) return -1; program->is_nand = is_nand; program->sector_size = attr_as_unsigned(node, "SECTOR_SIZE_IN_BYTES", &errors); program->num_sectors = attr_as_unsigned(node, "num_partition_sectors", &errors); program->partition = attr_as_unsigned(node, "physical_partition_number", &errors); program->start_sector = attr_as_string(node, "start_sector", &errors); if (is_nand) { program->pages_per_block = attr_as_unsigned(node, "PAGES_PER_BLOCK", &errors); } if (errors) { ux_err("errors while parsing erase tag\n"); free((void *)program->start_sector); free(program); return -EINVAL; } if (!program->num_sectors) { ux_err("erase tag with num_sectors=0 not allowed\n"); free((void *)program->start_sector); free(program); return -EINVAL; } list_append(ops, &program->node); return 0; } static int program_load_sparse(struct list_head *ops, struct firehose_op *program, struct qdl_file *file) { struct firehose_op *program_sparse = NULL; char tmp[PATH_MAX]; sparse_header_t sparse_header; unsigned int start_sector; uint32_t sparse_fill_value; uint64_t chunk_size; off_t sparse_offset; int chunk_type; if (sparse_header_parse(file, &sparse_header)) { /* * If the XML tag "program" contains the attribute 'sparse="true"' * for a partition node but lacks a sparse header, * it will be validated against the defined partition size. * If the sizes match, it is likely that the 'sparse="true"' attribute * was set by mistake, fix the sparse flag and let the caller add the * program entry to the list as a regular (non-sparse) program. */ if ((off_t)program->sector_size * program->num_sectors == qdl_file_seek(file, 0, SEEK_END)) { program->sparse = false; return 1; } ux_err_tag(UX_TAG_SPARSE, "Unable to parse sparse header at %s...failed\n", program->filename); return -1; } if (program->sector_size == 0) { ux_err_tag(UX_TAG_SPARSE, "SECTOR_SIZE_IN_BYTES is zero for sparse image %s\n", program->filename); return -1; } if (!program->start_sector) { ux_err_tag(UX_TAG_SPARSE, "missing start_sector for sparse image %s\n", program->filename); return -1; } for (uint32_t i = 0; i < sparse_header.total_chunks; ++i) { chunk_type = sparse_chunk_header_parse(file, &sparse_header, &chunk_size, &sparse_fill_value, &sparse_offset); if (chunk_type < 0) { ux_err_tag(UX_TAG_SPARSE, "Unable to parse sparse chunk %i at %s...failed\n", i, program->filename); return -1; } if (chunk_size == 0) continue; if (chunk_size % program->sector_size != 0) { ux_err_tag(UX_TAG_SPARSE, "File chunk #%u size %" PRIu64 " is not a sector-multiple\n", i, chunk_size); return -1; } if (chunk_size / program->sector_size >= UINT_MAX) { /* * Perhaps the programmer can handle larger "num_sectors"? * Let's cap it for now, it's big enough for now... */ ux_err_tag(UX_TAG_SPARSE, "File chunk #%u size %" PRIu64 " is too large\n", i, chunk_size); return -1; } if (chunk_type == CHUNK_TYPE_RAW || chunk_type == CHUNK_TYPE_FILL) { program_sparse = firehose_alloc_op(FIREHOSE_OP_PROGRAM); if (!program_sparse) return -1; program_sparse->pages_per_block = program->pages_per_block; program_sparse->sector_size = program->sector_size; program_sparse->file_offset = program->file_offset; program_sparse->filename = strdup(program->filename); program_sparse->label = program->label ? strdup(program->label) : NULL; program_sparse->partition = program->partition; program_sparse->sparse = program->sparse; program_sparse->start_sector = strdup(program->start_sector); program_sparse->last_sector = program->last_sector; program_sparse->is_nand = program->is_nand; program_sparse->sparse_chunk_type = chunk_type; program_sparse->num_sectors = chunk_size / program->sector_size; if (chunk_type == CHUNK_TYPE_RAW) program_sparse->sparse_offset = sparse_offset; else program_sparse->sparse_fill_value = sparse_fill_value; list_append(ops, &program_sparse->node); } start_sector = (unsigned int)strtoul(program->start_sector, NULL, 0); start_sector += chunk_size / program->sector_size; sprintf(tmp, "%u", start_sector); free((void *)program->start_sector); program->start_sector = strdup(tmp); } return 0; } static int program_resolve_path(struct firehose_op *program, const char *program_file, struct contents_filter *contents_filter, const char *incdir) { char *program_file_copy; char candidate[PATH_MAX]; const char *filename = program->filename; const char *program_dir; struct pathbuf pathbuf = {}; char *resolved; size_t len; int ret; /* Don't attempt to resolve filenames in zip files */ if (program->zip) return 0; /* Attempt to look up the file in the contents database */ ret = contents_resolve_path(contents_filter, filename, &pathbuf); if (ret == 1) { resolved = strdup(qdl_pathbuf_str(&pathbuf)); if (!resolved) return -1; free((void *)program->filename); program->filename = resolved; return 0; } /* Look for the file in include directory */ if (incdir) { snprintf(candidate, sizeof(candidate), "%s/%s", incdir, filename); if (!access(candidate, F_OK)) { resolved = strdup(candidate); if (!resolved) return -ENOMEM; free((void *)program->filename); program->filename = resolved; return 0; } } /* Look for the file adjacent to the program XML */ len = strlen(program_file) + 1; program_file_copy = alloca(len); memcpy(program_file_copy, program_file, len); program_dir = dirname(program_file_copy); if (strcmp(program_dir, ".")) { ret = snprintf(candidate, sizeof(candidate), "%s/%s", program_dir, filename); if (ret < 0 || (size_t)ret >= sizeof(candidate)) return -ENAMETOOLONG; if (!access(candidate, F_OK)) { resolved = strdup(candidate); if (!resolved) return -ENOMEM; free((void *)program->filename); program->filename = resolved; } } return 0; } static int load_program_tag(struct list_head *ops, xmlNode *node, bool is_nand, bool allow_missing, struct qdl_zip *zip, const char *program_file, struct contents_filter *contents_filter, const char *incdir) { struct firehose_op *program; struct qdl_file file = {}; int errors = 0; int ret; program = firehose_alloc_op(FIREHOSE_OP_PROGRAM); if (!program) return -1; program->is_nand = is_nand; program->sector_size = attr_as_unsigned(node, "SECTOR_SIZE_IN_BYTES", &errors); program->zip = qdl_zip_get(zip); program->filename = attr_as_string(node, "filename", &errors); program->label = attr_as_string(node, "label", &errors); program->num_sectors = attr_as_unsigned(node, "num_partition_sectors", &errors); program->partition = attr_as_unsigned(node, "physical_partition_number", &errors); program->sparse = attr_as_bool(node, "sparse", &errors); program->start_sector = attr_as_string(node, "start_sector", &errors); if (is_nand) { program->pages_per_block = attr_as_unsigned(node, "PAGES_PER_BLOCK", &errors); if (xmlGetProp(node, (xmlChar *)"last_sector")) { program->last_sector = attr_as_unsigned(node, "last_sector", &errors); } } else { program->file_offset = attr_as_unsigned(node, "file_sector_offset", &errors); } if (errors) { ux_err("errors while parsing program tag\n"); goto err_free_op; } if (program->filename) { ret = program_resolve_path(program, program_file, contents_filter, incdir); if (ret < 0) goto err_free_op; ret = qdl_file_open(zip, program->filename, &file); if (ret < 0) { ux_info("unable to open %s", program->filename); if (!allow_missing) { ux_info("...failing\n"); goto err_free_op; } ux_info("...ignoring\n"); free((void *)program->filename); program->filename = NULL; } } if (program->filename && program->sparse) { ret = program_load_sparse(ops, program, &file); if (ret < 0) goto err_free_op; /* * ret == 0 means the sparse chunks were added to the program * list as separate ops; drop the filename of the parent to * prevent it from being written to the device. ret == 1 means * the image was not actually sparse and the parent op should be * programmed normally, so keep its filename. */ if (ret == 0) { free((void *)program->filename); program->filename = NULL; } } list_append(ops, &program->node); qdl_file_close(&file); return 0; err_free_op: qdl_file_close(&file); qdl_zip_put(program->zip); free((void *)program->filename); free((void *)program->label); free((void *)program->start_sector); free(program); return -1; } int program_load_xml(struct list_head *ops, xmlDoc *doc, struct qdl_zip *zip, const char *program_file, bool is_nand, bool allow_missing, struct contents_filter *contents_filter, const char *incdir) { xmlNode *node; xmlNode *root; int errors = 0; root = xmlDocGetRootElement(doc); for (node = root->children; node ; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (!xmlStrcmp(node->name, (xmlChar *)"erase")) errors = load_erase_tag(ops, node, is_nand); else if (!xmlStrcmp(node->name, (xmlChar *)"program")) errors = load_program_tag(ops, node, is_nand, allow_missing, zip, program_file, contents_filter, incdir); else { ux_err("unrecognized tag \"%s\" in program-type file \"%s\"\n", node->name, program_file); errors = -EINVAL; } if (errors) break; } return errors; } int program_load(struct list_head *ops, const char *program_file, bool is_nand, bool allow_missing, struct contents_filter *contents_filter, const char *incdir) { xmlDoc *doc; int errors; doc = xmlReadFile(program_file, NULL, 0); if (!doc) { ux_err("failed to parse program-type file \"%s\"\n", program_file); return -EINVAL; } errors = program_load_xml(ops, doc, NULL, program_file, is_nand, allow_missing, contents_filter, incdir); xmlFreeDoc(doc); return errors; } int erase_execute(struct qdl_device *qdl, struct firehose_op *op, int (*apply)(struct qdl_device *qdl, struct firehose_op *op)) { int ret; assert(op->type == FIREHOSE_OP_ERASE); ret = apply(qdl, op); if (ret) return ret; return 0; } static struct firehose_op *program_find_partition(struct list_head *ops, const char *partition) { struct firehose_op *program; const char *label; list_for_each_entry(program, ops, node) { if (program->type != FIREHOSE_OP_PROGRAM) continue; label = program->label; if (!label) continue; if (!strcmp(label, partition)) return program; } return NULL; } /** * program_find_bootable_partition() - find one bootable partition * * Returns partition number, or negative errno on failure. * * Scan program tags for a partition with the label "sbl1", "xbl" or "xbl_a" * and return the partition number for this. If more than one line matches * we're informing the caller so that they can warn the user about the * uncertainty of this logic. */ int program_find_bootable_partition(struct list_head *ops, bool *multiple_found) { struct firehose_op *program; int part = -ENOENT; *multiple_found = false; program = program_find_partition(ops, "xbl"); if (program) part = program->partition; program = program_find_partition(ops, "xbl_a"); if (program) { if (part != -ENOENT) *multiple_found = true; else part = program->partition; } program = program_find_partition(ops, "sbl1"); if (program) { if (part != -ENOENT) *multiple_found = true; else part = program->partition; } return part; } /** * program_is_sec_partition_flashed() - find if secdata partition is flashed * * Returns true if filename for secdata is set in program*.xml, * or false otherwise. */ int program_is_sec_partition_flashed(struct list_head *ops) { struct firehose_op *program; program = program_find_partition(ops, "secdata"); if (!program) return false; if (program->filename) return true; return false; } int program_cmd_add(struct list_head *ops, const char *address, const char *filename) { unsigned int start_sector; unsigned int num_sectors; struct firehose_op *program; char *gpt_partition; int partition; char buf[20]; int ret; ret = parse_storage_address(address, &partition, &start_sector, &num_sectors, &gpt_partition); if (ret < 0) return ret; program = firehose_alloc_op(FIREHOSE_OP_PROGRAM); if (!program) { ux_err("failed to allocate program command\n"); return -1; } program->sector_size = 0; program->file_offset = 0; program->filename = filename ? strdup(filename) : NULL; program->label = filename ? strdup(filename) : NULL; program->num_sectors = num_sectors; program->partition = partition; program->sparse = false; sprintf(buf, "%u", start_sector); program->start_sector = strdup(buf); program->last_sector = 0; program->is_nand = false; program->gpt_partition = gpt_partition; list_append(ops, &program->node); return 0; } int erase_cmd_add(struct list_head *ops, const char *address) { unsigned int start_sector; unsigned int num_sectors; struct firehose_op *program; char *gpt_partition; int partition; char buf[20]; int ret; ret = parse_storage_address(address, &partition, &start_sector, &num_sectors, &gpt_partition); if (ret < 0) return ret; program = firehose_alloc_op(FIREHOSE_OP_ERASE); if (!program) { ux_err("failed to allocate erase command\n"); return -1; } program->num_sectors = num_sectors; program->partition = partition; sprintf(buf, "%u", start_sector); program->start_sector = strdup(buf); program->gpt_partition = gpt_partition; list_append(ops, &program->node); return 0; } linux-msm-qdl-6e625aa/src/program.h000066400000000000000000000022121527206113100172360ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __PROGRAM_H__ #define __PROGRAM_H__ #include #include #include #include #include "list.h" struct qdl_device; struct firehose_op; struct qdl_zip; struct contents_filter; int program_load(struct list_head *ops, const char *program_file, bool is_nand, bool allow_missing, struct contents_filter *contents_filter, const char *incdir); int program_load_xml(struct list_head *ops, xmlDoc *doc, struct qdl_zip *zip, const char *program_file, bool is_nand, bool allow_missing, struct contents_filter *contents_filter, const char *incdir); int erase_execute(struct qdl_device *qdl, struct firehose_op *op, int (*apply)(struct qdl_device *qdl, struct firehose_op *op)); int program_find_bootable_partition(struct list_head *ops, bool *multiple_found); int program_is_sec_partition_flashed(struct list_head *ops); int program_cmd_add(struct list_head *ops, const char *address, const char *filename); int erase_cmd_add(struct list_head *ops, const char *address); #endif linux-msm-qdl-6e625aa/src/programmer.c000066400000000000000000000176411527206113100177510ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* * Sahara programmer handling: mapping the user's programmer specifier * onto the Sahara image table, and reading and writing the CPIO-based * programmer archives that carry multi-image boot chains. */ #include #include #include #include #include #include #include #include "qdl.h" #include "oscompat.h" #include "programmer.h" #define CPIO_MAGIC "070701" struct cpio_newc_header { char c_magic[6]; /* "070701" */ char c_ino[8]; char c_mode[8]; char c_uid[8]; char c_gid[8]; char c_nlink[8]; char c_mtime[8]; char c_filesize[8]; char c_devmajor[8]; char c_devminor[8]; char c_rdevmajor[8]; char c_rdevminor[8]; char c_namesize[8]; char c_check[8]; }; static uint32_t parse_ascii_hex32(const char *s) { uint32_t x = 0; for (int i = 0; i < 8; i++) { if (!isxdigit(s[i])) ux_die(1, "non-hex-digit found in archive header"); if (s[i] <= '9') x = (x << 4) | (s[i] - '0'); else x = (x << 4) | (10 + (s[i] | 32) - 'a'); } return x; } /** * decode_programmer_archive() - Attempt to decode a programmer CPIO archive * @blob: Loaded image to be decoded as archive * @images: List of Sahara images to populate * * The blob might be a CPIO archive containing Sahara images, in files with * names in the format ":". Load each such Sahara image into the * relevant spot in the @images array. * * The blob is always consumed (freed) on both success and error paths. * On error, any partially-populated @images entries are also freed. * * Returns: 0 if no archive was found, 1 if archive was decoded, -1 on error */ int decode_programmer_archive(struct sahara_image *blob, struct sahara_image *images) { struct cpio_newc_header *hdr; size_t filesize; size_t namesize; char name[128]; char *save; char *tok; void *ptr = blob->ptr; void *end = blob->ptr + blob->len; long id; if (blob->len < sizeof(*hdr) || memcmp(ptr, CPIO_MAGIC, 6)) return 0; for (;;) { if (ptr + sizeof(*hdr) > end) { ux_err("programmer archive is truncated\n"); goto err; } hdr = ptr; if (memcmp(hdr->c_magic, "070701", 6)) { ux_err("expected cpio header in programmer archive\n"); goto err; } filesize = parse_ascii_hex32(hdr->c_filesize); namesize = parse_ascii_hex32(hdr->c_namesize); ptr += sizeof(*hdr); if (ptr + namesize > end || ptr + filesize + namesize > end) { ux_err("programmer archive is truncated\n"); goto err; } if (namesize == 0 || namesize > sizeof(name)) { ux_err("unexpected filename length in programmer archive\n"); goto err; } memcpy(name, ptr, namesize); if (name[namesize - 1] != '\0') { ux_err("malformed filename in programmer archive\n"); goto err; } if (!strcmp(name, "TRAILER!!!")) break; tok = strtok_r(name, ":", &save); if (!tok) { ux_err("missing image id in programmer archive entry\n"); goto err; } id = strtoul(tok, NULL, 0); if (id <= 0 || id >= MAPPING_SZ) { ux_err("invalid image id \"%s\" in programmer archive\n", tok); goto err; } ptr += namesize; ptr = ALIGN_UP(ptr, 4); tok = strtok_r(NULL, ":", &save); if (tok) images[id].name = strdup(tok); images[id].len = filesize; images[id].ptr = malloc(filesize); memcpy(images[id].ptr, ptr, filesize); ptr += filesize; ptr = ALIGN_UP(ptr, 4); } free(blob->ptr); blob->ptr = NULL; blob->len = 0; return 1; err: sahara_images_free(images, MAPPING_SZ); free(blob->ptr); blob->ptr = NULL; blob->len = 0; return -1; } /** * decode_programmer() - decodes the programmer specifier * @s: programmer specifier, from the user * @images: array of images to populate * * This parses the programmer specifier @s, which can either be a single * filename, or a comma-separated series of : entries. * * In the first case an attempt will be made to decode the Sahara archive and * each programmer part will be loaded into their requested @images entry. If * the file isn't an archive @images[SAHARA_ID_EHOSTDL_IMG] is assigned. In the * second case, each comma-separated entry will be split on ':' and the given * will be assigned to the @image entry indicated by the given . * * Memory is not allocated for the various strings, instead @s will be modified * by the tokenizer and pointers to the individual parts will be stored in the * @images array. * * Returns: 0 on success, -1 otherwise. */ int decode_programmer(char *s, struct sahara_image *images) { struct sahara_image archive; char *filename; char *save1; char *pair; char *tail; long id; int ret; strtoul(s, &tail, 0); if (tail != s && tail[0] == ':') { for (pair = strtok_r(s, ",", &save1); pair; pair = strtok_r(NULL, ",", &save1)) { id = strtoul(pair, &tail, 0); if (tail == pair) { ux_err("invalid programmer specifier\n"); return -1; } if (id <= 0 || id >= MAPPING_SZ) { ux_err("invalid image id \"%s\"\n", pair); return -1; } filename = &tail[1]; ret = load_sahara_image(NULL, filename, &images[id]); if (ret < 0) return -1; } } else { ret = load_sahara_image(NULL, s, &archive); if (ret < 0) return -1; ret = decode_programmer_archive(&archive, images); if (ret < 0 || ret == 1) return ret; ret = decode_sahara_config(&archive, images, NULL); if (ret < 0 || ret == 1) return ret; images[SAHARA_ID_EHOSTDL_IMG] = archive; } return 0; } static char *qdl_basename_dup(const char *path) { char *tmp; char *base; tmp = strdup(path); if (!tmp) return NULL; base = basename(tmp); base = base ? strdup(base) : NULL; free(tmp); return base; } static int sahara_archive_pad(FILE *out, size_t len) { static const char zeros[4]; size_t pad = ROUND_UP(len, 4) - len; if (!pad) return 0; return fwrite(zeros, 1, pad, out) == pad ? 0 : -1; } static int sahara_archive_write_entry(FILE *out, unsigned int ino, const char *name, const void *data, size_t len) { char header[sizeof(struct cpio_newc_header) + 1]; size_t namesize = strlen(name) + 1; if (len > UINT32_MAX || namesize > UINT32_MAX) { ux_err("sahara archive entry \"%s\" is too large\n", name); return -1; } snprintf(header, sizeof(header), "%s%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x", CPIO_MAGIC, ino, 0100644, 0, 0, 1, 0, (unsigned int)len, 0, 0, 0, 0, (unsigned int)namesize, 0); if (fwrite(header, 1, sizeof(struct cpio_newc_header), out) != sizeof(struct cpio_newc_header)) return -1; if (fwrite(name, 1, namesize, out) != namesize) return -1; if (sahara_archive_pad(out, sizeof(struct cpio_newc_header) + namesize) < 0) return -1; if (len && fwrite(data, 1, len, out) != len) return -1; if (sahara_archive_pad(out, len) < 0) return -1; return 0; } int sahara_archive_write(const char *filename, const struct sahara_image *images) { char *basename = NULL; char name[128]; unsigned int ino = 1; unsigned int count = 0; FILE *out; int ret = -1; int i; for (i = 1; i < MAPPING_SZ; i++) { if (images[i].ptr) count++; } if (!count) { ux_err("no Sahara images found for archive\n"); return -1; } out = fopen(filename, "wb"); if (!out) { ux_err("failed to create \"%s\": %s\n", filename, strerror(errno)); return -1; } for (i = 1; i < MAPPING_SZ; i++) { if (!images[i].ptr) continue; free(basename); basename = images[i].name ? qdl_basename_dup(images[i].name) : NULL; if (basename) snprintf(name, sizeof(name), "%d:%s", i, basename); else snprintf(name, sizeof(name), "%d", i); if (sahara_archive_write_entry(out, ino++, name, images[i].ptr, images[i].len) < 0) goto out_close; } ret = sahara_archive_write_entry(out, ino++, "TRAILER!!!", NULL, 0); out_close: free(basename); if (fclose(out) && ret == 0) ret = -1; if (ret < 0) { ux_err("failed to write Sahara archive \"%s\"\n", filename); remove(filename); } return ret; } linux-msm-qdl-6e625aa/src/programmer.h000066400000000000000000000006611527206113100177500ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __PROGRAMMER_H__ #define __PROGRAMMER_H__ struct sahara_image; int decode_programmer(char *s, struct sahara_image *images); int decode_programmer_archive(struct sahara_image *blob, struct sahara_image *images); int sahara_archive_write(const char *filename, const struct sahara_image *images); #endif linux-msm-qdl-6e625aa/src/qdl.c000066400000000000000000000545021527206113100163530ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2016-2017, Linaro Ltd. * Copyright (c) 2018, The Linux Foundation. All rights reserved. * All rights reserved. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "qdl.h" #include "contents.h" #include "cli.h" #include "input_type.h" #include "programmer.h" #include "file.h" #include "firehose.h" #include "flashmap.h" #include "patch.h" #include "pathbuf.h" #include "program.h" #include "ufs.h" #include "oscompat.h" #include "vip.h" #define MAX_USBFS_BULK_SIZE (16 * 1024) bool qdl_debug; static int qdl_list(FILE *out) { struct qdl_device_desc *usb_devices; struct qud_device_desc *qud_devices; unsigned int usb_count = 0; unsigned int qud_count = 0; unsigned int i; usb_devices = usb_list(&usb_count); qud_devices = qud_list(&qud_count); if (usb_count == 0 && qud_count == 0) { fprintf(out, "No devices found\n"); } else { for (i = 0; i < usb_count; i++) fprintf(out, "%04x:%04x\t%s\n", usb_devices[i].vid, usb_devices[i].pid, usb_devices[i].serial); for (i = 0; i < qud_count; i++) fprintf(out, "05c6:%04x\t%s\t%s\n", qud_devices[i].pid, qud_devices[i].serial, qud_devices[i].path); } free(usb_devices); free(qud_devices); return 0; } /* Results of qdl_common_opt() for options every subcommand shares. */ enum { QDL_OPT_HANDLED, QDL_OPT_EXIT_OK, QDL_OPT_EXIT_FAIL, }; /* Long options for subcommands that take no extra options. */ static const struct option qdl_common_options[] = { {"debug", no_argument, 0, 'd'}, {"version", no_argument, 0, 'v'}, {"serial", required_argument, 0, 'S'}, {"backend", required_argument, 0, OPT_BACKEND}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; /* * Handle the option cases shared by every subcommand: --debug, * --version, --serial, --backend and --help, plus the unknown-option * fallback. Returns QDL_OPT_HANDLED when parsing should continue, or * QDL_OPT_EXIT_OK / QDL_OPT_EXIT_FAIL when the caller should return * with success / failure. */ static int qdl_common_opt(int opt, char **serial, enum QDL_DEVICE_TYPE *dev_type) { switch (opt) { case 'd': qdl_debug = true; return QDL_OPT_HANDLED; case 'v': print_version(); return QDL_OPT_EXIT_OK; case 'S': case OPT_SERIAL: *serial = optarg; return QDL_OPT_HANDLED; case OPT_BACKEND: if (decode_backend(optarg, dev_type) < 0) ux_diex(1, "unknown backend \"%s\" (expected auto|usb|qud)", optarg); return QDL_OPT_HANDLED; case 'h': print_usage(stdout); return QDL_OPT_EXIT_OK; default: print_usage(stderr); return QDL_OPT_EXIT_FAIL; } } /* * Common device session setup for the subcommands: initialize the * requested backend and open the (optionally serial-selected) device. * Returns NULL with the error already reported on failure. */ static struct qdl_device *qdl_session_open(enum QDL_DEVICE_TYPE dev_type, const char *serial) { struct qdl_device *qdl; qdl = qdl_init(dev_type); if (!qdl) { ux_err("backend not available\n"); return NULL; } if (qdl_debug) print_version(); if (qdl_open(qdl, serial)) { qdl_deinit(qdl); return NULL; } return qdl; } static void qdl_session_close(struct qdl_device *qdl) { qdl_close(qdl); qdl_deinit(qdl); } static int qdl_ramdump(int argc, char **argv) { struct qdl_device *qdl; char *ramdump_path = "."; char *filter = NULL; char *serial = NULL; enum QDL_DEVICE_TYPE qdl_dev_type = QDL_DEVICE_AUTO; int ret = 0; int opt; static struct option options[] = { {"debug", no_argument, 0, 'd'}, {"version", no_argument, 0, 'v'}, {"output", required_argument, 0, 'o'}, {"serial", required_argument, 0, 'S'}, {"backend", required_argument, 0, OPT_BACKEND}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; while ((opt = getopt_long(argc, argv, "dvo:S:h", options, NULL)) != -1) { if (opt == 'o') { ramdump_path = optarg; continue; } ret = qdl_common_opt(opt, &serial, &qdl_dev_type); if (ret == QDL_OPT_EXIT_OK) return 0; if (ret == QDL_OPT_EXIT_FAIL) return 1; } if (optind < argc) filter = argv[optind++]; if (optind != argc) { print_usage(stderr); return 1; } ux_init(); if (qdl_mkdir_p(ramdump_path) < 0) { ux_err("failed to create ramdump directory \"%s\": %s\n", ramdump_path, strerror(errno)); return 1; } qdl = qdl_session_open(qdl_dev_type, serial); if (!qdl) return 1; ret = sahara_run(qdl, NULL, ramdump_path, filter) < 0 ? 1 : 0; qdl_session_close(qdl); return ret; } /* * Shared entry point for Sahara-only subcommands that take no options * beyond the common set and no positional arguments: parse the options, * open the device session and hand it to @run. Returns the subcommand * exit code; @run reports failure with a negative return value. */ static int qdl_sahara_cmd(int argc, char **argv, int (*run)(struct qdl_device *qdl)) { struct qdl_device *qdl; char *serial = NULL; enum QDL_DEVICE_TYPE qdl_dev_type = QDL_DEVICE_AUTO; int ret; int opt; while ((opt = getopt_long(argc, argv, "dvS:h", qdl_common_options, NULL)) != -1) { ret = qdl_common_opt(opt, &serial, &qdl_dev_type); if (ret == QDL_OPT_EXIT_OK) return 0; if (ret == QDL_OPT_EXIT_FAIL) return 1; } if (optind != argc) { print_usage(stderr); return 1; } ux_init(); qdl = qdl_session_open(qdl_dev_type, serial); if (!qdl) return 1; ret = run(qdl) < 0 ? 1 : 0; qdl_session_close(qdl); return ret; } /* * Device reset ("reset") subcommand body. * * Resets the device from whichever state it is in: over the Sahara * protocol when the device sits in EDL or crash mode, falling back to a * Firehose power reset when a programmer is already running. Unlike the * "reset" flashing verb, no programmer needs to be uploaded. */ static int qdl_reset_run(struct qdl_device *qdl) { int ret; ret = sahara_device_reset(qdl); if (ret == 1) { ux_info("falling back to Firehose reset\n"); ret = firehose_reset(qdl); } return ret; } /* * Sahara kickstart ("ks") subcommand. * * Kickstart serves Sahara images to devices that fetch their firmware from the * host rather than from local storage, and stops once Sahara is done: no * Firehose programmer is involved and nothing is written to storage. * * Two transports are available. A kernel-provided Sahara device node (such as * /dev/mhi0_QAIC_SAHARA) is selected with -p and driven with plain * read()/write() through the raw-fd hooks below. When -p is omitted the same * backends the other subcommands use are allocated instead, so an EDL device * on the USB bus can be kickstarted directly. */ static int ks_read(struct qdl_device *qdl, void *buf, size_t len, unsigned int timeout __unused) { return read(qdl->fd, buf, len); } static int ks_write(struct qdl_device *qdl, const void *buf, size_t len, unsigned int timeout __unused) { return write(qdl->fd, buf, len); } /** * ks_add_mapping() - record a ":" image mapping * @arg: option argument to parse * @mappings: image array to populate, indexed by Sahara image id * * Returns: 0 on success, -1 on failure. */ static int ks_add_mapping(const char *arg, struct sahara_image *mappings) { const char *filename; const char *colon; long file_id; file_id = strtol(arg, NULL, 10); colon = strchr(arg, ':'); if (file_id < 0 || file_id >= MAPPING_SZ || !colon) { ux_err("invalid sahara mapping \"%s\", expected with id in 0..%d\n", arg, MAPPING_SZ - 1); return -1; } filename = colon + 1; if (load_sahara_image(NULL, filename, &mappings[file_id]) < 0) return -1; ux_debug("mapped sahara image id %ld to %s\n", file_id, filename); return 0; } static int qdl_ks(int argc, char **argv) { struct sahara_image mappings[MAPPING_SZ] = {}; struct qdl_device node_dev = { .fd = -1, .read = ks_read, .write = ks_write, }; enum QDL_DEVICE_TYPE qdl_dev_type = QDL_DEVICE_AUTO; struct qdl_device *qdl = NULL; bool found_mapping = false; const char *dev_node = NULL; char *serial = NULL; int ret = 0; int opt; static struct option options[] = { {"debug", no_argument, 0, 'd'}, {"version", no_argument, 0, 'v'}, {"port", required_argument, 0, 'p'}, {"sahara", required_argument, 0, 's'}, {"serial", required_argument, 0, OPT_SERIAL}, {"backend", required_argument, 0, OPT_BACKEND}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; while ((opt = getopt_long(argc, argv, "dvp:s:h", options, NULL)) != -1) { switch (opt) { case 'p': dev_node = optarg; break; case 's': if (ks_add_mapping(optarg, mappings) < 0) { ret = 1; goto out; } found_mapping = true; break; default: ret = qdl_common_opt(opt, &serial, &qdl_dev_type); if (ret == QDL_OPT_EXIT_OK) { ret = 0; goto out; } if (ret == QDL_OPT_EXIT_FAIL) { ret = 1; goto out; } ret = 0; } } /* At least one image mapping (-s) is required */ if (!found_mapping) { print_usage(stderr); ret = 1; goto out; } /* * -p names the transport explicitly, so device selection options that * only apply to the enumerated backends would be silently ignored. */ if (dev_node && (serial || qdl_dev_type != QDL_DEVICE_AUTO)) { ux_err("--port cannot be combined with --serial or --backend\n"); ret = 1; goto out; } ux_init(); if (dev_node) { if (qdl_debug) print_version(); node_dev.fd = qdl_open_device_node(dev_node); if (node_dev.fd < 0) { ux_err("unable to open %s: %s\n", dev_node, strerror(errno)); ret = 1; goto out; } qdl = &node_dev; } else { qdl = qdl_session_open(qdl_dev_type, serial); if (!qdl) { ret = 1; goto out; } } if (sahara_run(qdl, mappings, NULL, NULL) < 0) ret = 1; if (dev_node) close(node_dev.fd); else qdl_session_close(qdl); out: sahara_images_free(mappings, MAPPING_SZ); return ret; } static int qdl_ensure_configured(struct list_head *ops, enum qdl_storage_type storage_type) { struct firehose_op *op; if (list_empty(ops)) return 0; op = list_entry_first(ops, struct firehose_op, node); if (op->type == FIREHOSE_OP_CONFIGURE) return 0; op = firehose_alloc_op(FIREHOSE_OP_CONFIGURE); if (!op) return -1; op->storage_type = storage_type; list_prepend(ops, &op->node); return 0; } static int qdl_cmd_flash(struct list_head *firehose_ops, const char *arg, const char *incdir, struct sahara_image *images) { struct qdl_file flashmap; struct qdl_zip *zip = NULL; const char *dot; char *specifier; char *filename; char *tmp; char *base; int file_type = QDL_FILE_UNKNOWN; int ret; filename = qdl_split_specifier(arg, &specifier); if (!filename) { ux_err("failed to parse flash argument \"%s\" (expected or ::)\n", arg); return -1; } tmp = strdup(filename); if (!tmp) return -1; base = basename(tmp); dot = strrchr(base, '.'); if (dot && !strcmp(dot, ".xml")) { file_type = QDL_FILE_CONTENTS; } else if (dot && !strcmp(dot, ".json")) { file_type = QDL_CMD_FLASH; } else { ret = qdl_zip_open(filename, &zip); if (!ret) { ret = qdl_file_open(zip, "flashmap.json", &flashmap); if (!ret) { qdl_file_close(&flashmap); file_type = QDL_CMD_FLASH; } qdl_zip_put(zip); } } free(tmp); switch (file_type) { case QDL_FILE_CONTENTS: ret = contents_load(firehose_ops, filename, specifier, images, incdir); break; case QDL_CMD_FLASH: ret = flashmap_load(firehose_ops, filename, specifier, images, incdir); break; default: if (qdl_zip_supported()) ux_err("flash input must be contents.xml, flashmap.json, or a zip containing flashmap.json\n"); else ux_err("flash input must be contents.xml or flashmap.json (built without zip support)\n"); ret = -1; break; } free(filename); return ret; } static int qdl_cmd_reset(struct list_head *ops) { struct firehose_op *reset_op = firehose_alloc_op(FIREHOSE_OP_RESET); if (!reset_op) return -1; list_append(ops, &reset_op->node); return 0; } static int qdl_create_zip(int argc, char **argv) { struct sahara_image images[MAPPING_SZ] = {}; struct list_head ops = LIST_INIT(ops); const char *zipfile = argv[1]; char *specifier; char *filename; int ret; if (argc != 3) { print_usage(stderr); return 1; } ux_init(); if (!qdl_zip_supported()) { ux_err("create-zip requires zip-container support, which this qdl was built without\n"); return 1; } filename = qdl_split_specifier(argv[2], &specifier); if (!filename) { ux_err("failed to parse flash argument"); return 1; } ret = contents_load(&ops, filename, specifier, images, NULL); if (ret < 0) goto out_free_filename; ret = zipper_write(zipfile, &ops, images); sahara_images_free(images, MAPPING_SZ); firehose_free_ops(&ops); out_free_filename: free(filename); return ret ? 1 : 0; } static int qdl_sahara_archive(int argc, char **argv) { struct sahara_image images[MAPPING_SZ] = {}; const char *archive; char *specifier; char *filename; int ret = 0; if (argc != 3) { print_usage(stderr); return 1; } ux_init(); archive = argv[1]; filename = qdl_split_specifier(argv[2], &specifier); if (!filename) { ux_err("failed to parse Sahara archive input \"%s\"\n", argv[2]); ret = -1; goto out_free_images; } if (qdl_is_contents_xml(filename)) { ret = contents_load_programmers(filename, specifier, images); } else { if (specifier) { ux_err("selectors can only be used with contents.xml inputs\n"); ret = -1; } else { ret = decode_programmer(filename, images); } } free(filename); if (ret < 0) goto out_free_images; ret = 0; ret = sahara_archive_write(archive, images); out_free_images: sahara_images_free(images, MAPPING_SZ); return ret ? 1 : 0; } static int qdl_determine_bootable(struct list_head *ops) { struct firehose_op *op; bool multiple; int bootable; bootable = program_find_bootable_partition(ops, &multiple); if (bootable < 0) { ux_debug("no boot partition found\n"); return 0; } if (multiple) ux_info("Multiple candidates for primary bootloader found, using partition %d\n", bootable); op = firehose_alloc_op(FIREHOSE_OP_SET_BOOTABLE); if (!op) return -1; op->partition = bootable; list_append(ops, &op->node); return 0; } /* * Walk the firehose op list and emit one hex line per * FIREHOSE_OP_GET_SHA256_DIGEST entry. firehose_run() fills op->digest; * formatting and printing live here so firehose.c stays out of the * user-facing output policy. * * If the request shipped but the device returned no digest * (digest_valid stayed false), surface that to the user instead of * silently skipping the region. */ static void print_sha256_results(struct list_head *ops) { struct firehose_op *op; list_for_each_entry(op, ops, node) { char hex[SHA256_DIGEST_STRING_LENGTH]; size_t i; if (op->type != FIREHOSE_OP_GET_SHA256_DIGEST) continue; if (!op->digest_valid) { ux_err("no sha256 digest returned for %s+0x%x\n", op->start_sector, op->num_sectors); continue; } for (i = 0; i < SHA256_DIGEST_LENGTH; i++) snprintf(hex + i * 2, 3, "%02x", op->digest[i]); hex[SHA256_DIGEST_STRING_LENGTH - 1] = '\0'; printf("%s\n", hex); fflush(stdout); } } /* * Walk the positional arguments starting at @start, selecting the programmer * and building the firehose op list (and the UFS provisioning description). * Argument value errors ux_diex() out as before; returns 0 on success or -1 when * a load helper fails and the caller should clean up. */ static int qdl_build_op_list(int argc, char **argv, int start, struct qdl_opts *opts, struct list_head *ops, struct sahara_image *images, struct ufs_provisioning *ufs) { bool saw_file = false; bool saw_verb = false; int idx = start; int type; int ret; /* * The programmer needs to either be selected explicitly or through the * "flash" subcommand. Handling of "flash" happens in the loop below. */ if (strcmp(argv[idx], "flash")) { ret = decode_programmer(argv[idx++], images); if (ret < 0) return -1; } do { type = detect_type(argv[idx]); if (type < 0 || type == QDL_FILE_UNKNOWN) ux_diex(1, "failed to detect file type of %s\n", argv[idx]); /* * The usage synopsis lists input XML files and command verbs * (read/write/erase/sha256/flash/reset) as separate forms; they * must not be mixed. Combining them once let a verb like "reset" * be appended out of order relative to ops added after parsing. * QDL_CMD_* follow the QDL_FILE_* values in the enum. */ if (type >= QDL_CMD_READ) saw_verb = true; else saw_file = true; if (saw_file && saw_verb) ux_diex(1, "input XML files cannot be combined with command " "verbs (read/write/erase/sha256/flash/reset)"); switch (type) { case QDL_FILE_PATCH: ret = patch_load(ops, argv[idx]); if (ret < 0) ux_diex(1, "patch_load %s failed", argv[idx]); break; case QDL_FILE_PROGRAM: ret = program_load(ops, argv[idx], opts->storage_type == QDL_STORAGE_NAND, opts->allow_missing, NULL, opts->incdir); if (ret < 0) ux_diex(1, "program_load %s failed", argv[idx]); if (!opts->allow_fusing && program_is_sec_partition_flashed(ops)) ux_diex(1, "secdata partition to be programmed, which can lead to irreversible" " changes. Allow explicitly with --allow-fusing parameter"); break; case QDL_FILE_READ: ret = read_op_load(ops, argv[idx], opts->incdir); if (ret < 0) ux_diex(1, "read_op_load %s failed", argv[idx]); break; case QDL_FILE_UFS: if (opts->storage_type != QDL_STORAGE_UFS) ux_diex(1, "attempting to load provisioning config when storage isn't \"ufs\""); ret = ufs_load(ufs, argv[idx], opts->finalize_provisioning); if (ret < 0) ux_diex(1, "ufs_load %s failed", argv[idx]); break; case QDL_CMD_READ: if (idx + 2 >= argc) ux_diex(1, "read command missing arguments"); ret = read_cmd_add(ops, argv[idx + 1], argv[idx + 2]); if (ret < 0) ux_diex(1, "failed to add read command"); idx += 2; break; case QDL_CMD_WRITE: if (idx + 2 >= argc) ux_diex(1, "write command missing arguments"); ret = program_cmd_add(ops, argv[idx + 1], argv[idx + 2]); if (ret < 0) ux_diex(1, "failed to add write command"); idx += 2; break; case QDL_CMD_ERASE: if (idx + 1 >= argc) ux_diex(1, "erase command missing address"); ret = erase_cmd_add(ops, argv[idx + 1]); if (ret < 0) ux_diex(1, "failed to add erase command"); idx += 1; break; case QDL_CMD_SHA256: if (idx + 1 >= argc) ux_diex(1, "sha256 command missing address"); ret = sha256_cmd_add(ops, argv[idx + 1]); if (ret < 0) ux_diex(1, "failed to add sha256 command"); idx += 1; break; case QDL_CMD_FLASH: if (idx + 1 >= argc) ux_diex(1, "flash command missing operands"); ret = qdl_cmd_flash(ops, argv[idx + 1], opts->incdir, images); if (ret < 0) return -1; idx += 1; break; case QDL_CMD_RESET: /* Do not allocate two reset commands */ opts->skip_reset = true; ret = qdl_cmd_reset(ops); if (ret < 0) return -1; /* Reset ends the run; stop processing chained commands */ return 0; default: ux_diex(1, "%s type not yet supported", argv[idx]); break; } } while (++idx < argc); return 0; } static int qdl_flash(int argc, char **argv) { struct sahara_image sahara_images[MAPPING_SZ] = {}; struct list_head firehose_ops = LIST_INIT(firehose_ops); struct ufs_provisioning ufs; struct qdl_device *qdl = NULL; struct qdl_opts opts; int ret; ufs_provisioning_init(&ufs); ret = qdl_parse_args(argc, argv, &opts); if (ret == QDL_ARGS_DONE) return 0; if (ret == QDL_ARGS_ERROR) return 1; qdl = qdl_init(opts.dev_type); if (!qdl) { ret = -1; goto out_cleanup; } qdl->slot = opts.slot; qdl->skipblock_mode = opts.skipblock_mode; if (opts.vip_table_path) { if (opts.vip_generate_dir) ux_diex(1, "VIP mode and VIP table generation can't be enabled together\n"); ret = vip_transfer_init(qdl, opts.vip_table_path); if (ret) ux_diex(1, "VIP initialization failed\n"); } if (opts.out_chunk_size) qdl_set_out_chunk_size(qdl, opts.out_chunk_size); if (opts.vip_generate_dir) { ret = vip_gen_init(qdl, opts.vip_generate_dir); if (ret) goto out_cleanup; } ux_init(); if (qdl_debug) print_version(); ret = qdl_build_op_list(argc, argv, optind, &opts, &firehose_ops, sahara_images, &ufs); if (ret < 0) goto out_cleanup; ret = qdl_ensure_configured(&firehose_ops, opts.storage_type); if (ret < 0) goto out_cleanup; ret = qdl_determine_bootable(&firehose_ops); if (ret) goto out_cleanup; /* * Reset is the last operation in any flashing run, modelled as a regular * firehose op so callers can compose it like any other. Skip the append * to leave the programmer alive across qdl invocations. */ if (!opts.skip_reset) { ret = qdl_cmd_reset(&firehose_ops); if (ret < 0) goto out_cleanup; } ret = qdl_open(qdl, opts.serial); if (ret) goto out_cleanup; ret = sahara_run(qdl, sahara_images, NULL, NULL); if (ret < 0) goto out_cleanup; if (ufs_need_provisioning(&ufs)) ret = firehose_provision(qdl, &ufs, opts.skip_reset); else ret = firehose_run(qdl, &firehose_ops); if (ret < 0) goto out_cleanup; print_sha256_results(&firehose_ops); out_cleanup: if (qdl) { if (opts.vip_generate_dir) vip_gen_finalize(qdl); qdl_close(qdl); } sahara_images_free(sahara_images, MAPPING_SZ); firehose_free_ops(&firehose_ops); ufs_provisioning_cleanup(&ufs); if (qdl) { if (qdl->vip_data.state != VIP_DISABLED) vip_transfer_deinit(qdl); qdl_deinit(qdl); } return !!ret; } int main(int argc, char **argv) { int i; /* execve() allows an empty argv, where argv[0] is already NULL */ if (argc < 1) return 1; ux_set_progname(argv[0]); if (qdl_take_color_option(&argc, argv)) return 1; for (i = 1; i < argc; i++) { if (!strcmp(argv[i], "list")) return qdl_list(stdout); if (!strcmp(argv[i], "ramdump")) return qdl_ramdump(argc - i, argv + i); if (!strcmp(argv[i], "chipinfo")) return qdl_sahara_cmd(argc - i, argv + i, sahara_chipinfo); if (!strcmp(argv[i], "reset")) return qdl_sahara_cmd(argc - i, argv + i, qdl_reset_run); if (!strcmp(argv[i], "ks")) return qdl_ks(argc - i, argv + i); if (!strcmp(argv[i], "create-zip")) return qdl_create_zip(argc - i, argv + i); if (!strcmp(argv[i], "create-sahara-archive")) return qdl_sahara_archive(argc - i, argv + i); if (argv[i][0] != '-') break; } return qdl_flash(argc, argv); } linux-msm-qdl-6e625aa/src/qud.c000066400000000000000000000374411527206113100163670ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. * * QUD ("Qualcomm USB Driver") backend: talks to a Qualcomm EDL device * through a kernel-mode driver that exposes the device as a character * file, rather than via libusb. Today only the Windows path is implemented: * the official QDLoader 9008 driver presents the device as a serial port * (\\.\COMx). The Sahara/Firehose framing is identical to the libusb * backend; this file only deals with how user space reaches the underlying * bulk pipes. */ #include #include #include #include #include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN /* * CancelIoEx() requires Vista+; MinGW headers gate it on _WIN32_WINNT >= 0x0600. * Windows 7 is the de-facto floor for this project so this is safe. */ #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0600 #endif #include /* * must precede any header that declares the GUIDs we reference, * so that GUID_DEVCLASS_PORTS gets a real definition in this TU instead of an * extern reference (MinGW does not ship a uuid.lib that would resolve it). */ #include #include #include #include #include #endif #include "qdl.h" #include "oscompat.h" #ifdef _WIN32 #define QCOM_HWID_PREFIX "USB\\VID_05C6&PID_" /* * The bus-reported device description is a PnP property (DEVPKEY), not * an SPDRP_* registry property; older MinGW headers don't define the * key, so we declare it locally. GUID + PID per Microsoft docs. */ static const DEVPROPKEY qud_devpkey_bus_reported_device_desc = { { 0x540b947e, 0x8b40, 0x45bc, { 0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2 } }, 4 }; struct qdl_device_qud { struct qdl_device base; void *handle; /* HANDLE; void* avoids leaking to consumers */ char path[PATH_MAX]; }; /* * Pull the iSerial out of the SetupAPI device-instance ID. The instance ID for * a USB device looks like "USB\\VID_05C6&PID_9008\\"; the trailing * component after the last backslash is the iSerial as Windows stored it. */ static void win_extract_serial(const char *instance_id, char *out, size_t out_size) { const char *last; out[0] = '\0'; last = strrchr(instance_id, '\\'); if (!last) return; last++; snprintf(out, out_size, "%s", last); } /* * Read PortName ("COM5", "COM12", ...) from the device's "Device Parameters" * registry subkey. */ static bool win_read_port_name(HDEVINFO devinfo, SP_DEVINFO_DATA *info, char *out, size_t out_size) { DWORD type = 0; DWORD size = 0; HKEY key; LSTATUS s; key = SetupDiOpenDevRegKey(devinfo, info, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_QUERY_VALUE); if (key == INVALID_HANDLE_VALUE) return false; size = (DWORD)out_size; s = RegQueryValueExA(key, "PortName", NULL, &type, (LPBYTE)out, &size); RegCloseKey(key); if (s != ERROR_SUCCESS || type != REG_SZ) return false; if (size > 0 && out[size - 1] != '\0' && size < out_size) out[size] = '\0'; return true; } /* * The EDL firmware leaves iSerial == 0 and stuffs the real serial into * iProduct as "..._SN:_...". Windows surfaces iProduct verbatim * through SPDRP_BUSREPORTEDDEVICEDESC, so we can recover the same serial * the libusb backend extracts in usb_list() without opening the device. * Returns true and writes the serial on success; false if the property * is missing, empty, or does not contain the _SN: token. */ static bool win_read_iproduct_sn(HDEVINFO devinfo, SP_DEVINFO_DATA *info, char *out, size_t out_size) { WCHAR wdesc[256]; char desc[512]; DEVPROPTYPE prop_type = 0; DWORD required = 0; const char *p; size_t len; int n; if (!SetupDiGetDevicePropertyW(devinfo, info, &qud_devpkey_bus_reported_device_desc, &prop_type, (PBYTE)wdesc, sizeof(wdesc), &required, 0)) return false; if (prop_type != DEVPROP_TYPE_STRING) return false; wdesc[ARRAY_SIZE(wdesc) - 1] = L'\0'; n = WideCharToMultiByte(CP_UTF8, 0, wdesc, -1, desc, sizeof(desc), NULL, NULL); if (n <= 0) return false; p = strstr(desc, "_SN:"); if (!p) return false; p += 4; len = strcspn(p, " _"); if (len == 0 || len + 1 > out_size) return false; memcpy(out, p, len); out[len] = '\0'; return true; } /* * Parse the 4-digit hex PID immediately following the QCOM_HWID_PREFIX * in a HARDWAREID string. Returns 0 if no valid PID is found. */ static unsigned int win_parse_pid(const char *hwid) { const char *p = hwid + sizeof(QCOM_HWID_PREFIX) - 1; unsigned int pid = 0; int i; for (i = 0; i < 4; i++) { char c = p[i]; unsigned int digit; if (c >= '0' && c <= '9') digit = (unsigned int)(c - '0'); else if (c >= 'a' && c <= 'f') digit = (unsigned int)(c - 'a') + 10; else if (c >= 'A' && c <= 'F') digit = (unsigned int)(c - 'A') + 10; else return 0; pid = (pid << 4) | digit; } return pid; } /* * Windows encodes each USB interface's class/subclass/protocol triple * in the devnode's compatible ids as "USB\Class_xx&SubClass_xx&Prot_xx". * Match the same EDL interface signature as the libusb backend * (usb_match_edl_interface() in usb.c): vendor-specific class and * subclass with a known Sahara protocol - 0x10, 0x11 or 0x13 on modern * devices, 0xff on older targets. */ static bool win_match_edl_interface(HDEVINFO devinfo, SP_DEVINFO_DATA *info) { static const char * const edl_ids[] = { "USB\\Class_FF&SubClass_FF&Prot_10", "USB\\Class_FF&SubClass_FF&Prot_11", "USB\\Class_FF&SubClass_FF&Prot_13", "USB\\Class_FF&SubClass_FF&Prot_FF", }; char ids[1024]; DWORD type = 0; const char *p; size_t i; if (!SetupDiGetDeviceRegistryPropertyA(devinfo, info, SPDRP_COMPATIBLEIDS, &type, (BYTE *)ids, sizeof(ids), NULL)) return false; if (type != REG_MULTI_SZ) return false; /* REG_MULTI_SZ: walk the NUL-separated entries */ for (p = ids; *p; p += strlen(p) + 1) { for (i = 0; i < ARRAY_SIZE(edl_ids); i++) { if (!_stricmp(p, edl_ids[i])) return true; } } return false; } static int win_enumerate_qcom(struct qud_device_desc *out, size_t out_max) { HDEVINFO devinfo; SP_DEVINFO_DATA info = { .cbSize = sizeof(info) }; char hwid[256]; char instance_id[256]; char port_name[32]; size_t count = 0; DWORD i; devinfo = SetupDiGetClassDevsA(&GUID_DEVCLASS_PORTS, NULL, NULL, DIGCF_PRESENT); if (devinfo == INVALID_HANDLE_VALUE) return -1; for (i = 0; SetupDiEnumDeviceInfo(devinfo, i, &info); i++) { unsigned int pid; DWORD type = 0; DWORD size = 0; if (!SetupDiGetDeviceRegistryPropertyA(devinfo, &info, SPDRP_HARDWAREID, &type, (BYTE *)hwid, sizeof(hwid), &size)) continue; /* HARDWAREID is a REG_MULTI_SZ; first entry is what we want. */ if (strncmp(hwid, QCOM_HWID_PREFIX, sizeof(QCOM_HWID_PREFIX) - 1) != 0) continue; /* * The prefix match already guarantees Qualcomm's vid; require * the same EDL interface signature the libusb backend * matches, so both backends filter other Qualcomm COM ports * the same way. */ if (!win_match_edl_interface(devinfo, &info)) continue; pid = win_parse_pid(hwid); if (!SetupDiGetDeviceInstanceIdA(devinfo, &info, instance_id, sizeof(instance_id), NULL)) continue; if (!win_read_port_name(devinfo, &info, port_name, sizeof(port_name))) continue; if (count < out_max) { out[count].pid = pid; if (!win_read_iproduct_sn(devinfo, &info, out[count].serial, sizeof(out[count].serial))) win_extract_serial(instance_id, out[count].serial, sizeof(out[count].serial)); snprintf(out[count].path, sizeof(out[count].path), "\\\\.\\%s", port_name); } count++; } SetupDiDestroyDeviceInfoList(devinfo); return (int)count; } static int win_configure_handle(HANDLE h) { COMMTIMEOUTS to = { 0 }; DCB dcb = { .DCBlength = sizeof(dcb) }; /* * The QDLoader 9008 driver presents the device as a serial port, but * line-discipline parameters (baud, parity, stop bits) are irrelevant * over USB-CDC. Some driver builds nevertheless reject I/O until DCB * has been initialised, so we set sane defaults and never touch them * again. */ if (!GetCommState(h, &dcb)) return -1; dcb.BaudRate = 115200; dcb.ByteSize = 8; dcb.Parity = NOPARITY; dcb.StopBits = ONESTOPBIT; dcb.fBinary = TRUE; dcb.fNull = FALSE; dcb.fAbortOnError = FALSE; /* * Force-disable hardware/software flow control: some QDLoader driver * builds gate bulk-IN delivery on these handshake lines, which would * stall reads even though USB-CDC has no real RS-232 signals. */ dcb.fOutxCtsFlow = FALSE; dcb.fOutxDsrFlow = FALSE; dcb.fDsrSensitivity = FALSE; dcb.fOutX = FALSE; dcb.fInX = FALSE; dcb.fDtrControl = DTR_CONTROL_ENABLE; dcb.fRtsControl = RTS_CONTROL_ENABLE; if (!SetCommState(h, &dcb)) return -1; /* * Mirror libusb bulk-read semantics: ReadFile returns as soon as any * data is available, or after ReadTotalTimeoutConstant ms with zero * bytes if the link is idle (MSDN COMMTIMEOUTS, "MAXDWORD interval + * MAXDWORD multiplier" case). qud_read() rewrites the constant * per call; this just seeds a sane default for writes and any read * issued before that. */ to.ReadIntervalTimeout = MAXDWORD; to.ReadTotalTimeoutMultiplier = MAXDWORD; to.ReadTotalTimeoutConstant = 1000; to.WriteTotalTimeoutMultiplier = 0; to.WriteTotalTimeoutConstant = 0; if (!SetCommTimeouts(h, &to)) return -1; SetupComm(h, 64 * 1024, 64 * 1024); /* * Only purge the TX side. The QDLoader driver claims the USB * interface during PnP enumeration, well before CreateFile() runs, * so the device's Sahara HELLO is typically already sitting in the * driver's RX FIFO by the time we get here. Purging RX would * discard it, and the device only sends HELLO once. */ PurgeComm(h, PURGE_TXCLEAR | PURGE_TXABORT); return 0; } static int qud_open(struct qdl_device *qdl, const char *serial) { struct qdl_device_qud *qd = container_of(qdl, struct qdl_device_qud, base); struct qud_device_desc matches[16]; const char *path = NULL; HANDLE h; int found; int i; /* Allow an explicit \\.\COMx (or COMx) path via --serial. */ if (serial && (serial[0] == '\\' || (strlen(serial) >= 3 && (serial[0] == 'C' || serial[0] == 'c') && (serial[1] == 'O' || serial[1] == 'o') && (serial[2] == 'M' || serial[2] == 'm')))) { path = serial; } else { found = win_enumerate_qcom(matches, ARRAY_SIZE(matches)); if (found < 0) { ux_err_tag(UX_TAG_QUD, "SetupDiGetClassDevs failed (error %lu)\n", (unsigned long)GetLastError()); return -1; } if (found == 0) { ux_err_tag(UX_TAG_QUD, "no Qualcomm COM ports found\n"); return -1; } for (i = 0; i < found && i < (int)ARRAY_SIZE(matches); i++) { if (!serial || serial[0] == '\0' || _stricmp(matches[i].serial, serial) == 0) { path = matches[i].path; break; } } if (!path) { ux_err_tag(UX_TAG_QUD, "no Qualcomm device matching serial \"%s\"\n", serial); return -1; } } h = CreateFileA(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); if (h == INVALID_HANDLE_VALUE) { ux_err_tag(UX_TAG_QUD, "CreateFile(%s) failed (error %lu)\n", path, (unsigned long)GetLastError()); return -1; } if (win_configure_handle(h) < 0) { ux_err_tag(UX_TAG_QUD, "failed to configure %s (error %lu)\n", path, (unsigned long)GetLastError()); CloseHandle(h); return -1; } qd->handle = h; snprintf(qd->path, sizeof(qd->path), "%s", path); ux_debug_tag(UX_TAG_QUD, "opened %s\n", path); return 0; } static int win_overlapped_io(HANDLE h, void *buf, size_t len, unsigned int timeout_ms, bool is_write) { OVERLAPPED ovl = { 0 }; HANDLE ev; DWORD wait; DWORD done = 0; BOOL ok; ev = CreateEvent(NULL, TRUE, FALSE, NULL); if (!ev) return -EIO; ovl.hEvent = ev; if (is_write) ok = WriteFile(h, buf, (DWORD)len, NULL, &ovl); else ok = ReadFile(h, buf, (DWORD)len, NULL, &ovl); if (!ok && GetLastError() != ERROR_IO_PENDING) { CloseHandle(ev); return -EIO; } wait = WaitForSingleObject(ev, timeout_ms); if (wait == WAIT_TIMEOUT) { CancelIoEx(h, &ovl); /* Drain the cancellation so the OVERLAPPED isn't reused live. */ GetOverlappedResult(h, &ovl, &done, TRUE); CloseHandle(ev); return -ETIMEDOUT; } if (wait != WAIT_OBJECT_0) { CloseHandle(ev); return -EIO; } if (!GetOverlappedResult(h, &ovl, &done, FALSE)) { CloseHandle(ev); return -EIO; } CloseHandle(ev); return (int)done; } static int qud_read(struct qdl_device *qdl, void *buf, size_t len, unsigned int timeout) { struct qdl_device_qud *qd = container_of(qdl, struct qdl_device_qud, base); HANDLE h = (HANDLE)qd->handle; COMMTIMEOUTS to = { .ReadIntervalTimeout = MAXDWORD, .ReadTotalTimeoutMultiplier = MAXDWORD, .ReadTotalTimeoutConstant = timeout ? timeout : 1, .WriteTotalTimeoutMultiplier = 0, .WriteTotalTimeoutConstant = 0, }; int ret; /* * The COM driver itself enforces the per-call timeout via * COMMTIMEOUTS; the WaitForSingleObject() inside win_overlapped_io() * is a backstop with a small grace window in case the driver * completes a hair after the constant. */ if (!SetCommTimeouts(h, &to)) return -EIO; ret = win_overlapped_io(h, buf, len, timeout + 250, false); if (ret == 0) return -ETIMEDOUT; return ret; } static int qud_write(struct qdl_device *qdl, const void *buf, size_t len, unsigned int timeout) { struct qdl_device_qud *qd = container_of(qdl, struct qdl_device_qud, base); return win_overlapped_io((HANDLE)qd->handle, (void *)buf, len, timeout, true); } static void qud_close(struct qdl_device *qdl) { struct qdl_device_qud *qd = container_of(qdl, struct qdl_device_qud, base); HANDLE h = (HANDLE)qd->handle; if (h && h != INVALID_HANDLE_VALUE) { CloseHandle(h); qd->handle = NULL; } } static void qud_set_out_chunk_size(struct qdl_device *qdl __unused, long size __unused) { /* The kernel-mode driver handles bulk-transfer chunking. */ } struct qdl_device *qud_init(void) { struct qdl_device_qud *qd = calloc(1, sizeof(*qd)); if (!qd) return NULL; qd->handle = NULL; qd->base.dev_type = QDL_DEVICE_QUD; qd->base.open = qud_open; qd->base.read = qud_read; qd->base.write = qud_write; qd->base.close = qud_close; qd->base.set_out_chunk_size = qud_set_out_chunk_size; qd->base.max_payload_size = 1048576; return &qd->base; } /* * qud_probe_present() - count Qualcomm COM ports exposed by the QDLoader * 9008 driver. Used by the auto-backend selector on Windows to decide * whether the QUD backend can take over when libusb cannot open the * device. */ int qud_probe_present(void) { struct qud_device_desc matches[16]; int found; found = win_enumerate_qcom(matches, ARRAY_SIZE(matches)); return found > 0 ? found : 0; } /* * Enumerate Qualcomm COM ports the QUD backend can drive, returning a * heap-allocated array of qud_device_desc and writing the count to * *devices_found. Caller frees with free(). Returns NULL on error or * when no devices are present (count is still written). */ struct qud_device_desc *qud_list(unsigned int *devices_found) { struct qud_device_desc *result; int found; *devices_found = 0; result = calloc(16, sizeof(*result)); if (!result) return NULL; found = win_enumerate_qcom(result, 16); if (found <= 0) { free(result); return NULL; } if (found > 16) { ux_err_tag(UX_TAG_QUD, "%d devices present, only the first 16 will be listed\n", found); found = 16; } *devices_found = (unsigned int)found; return result; } #else /* unsupported platform */ struct qdl_device *qud_init(void) { ux_err("qud backend is not supported on this platform\n"); return NULL; } int qud_probe_present(void) { return 0; } struct qud_device_desc *qud_list(unsigned int *devices_found) { *devices_found = 0; return NULL; } #endif linux-msm-qdl-6e625aa/src/read.c000066400000000000000000000072051527206113100165040ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2016-2017, Linaro Ltd. * All rights reserved. */ #include #include #include #include #include #include #include #include "list.h" #include "read.h" #include "qdl.h" #include "oscompat.h" #include "firehose.h" #include "gpt.h" int read_op_load(struct list_head *ops, const char *read_op_file, const char *incdir) { struct firehose_op *read_op; xmlNode *node; xmlNode *root; xmlDoc *doc; int errors; char tmp[PATH_MAX]; doc = xmlReadFile(read_op_file, NULL, 0); if (!doc) { ux_err("failed to parse read-type file \"%s\"\n", read_op_file); return -EINVAL; } root = xmlDocGetRootElement(doc); for (node = root->children; node ; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (xmlStrcmp(node->name, (xmlChar *)"read")) { ux_err("unrecognized tag \"%s\" in read-type file \"%s\", ignoring\n", node->name, read_op_file); continue; } errors = 0; read_op = firehose_alloc_op(FIREHOSE_OP_READ); if (!read_op) { xmlFreeDoc(doc); return -ENOMEM; } read_op->sector_size = attr_as_unsigned(node, "SECTOR_SIZE_IN_BYTES", &errors); read_op->filename = attr_as_string(node, "filename", &errors); read_op->partition = attr_as_unsigned(node, "physical_partition_number", &errors); read_op->num_sectors = attr_as_unsigned(node, "num_partition_sectors", &errors); read_op->start_sector = attr_as_string(node, "start_sector", &errors); if (errors) { ux_err("errors while parsing read-type file \"%s\"\n", read_op_file); free((void *)read_op->filename); free((void *)read_op->start_sector); free(read_op); xmlFreeDoc(doc); return -EINVAL; } if (incdir && read_op->filename) { snprintf(tmp, PATH_MAX, "%s/%s", incdir, read_op->filename); if (access(tmp, F_OK) != -1) { free((void *)read_op->filename); read_op->filename = strdup(tmp); } } list_append(ops, &read_op->node); } xmlFreeDoc(doc); return 0; } int read_cmd_add(struct list_head *ops, const char *address, const char *filename) { struct firehose_op *read_op; unsigned int start_sector; unsigned int num_sectors; char *gpt_partition; int partition; char buf[20]; int ret; ret = parse_storage_address(address, &partition, &start_sector, &num_sectors, &gpt_partition); if (ret < 0) return ret; if (num_sectors == 0 && !gpt_partition) { ux_err("read command without length specifier not supported\n"); return -1; } read_op = firehose_alloc_op(FIREHOSE_OP_READ); if (!read_op) return -1; read_op->sector_size = 0; read_op->filename = strdup(filename); read_op->partition = partition; read_op->num_sectors = num_sectors; sprintf(buf, "%u", start_sector); read_op->start_sector = strdup(buf); read_op->gpt_partition = gpt_partition; list_append(ops, &read_op->node); return 0; } int sha256_cmd_add(struct list_head *ops, const char *address) { struct firehose_op *op; unsigned int start_sector; unsigned int num_sectors; char *gpt_partition; int partition; char buf[20]; int ret; ret = parse_storage_address(address, &partition, &start_sector, &num_sectors, &gpt_partition); if (ret < 0) return ret; if (num_sectors == 0 && !gpt_partition) { ux_err("sha256 command without length specifier not supported\n"); return -1; } op = firehose_alloc_op(FIREHOSE_OP_GET_SHA256_DIGEST); if (!op) return -1; op->sector_size = 0; op->partition = partition; op->num_sectors = num_sectors; sprintf(buf, "%u", start_sector); op->start_sector = strdup(buf); op->gpt_partition = gpt_partition; list_append(ops, &op->node); return 0; } linux-msm-qdl-6e625aa/src/read.h000066400000000000000000000007441527206113100165120ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #ifndef __READ_H__ #define __READ_H__ #include #include "list.h" struct qdl_device; struct firehose_op; int read_op_load(struct list_head *ops, const char *read_op_file, const char *incdir); int read_cmd_add(struct list_head *ops, const char *address, const char *filename); int sha256_cmd_add(struct list_head *ops, const char *address); #endif linux-msm-qdl-6e625aa/src/sahara.c000066400000000000000000001242771527206113100170410ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2016-2017, Linaro Ltd. * All rights reserved. */ #include #include #include #include #include #include #include #include #include #include #include #include #include "qdl.h" #include "oscompat.h" /* Minimal ELF64 definitions — is not available on all platforms */ #define ELFMAG "\177ELF" #define SELFMAG 4 #define ET_CORE 4 #define PT_LOAD 1 #define EI_NIDENT 16 typedef uint16_t Elf64_Half; typedef uint32_t Elf64_Word; typedef uint64_t Elf64_Addr; typedef uint64_t Elf64_Off; typedef uint64_t Elf64_Xword; typedef struct { unsigned char e_ident[EI_NIDENT]; Elf64_Half e_type; Elf64_Half e_machine; Elf64_Word e_version; Elf64_Addr e_entry; Elf64_Off e_phoff; Elf64_Off e_shoff; Elf64_Word e_flags; Elf64_Half e_ehsize; Elf64_Half e_phentsize; Elf64_Half e_phnum; Elf64_Half e_shentsize; Elf64_Half e_shnum; Elf64_Half e_shstrndx; } Elf64_Ehdr; typedef struct { Elf64_Word p_type; Elf64_Word p_flags; Elf64_Off p_offset; Elf64_Addr p_vaddr; Elf64_Addr p_paddr; Elf64_Xword p_filesz; Elf64_Xword p_memsz; Elf64_Xword p_align; } Elf64_Phdr; #define SAHARA_HELLO_CMD 0x1 /* Min protocol version 1.0 */ #define SAHARA_HELLO_RESP_CMD 0x2 /* Min protocol version 1.0 */ #define SAHARA_READ_DATA_CMD 0x3 /* Min protocol version 1.0 */ #define SAHARA_END_OF_IMAGE_CMD 0x4 /* Min protocol version 1.0 */ #define SAHARA_DONE_CMD 0x5 /* Min protocol version 1.0 */ #define SAHARA_DONE_RESP_CMD 0x6 /* Min protocol version 1.0 */ #define SAHARA_RESET_CMD 0x7 /* Min protocol version 1.0 */ #define SAHARA_RESET_RESP_CMD 0x8 /* Min protocol version 1.0 */ #define SAHARA_MEM_DEBUG_CMD 0x9 /* Min protocol version 2.0 */ #define SAHARA_MEM_READ_CMD 0xa /* Min protocol version 2.0 */ #define SAHARA_CMD_READY_CMD 0xb /* Min protocol version 2.1 */ #define SAHARA_SWITCH_MODE_CMD 0xc /* Min protocol version 2.1 */ #define SAHARA_EXECUTE_CMD 0xd /* Min protocol version 2.1 */ #define SAHARA_EXECUTE_RESP_CMD 0xe /* Min protocol version 2.1 */ #define SAHARA_EXECUTE_DATA_CMD 0xf /* Min protocol version 2.1 */ #define SAHARA_MEM_DEBUG64_CMD 0x10 /* Min protocol version 2.5 */ #define SAHARA_MEM_READ64_CMD 0x11 /* Min protocol version 2.5 */ #define SAHARA_READ_DATA64_CMD 0x12 /* Min protocol version 2.8 */ #define SAHARA_RESET_STATE_CMD 0x13 /* Min protocol version 2.9 */ #define SAHARA_WRITE_DATA_CMD 0x14 /* Min protocol version 3.0 */ #define SAHARA_VERSION 2 #define SAHARA_SUCCESS 0 #define SAHARA_MODE_IMAGE_TX_PENDING 0x0 #define SAHARA_MODE_IMAGE_TX_COMPLETE 0x1 #define SAHARA_MODE_MEMORY_DEBUG 0x2 #define SAHARA_MODE_COMMAND 0x3 #define SAHARA_HELLO_LENGTH 0x30 #define SAHARA_READ_DATA_LENGTH 0x14 #define SAHARA_READ_DATA64_LENGTH 0x20 #define SAHARA_END_OF_IMAGE_LENGTH 0x10 #define SAHARA_MEM_READ64_LENGTH 0x18 #define SAHARA_MEM_DEBUG64_LENGTH 0x18 #define SAHARA_DONE_LENGTH 0x8 #define SAHARA_DONE_RESP_LENGTH 0xc #define SAHARA_RESET_LENGTH 0x8 #define SAHARA_EXECUTE_LENGTH 0xc #define SAHARA_SWITCH_MODE_LENGTH 0xc /* Sahara command-mode client commands, carried by SAHARA_EXECUTE_CMD */ #define SAHARA_EXEC_CMD_SERIAL_NUM_READ 0x01 #define SAHARA_EXEC_CMD_MSM_HW_ID_READ 0x02 #define SAHARA_EXEC_CMD_OEM_PK_HASH_READ 0x03 #define SAHARA_EXEC_CMD_READ_CHIP_ID_V3 0x0a #define DEBUG_BLOCK_SIZE (512u * 1024u) #define SAHARA_CMD_TIMEOUT_MS 1000 #define SAHARA_RESET_RETRIES 5 #define SAHARA_RESET_VERIFY_TIMEOUT_MS 3000 struct sahara_pkt { uint32_t cmd; uint32_t length; union { struct { uint32_t version; uint32_t compatible; uint32_t max_len; uint32_t mode; uint32_t reserved[6]; } hello_req; struct { uint32_t version; uint32_t compatible; uint32_t status; uint32_t mode; uint32_t reserved[6]; } hello_resp; struct { uint32_t image; uint32_t offset; uint32_t length; } read_req; struct { uint32_t image; uint32_t status; } eoi; struct { } done_req; struct { uint32_t status; } done_resp; struct { uint64_t addr; uint64_t length; } debug64_req; struct { uint64_t image; uint64_t offset; uint64_t length; } read64_req; struct { uint32_t client_cmd; } exec_req; struct { uint32_t client_cmd; uint32_t data_length; } exec_resp; struct { uint32_t mode; } switch_mode; }; }; struct sahara_debug_region64 { uint64_t type; uint64_t addr; uint64_t length; char region[20]; char filename[20]; }; static const char *sahara_status_str(uint32_t status) { switch (status) { case 0x00: return "success"; case 0x01: return "invalid command received in current state"; case 0x02: return "protocol mismatch between host and target"; case 0x03: return "invalid target protocol version"; case 0x04: return "invalid host protocol version"; case 0x05: return "invalid packet size received"; case 0x06: return "unexpected image ID received"; case 0x07: return "invalid image header size received"; case 0x08: return "invalid image data size received"; case 0x09: return "invalid image type received"; case 0x0a: return "invalid transmission length"; case 0x0b: return "invalid reception length"; case 0x0c: return "general transmission or reception error"; case 0x0d: return "error while transmitting READ_DATA packet"; case 0x0e: return "cannot receive specified number of program headers"; case 0x0f: return "invalid data length received for program headers"; case 0x10: return "multiple shared segments found in ELF image"; case 0x11: return "uninitialized program header location"; case 0x12: return "invalid destination address"; case 0x13: return "invalid data size received in image header"; case 0x14: return "invalid ELF header received"; case 0x15: return "unknown host error received in HELLO_RESP"; case 0x16: return "timeout while receiving data"; case 0x17: return "timeout while transmitting data"; case 0x18: return "invalid mode received from host"; case 0x19: return "invalid memory read access"; case 0x1a: return "host cannot handle read data size requested"; case 0x1b: return "memory debug not supported"; case 0x1c: return "invalid mode switch"; case 0x1d: return "failed to execute command"; case 0x1e: return "invalid parameter passed to command execution"; case 0x1f: return "unsupported client command received"; case 0x20: return "invalid client command received for data response"; case 0x21: return "failed to authenticate hash table"; case 0x22: return "failed to verify hash for a given segment of ELF image"; case 0x23: return "failed to find hash table in ELF image"; case 0x24: return "target failed to initialize"; case 0x25: return "failed to find valid ELF image entry address"; case 0x26: return "invalid ELF hash table size"; case 0x27: return "Sahara reset command received from host"; case 0x28: return "failed to find XBL_SEC segment in the ELF image"; case 0x29: return "invalid ELF image format received"; case 0x2a: return "invalid XBL-SEC ELF info received"; case 0x2b: return "failure seen in auth initialization"; case 0x2c: return "no loadable segments found"; case 0x2d: return "invalid XBL info received"; case 0x2e: return "FDL timeout reset occurred on target"; case 0x2f: return "USB reconnect detected by target"; case 0x30: return "target error does not map to any known Sahara error"; default: return "invalid status code"; } } static void sahara_send_reset(struct qdl_device *qdl) { struct sahara_pkt resp; resp.cmd = SAHARA_RESET_CMD; resp.length = SAHARA_RESET_LENGTH; qdl_write(qdl, &resp, resp.length, SAHARA_CMD_TIMEOUT_MS); } static int sahara_send_hello_resp(struct qdl_device *qdl, unsigned int version, unsigned int mode) { struct sahara_pkt resp = {}; resp.cmd = SAHARA_HELLO_RESP_CMD; resp.length = SAHARA_HELLO_LENGTH; resp.hello_resp.version = version; resp.hello_resp.compatible = 1; resp.hello_resp.status = SAHARA_SUCCESS; resp.hello_resp.mode = mode; ux_debug_tag(UX_TAG_SAHARA, "%s HELLO_RESP version: 0x%x compatible: 0x%x mode: %u\n", ux_direction(true), resp.hello_resp.version, resp.hello_resp.compatible, resp.hello_resp.mode); qdl_write(qdl, &resp, resp.length, SAHARA_CMD_TIMEOUT_MS); return 0; } static int sahara_hello(struct qdl_device *qdl, struct sahara_pkt *pkt) { if (pkt->length != SAHARA_HELLO_LENGTH) { ux_err_tag(UX_TAG_SAHARA, "unexpected HELLO packet length %u\n", pkt->length); sahara_send_reset(qdl); return -1; } ux_debug_tag(UX_TAG_SAHARA, "%s HELLO version: 0x%x compatible: 0x%x max_len: %d mode: %d\n", ux_direction(false), pkt->hello_req.version, pkt->hello_req.compatible, pkt->hello_req.max_len, pkt->hello_req.mode); return sahara_send_hello_resp(qdl, SAHARA_VERSION, pkt->hello_req.mode); } static int sahara_read(struct qdl_device *qdl, struct sahara_pkt *pkt, const struct sahara_image *images) { const struct sahara_image *image; unsigned int image_idx; size_t offset; size_t len; int ret; if (pkt->length != SAHARA_READ_DATA_LENGTH) { ux_err_tag(UX_TAG_SAHARA, "unexpected READ_DATA packet length %u\n", pkt->length); sahara_send_reset(qdl); return -1; } ux_debug_tag(UX_TAG_SAHARA, "%s READ_DATA image: %d offset: 0x%x length: 0x%x\n", ux_direction(false), pkt->read_req.image, pkt->read_req.offset, pkt->read_req.length); image_idx = pkt->read_req.image; if (!images || image_idx >= MAPPING_SZ || !images[image_idx].ptr) { ux_err_tag(UX_TAG_SAHARA, "device requested unknown image id %u, " "ensure that all Sahara images are provided\n", image_idx); sahara_send_reset(qdl); return -1; } offset = pkt->read_req.offset; len = pkt->read_req.length; image = &images[image_idx]; if (offset > image->len || len > image->len - offset) { ux_err_tag(UX_TAG_SAHARA, "device requested invalid range of image %d\n", image_idx); return -1; } if (offset == 0) ux_info_tag(UX_TAG_SAHARA, "sending %s (%zu bytes)\n", image->name ? image->name : "(unknown)", image->len); ux_progress("%s", offset + len, image->len, image->name ? image->name : "image"); ret = qdl_write(qdl, image->ptr + offset, len, SAHARA_CMD_TIMEOUT_MS); if (ret < 0 || ((size_t)ret != len)) { ux_err_tag(UX_TAG_SAHARA, "failed to write %zu bytes\n", len); return -1; } return 0; } static int sahara_read64(struct qdl_device *qdl, struct sahara_pkt *pkt, const struct sahara_image *images) { const struct sahara_image *image; unsigned int image_idx; size_t offset; size_t len; int ret; if (pkt->length != SAHARA_READ_DATA64_LENGTH) { ux_err_tag(UX_TAG_SAHARA, "unexpected READ_DATA64 packet length %u\n", pkt->length); sahara_send_reset(qdl); return -1; } ux_debug_tag(UX_TAG_SAHARA, "%s READ_DATA64 image: %" PRId64 " offset: 0x%" PRIx64 " length: 0x%" PRIx64 "\n", ux_direction(false), pkt->read64_req.image, pkt->read64_req.offset, pkt->read64_req.length); image_idx = pkt->read64_req.image; if (!images || image_idx >= MAPPING_SZ || !images[image_idx].ptr) { ux_err_tag(UX_TAG_SAHARA, "device requested unknown image id %u, " "ensure that all Sahara images are provided\n", image_idx); sahara_send_reset(qdl); return -1; } offset = pkt->read64_req.offset; len = pkt->read64_req.length; image = &images[image_idx]; if (offset > image->len || len > image->len - offset) { ux_err_tag(UX_TAG_SAHARA, "device requested invalid range of image %d\n", image_idx); return -1; } if (offset == 0) ux_info_tag(UX_TAG_SAHARA, "sending %s (%zu bytes)\n", image->name ? image->name : "(unknown)", image->len); ux_progress("%s", offset + len, image->len, image->name ? image->name : "image"); ret = qdl_write(qdl, image->ptr + offset, len, SAHARA_CMD_TIMEOUT_MS); if (ret < 0 || ((size_t)ret != len)) { ux_err_tag(UX_TAG_SAHARA, "failed to write %zu bytes\n", len); return -1; } return 0; } static int sahara_eoi(struct qdl_device *qdl, struct sahara_pkt *pkt) { struct sahara_pkt done; if (pkt->length != SAHARA_END_OF_IMAGE_LENGTH) { ux_err_tag(UX_TAG_SAHARA, "unexpected END_OF_IMAGE packet length %u\n", pkt->length); sahara_send_reset(qdl); return -1; } ux_debug_tag(UX_TAG_SAHARA, "%s END_OF_IMAGE image: %u status: 0x%x (%s)\n", ux_direction(false), pkt->eoi.image, pkt->eoi.status, sahara_status_str(pkt->eoi.status)); if (pkt->eoi.status != 0) { ux_err_tag(UX_TAG_SAHARA, "received non-successful end-of-image result: " "0x%x (%s)\n", pkt->eoi.status, sahara_status_str(pkt->eoi.status)); return -1; } done.cmd = SAHARA_DONE_CMD; done.length = SAHARA_DONE_LENGTH; qdl_write(qdl, &done, done.length, SAHARA_CMD_TIMEOUT_MS); return 0; } static int sahara_done(struct qdl_device *qdl, struct sahara_pkt *pkt) { if (pkt->length != SAHARA_DONE_RESP_LENGTH) { ux_err_tag(UX_TAG_SAHARA, "unexpected DONE_RESP packet length %u\n", pkt->length); sahara_send_reset(qdl); return -1; } // 0 == PENDING, 1 == COMPLETE. Device expects more images if // PENDING is set in status. ux_debug_tag(UX_TAG_SAHARA, "%s DONE status: %u (%s)\n", ux_direction(false), pkt->done_resp.status, pkt->done_resp.status ? "complete" : "pending"); return pkt->done_resp.status; } static ssize_t sahara_debug64_one(struct qdl_device *qdl, struct sahara_debug_region64 region, const char *ramdump_path) { struct sahara_pkt read_req; uint64_t remain; size_t offset, buf_offset; size_t chunk; ssize_t written; ssize_t ret = -1; ssize_t n; void *buf; int fd; buf = malloc(DEBUG_BLOCK_SIZE); if (!buf) return -1; char path[PATH_MAX]; /* * region.filename is provided by the device. Reject empty names and * any name containing a path separator so a malicious or malformed * device cannot direct the dump outside of ramdump_path. */ if (region.filename[0] == '\0' || strpbrk(region.filename, "/\\")) { ux_err_tag(UX_TAG_SAHARA, "device provided unsafe ramdump region filename\n"); free(buf); return -1; } snprintf(path, sizeof(path), "%s/%s", ramdump_path, region.filename); fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0644); if (fd < 0) { ux_warn_tag(UX_TAG_SAHARA, "failed to open \"%s\"", region.filename); free(buf); return -1; } chunk = 0; while (chunk < region.length) { remain = MIN((uint64_t)(region.length - chunk), DEBUG_BLOCK_SIZE); read_req.cmd = SAHARA_MEM_READ64_CMD; read_req.length = SAHARA_MEM_READ64_LENGTH; read_req.debug64_req.addr = region.addr + chunk; read_req.debug64_req.length = remain; n = qdl_write(qdl, &read_req, read_req.length, SAHARA_CMD_TIMEOUT_MS); if (n < 0) { ux_warn_tag(UX_TAG_SAHARA, "failed to send ramdump read request"); goto out; } offset = 0; while (offset < remain) { buf_offset = 0; n = qdl_read(qdl, buf, DEBUG_BLOCK_SIZE, 30000); if (n < 0) { ux_warn_tag(UX_TAG_SAHARA, "failed to read ramdump chunk"); goto out; } while (buf_offset < (size_t)n) { written = write(fd, buf + buf_offset, n - buf_offset); if (written <= 0) { ux_warn_tag(UX_TAG_SAHARA, "failed to write ramdump chunk to \"%s\"", region.filename); goto out; } buf_offset += written; } offset += buf_offset; } qdl_read(qdl, buf, DEBUG_BLOCK_SIZE, 10); chunk += DEBUG_BLOCK_SIZE; ux_progress("%s", chunk, region.length, region.filename); } ret = 0; out: close(fd); free(buf); return ret; } // simple pattern matching function supporting * and ? bool pattern_match(const char *pattern, const char *string) { if (*pattern == '\0' && *string == '\0') return true; if (*pattern == '*') return pattern_match(pattern + 1, string) || (*string != '\0' && pattern_match(pattern, string + 1)); if (*pattern == '?') return (*string != '\0') && pattern_match(pattern + 1, string + 1); if (*pattern == *string) return pattern_match(pattern + 1, string + 1); return false; } static bool sahara_debug64_filter(const char *filename, const char *filter) { char stem[PATH_MAX] = {}; bool anymatch = false; const char *dot; char *ptr; char *tmp; char *s; if (!filter) return false; /* Build a stem (filename without extension) for bare-name filter tokens */ dot = strrchr(filename, '.'); if (dot && dot != filename) { size_t len = dot - filename; if (len < sizeof(stem)) { memcpy(stem, filename, len); } } tmp = strdup(filter); for (s = strtok_r(tmp, ",", &ptr); s; s = strtok_r(NULL, ",", &ptr)) { if (pattern_match(s, filename) || (stem[0] && pattern_match(s, stem))) { anymatch = true; break; } } free(tmp); return !anymatch; } static int write_zeroes(FILE *out, size_t count) { static const char zeros[4096]; size_t chunk; while (count > 0) { chunk = count > sizeof(zeros) ? sizeof(zeros) : count; if (fwrite(zeros, 1, chunk, out) != chunk) return -1; count -= chunk; } return 0; } static ssize_t copy_file_to(FILE *out, const char *path) { char buf[65536]; ssize_t written = 0; FILE *in; size_t n; in = fopen(path, "rb"); if (!in) return -1; while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { if (fwrite(buf, 1, n, out) != n) { fclose(in); return -1; } written += n; } fclose(in); return written; } /* * sahara_build_minidump_elf() - assemble a minidump ELF from downloaded regions * @ramdump_path: directory containing the downloaded region files * @filter: the segment filter that was applied during download (may be NULL) * @table: the Sahara debug region table received from the device * @n_regions: number of entries in @table * * When the kernel minidump backend is active, one of the downloaded regions is * named "KELF". It is a fully formed ELF core header whose PT_LOAD program * headers describe the physical address and output offset of every other kernel * region. A second region ("Kvmcorein") carries the vmcoreinfo data that fills * the tail of the PT_NOTE section and has no PT_LOAD entry of its own. * * This function detects those two special regions, then iterates the PT_LOAD * headers in p_offset order, matches each one to a downloaded region file by * physical address, and concatenates everything into a single minidump.elf * ready for crash or gdb. * * All segments are written sequentially because meminspect assigns p_offset * values in a strictly increasing, contiguous order. */ static void sahara_build_minidump_elf(const char *ramdump_path, const char *filter, const struct sahara_debug_region64 *table, size_t n_regions) { char path[PATH_MAX]; char out_path[PATH_MAX]; Elf64_Ehdr *ehdr; Elf64_Phdr *phdrs; void *kelf_buf = NULL; size_t kelf_size; ssize_t kelf_idx = -1; ssize_t vmcore_idx = -1; ssize_t written; FILE *out = NULL; FILE *in; size_t i, j; bool matched; /* Locate the KELF region; bail out silently if absent (not a minidump) */ for (i = 0; i < n_regions; i++) { if (!strncmp(table[i].region, "md_KELF", 7)) { kelf_idx = i; break; } } if (kelf_idx < 0) return; /* If KELF itself was excluded by the filter, nothing to do */ if (sahara_debug64_filter(table[kelf_idx].filename, filter)) return; /* Read the KELF file into memory so we can parse its program headers */ snprintf(path, sizeof(path), "%s/%s", ramdump_path, table[kelf_idx].filename); in = fopen(path, "rb"); if (!in) { ux_err_tag(UX_TAG_MINIDUMP, "failed to open KELF file %s\n", path); return; } long kelf_size_l; fseek(in, 0, SEEK_END); kelf_size_l = ftell(in); if (kelf_size_l <= 0) { ux_err_tag(UX_TAG_MINIDUMP, "failed to get KELF file size\n"); fclose(in); return; } fseek(in, 0, SEEK_SET); kelf_size = (size_t)kelf_size_l; kelf_buf = malloc(kelf_size); if (!kelf_buf) { fclose(in); return; } if (fread(kelf_buf, 1, kelf_size, in) != kelf_size) { ux_err_tag(UX_TAG_MINIDUMP, "failed to read KELF file\n"); fclose(in); goto out_free; } fclose(in); ehdr = kelf_buf; if (kelf_size < sizeof(*ehdr) || memcmp(ehdr->e_ident, ELFMAG, SELFMAG) || ehdr->e_type != ET_CORE || ehdr->e_phentsize != sizeof(Elf64_Phdr) || ehdr->e_phoff > kelf_size || (size_t)ehdr->e_phnum * sizeof(Elf64_Phdr) > kelf_size - ehdr->e_phoff) { ux_err_tag(UX_TAG_MINIDUMP, "KELF does not look like a 64-bit ELF core file\n"); goto out_free; } phdrs = (Elf64_Phdr *)((char *)kelf_buf + ehdr->e_phoff); /* * Identify the vmcoreinfo region: among all K-prefixed regions, it is * the one whose physical address has no matching PT_LOAD p_paddr entry. * It fills the tail of the PT_NOTE section and must be placed immediately * after the KELF content in the output file. */ for (i = 0; i < n_regions; i++) { if ((ssize_t)i == kelf_idx || strncmp(table[i].region, "md_K", 4) != 0) continue; matched = false; for (j = 0; j < ehdr->e_phnum; j++) { if (phdrs[j].p_type == PT_LOAD && phdrs[j].p_paddr == table[i].addr) { matched = true; break; } } if (!matched) { vmcore_idx = i; break; } } snprintf(out_path, sizeof(out_path), "%s/minidump.elf", ramdump_path); out = fopen(out_path, "wb"); if (!out) { ux_err_tag(UX_TAG_MINIDUMP, "failed to create %s\n", out_path); goto out_free; } /* 1. Write KELF: provides the ELF header, phdrs, and partial PT_NOTE */ if (fwrite(kelf_buf, 1, kelf_size, out) != kelf_size) { ux_err_tag(UX_TAG_MINIDUMP, "failed to write KELF to output\n"); goto out_close; } /* 2. Write vmcoreinfo immediately after KELF to complete the PT_NOTE */ if (vmcore_idx >= 0) { snprintf(path, sizeof(path), "%s/%s", ramdump_path, table[vmcore_idx].filename); if (copy_file_to(out, path) < 0) ux_err_tag(UX_TAG_MINIDUMP, "vmcoreinfo region missing, " "crash may not load correctly\n"); } /* * 3. Write PT_LOAD segments in ascending p_offset order. * meminspect assigns p_offset values contiguously in registration * order so iterating the phdr array gives the correct sequence. */ for (j = 0; j < ehdr->e_phnum; j++) { if (phdrs[j].p_type != PT_LOAD) continue; matched = false; for (i = 0; i < n_regions; i++) { if (table[i].addr != phdrs[j].p_paddr) continue; snprintf(path, sizeof(path), "%s/%s", ramdump_path, table[i].filename); written = copy_file_to(out, path); if (written < 0) { ux_err_tag(UX_TAG_MINIDUMP, "region %s missing, skipping\n", table[i].filename); matched = true; break; } /* Zero-pad to p_filesz to keep offsets aligned */ if ((size_t)written < phdrs[j].p_filesz && write_zeroes(out, phdrs[j].p_filesz - (size_t)written)) { ux_err_tag(UX_TAG_MINIDUMP, "failed to pad region %s\n", table[i].filename); goto out_close; } matched = true; break; } if (!matched) ux_err_tag(UX_TAG_MINIDUMP, "no region found for PT_LOAD at paddr 0x%" PRIx64 "\n", phdrs[j].p_paddr); } ux_info_tag(UX_TAG_MINIDUMP, "ELF written to %s\n", out_path); out_close: fclose(out); out_free: free(kelf_buf); } static void sahara_debug64(struct qdl_device *qdl, struct sahara_pkt *pkt, const char *ramdump_path, const char *filter) { struct sahara_debug_region64 *table; struct sahara_pkt read_req; size_t num_regions; ssize_t n; size_t i; if (pkt->length != SAHARA_MEM_DEBUG64_LENGTH) { ux_err_tag(UX_TAG_SAHARA, "unexpected MEM_DEBUG64 packet length %u\n", pkt->length); sahara_send_reset(qdl); return; } ux_debug_tag(UX_TAG_SAHARA, "%s MEMORY_DEBUG64 address: 0x%" PRIx64 " length: 0x%" PRIx64 "\n", ux_direction(false), pkt->debug64_req.addr, pkt->debug64_req.length); read_req.cmd = SAHARA_MEM_READ64_CMD; read_req.length = SAHARA_MEM_READ64_LENGTH; read_req.debug64_req.addr = pkt->debug64_req.addr; read_req.debug64_req.length = pkt->debug64_req.length; n = qdl_write(qdl, &read_req, read_req.length, SAHARA_CMD_TIMEOUT_MS); if (n < 0) return; if (read_req.debug64_req.length > 64 * 1024) { ux_err_tag(UX_TAG_SAHARA, "DEBUG64 table length 0x%" PRIx64 " exceeds limit\n", read_req.debug64_req.length); return; } table = calloc(1, read_req.debug64_req.length); if (!table) return; n = qdl_read(qdl, table, read_req.debug64_req.length, SAHARA_CMD_TIMEOUT_MS); if (n < 0) { free(table); return; } /* Only iterate over region entries actually received. */ num_regions = (size_t)n / sizeof(table[0]); /* The device-provided name fields may not be NUL-terminated. */ for (i = 0; i < num_regions; i++) { table[i].region[sizeof(table[i].region) - 1] = '\0'; table[i].filename[sizeof(table[i].filename) - 1] = '\0'; } for (i = 0; i < num_regions; i++) { if (sahara_debug64_filter(table[i].filename, filter)) { ux_info_tag(UX_TAG_SAHARA, "%s skipped per filter\n", table[i].filename); continue; } ux_debug_tag(UX_TAG_SAHARA, "%-2zu: type 0x%" PRIx64 " address: 0x%" PRIx64 " length: 0x%" PRIx64 " region: %s filename: %s\n", i, table[i].type, table[i].addr, table[i].length, table[i].region, table[i].filename); n = sahara_debug64_one(qdl, table[i], ramdump_path); if (n < 0) break; ux_info_tag(UX_TAG_SAHARA, "%s dumped successfully\n", table[i].filename); } sahara_build_minidump_elf(ramdump_path, filter, table, num_regions); free(table); sahara_send_reset(qdl); } static bool sahara_has_done_pending_quirk(const struct sahara_image *images) { unsigned int count = 0; int i; if (!images) return false; /* * E.g MSM8916 EDL reports done = pending, allow this when one a single * image is provided, and it's used as SAHARA_ID_EHOSTDL_IMG. */ for (i = 0; i < MAPPING_SZ; i++) { if (images[i].ptr) count++; } return count == 1 && images[SAHARA_ID_EHOSTDL_IMG].ptr; } static void sahara_debug_list_images(const struct sahara_image *images) { int i; ux_debug_tag(UX_TAG_SAHARA, "images:\n"); for (i = 0; i < MAPPING_SZ; i++) { if (images[i].ptr) ux_debug_tag(UX_TAG_SAHARA, " %2d: %s\n", i, images[i].name ? : "(unknown)"); } } static uint16_t sahara_le16(const uint8_t *p) { return (uint16_t)(p[0] | (p[1] << 8)); } static uint32_t sahara_le32(const uint8_t *p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); } static uint64_t sahara_le64(const uint8_t *p) { return (uint64_t)sahara_le32(p) | ((uint64_t)sahara_le32(p + 4) << 32); } static void sahara_hexstr(const uint8_t *buf, size_t len, char *out, size_t out_sz) { static const char hex[] = "0123456789abcdef"; size_t i; for (i = 0; i < len && (2 * i + 2) < out_sz; i++) { out[2 * i] = hex[buf[i] >> 4]; out[2 * i + 1] = hex[buf[i] & 0xf]; } out[2 * i] = '\0'; } /* * Normalise the raw OEM PK hash payload down to a single digest. Targets return * it in a few shapes: the bare digest, the digest repeated once per root * certificate, or the digest in a wider fixed-size field zero-padded to the * full width. Trim a repeated copy (detected by the first block recurring in * full), then strip trailing zero padding and round the length back up * to the nearest standard digest size (SHA-256/384/512), so a hash whose final * bytes are legitimately zero is not truncated. The rounding never grows the * result beyond the bytes actually read. */ static size_t sahara_pkhash_trim(const uint8_t *buf, size_t len) { static const size_t digest_sizes[] = { 32, 48, 64 }; size_t orig_len = len; size_t i; for (i = 4; i * 2 <= len; i++) { if (!memcmp(buf, buf + i, i)) { len = i; break; } } while (len > 0 && buf[len - 1] == 0) len--; for (i = 0; i < ARRAY_SIZE(digest_sizes); i++) { if (len <= digest_sizes[i] && digest_sizes[i] <= orig_len) { len = digest_sizes[i]; break; } } return len; } static int sahara_read_payload(struct qdl_device *qdl, void *buf, size_t len) { uint8_t *p = buf; size_t off = 0; int n; while (off < len) { n = qdl_read(qdl, p + off, len - off, SAHARA_CMD_TIMEOUT_MS); if (n < 0) return -1; if (n == 0) break; off += n; } return (int)off; } /* * Run one Sahara command-mode client command: send EXECUTE, learn the reply * size from EXECUTE_RESP, request the payload with EXECUTE_DATA and read it. */ static int sahara_command_exec(struct qdl_device *qdl, uint32_t client_cmd, void *buf, size_t buf_len, size_t *out_len) { struct sahara_pkt req = {}; struct sahara_pkt *resp; uint8_t rxbuf[64]; uint32_t data_len; int n; req.cmd = SAHARA_EXECUTE_CMD; req.length = SAHARA_EXECUTE_LENGTH; req.exec_req.client_cmd = client_cmd; if (qdl_write(qdl, &req, req.length, SAHARA_CMD_TIMEOUT_MS) < 0) return -1; n = qdl_read(qdl, rxbuf, sizeof(rxbuf), SAHARA_CMD_TIMEOUT_MS); if (n < 0) return -1; resp = (struct sahara_pkt *)rxbuf; if ((size_t)n < 4 * sizeof(uint32_t) || resp->cmd != SAHARA_EXECUTE_RESP_CMD) { ux_debug_tag(UX_TAG_SAHARA, "unexpected reply to exec cmd 0x%x\n", client_cmd); return -1; } data_len = resp->exec_resp.data_length; if (data_len == 0 || data_len > buf_len) { ux_debug_tag(UX_TAG_SAHARA, "exec cmd 0x%x reported invalid length %u\n", client_cmd, data_len); return -1; } req.cmd = SAHARA_EXECUTE_DATA_CMD; req.length = SAHARA_EXECUTE_LENGTH; req.exec_req.client_cmd = client_cmd; if (qdl_write(qdl, &req, req.length, SAHARA_CMD_TIMEOUT_MS) < 0) return -1; n = sahara_read_payload(qdl, buf, data_len); if (n < 0 || (size_t)n < data_len) return -1; *out_len = data_len; return 0; } /* * Query and print the chip identity exposed over Sahara command mode. The set * of readable items depends on the protocol version: pre-v3 targets answer * MSM_HW_ID_READ, while v3 targets no longer do and instead expose the same * fields through READ_CHIP_ID_V3. The serial number and (where still permitted) * the OEM PK hash are read on all versions. */ static int sahara_command_info(struct qdl_device *qdl, unsigned int version) { uint8_t payload[512]; char hexbuf[2 * sizeof(payload) + 1]; const char *pkhash = NULL; bool have_serial = false; bool have_hwid = false; uint32_t serial = 0; uint64_t hwid = 0; uint32_t msm_id = 0; unsigned int oem_id = 0; unsigned int model_id = 0; size_t len; if (sahara_command_exec(qdl, SAHARA_EXEC_CMD_SERIAL_NUM_READ, payload, sizeof(payload), &len) == 0 && len >= 4) { serial = sahara_le32(payload); have_serial = true; } if (version < 3) { if (sahara_command_exec(qdl, SAHARA_EXEC_CMD_MSM_HW_ID_READ, payload, sizeof(payload), &len) == 0 && len >= 8) { hwid = sahara_le64(payload); msm_id = hwid >> 32; oem_id = (hwid >> 16) & 0xffff; model_id = hwid & 0xffff; have_hwid = true; } } else { if (sahara_command_exec(qdl, SAHARA_EXEC_CMD_READ_CHIP_ID_V3, payload, sizeof(payload), &len) == 0 && len >= 44) { msm_id = sahara_le32(payload + 36); oem_id = sahara_le16(payload + 40); model_id = sahara_le16(payload + 42); /* Some v3 targets carry the OEM ID in an alternate slot */ if (oem_id == 0 && len >= 46) oem_id = sahara_le16(payload + 44); hwid = ((uint64_t)msm_id << 32) | ((uint64_t)oem_id << 16) | model_id; have_hwid = true; } } if (sahara_command_exec(qdl, SAHARA_EXEC_CMD_OEM_PK_HASH_READ, payload, sizeof(payload), &len) == 0 && len > 0) { len = sahara_pkhash_trim(payload, len); sahara_hexstr(payload, len, hexbuf, sizeof(hexbuf)); pkhash = hexbuf; } if (!have_serial && !have_hwid && !pkhash) { ux_err_tag(UX_TAG_SAHARA, "device did not return any chip identity information\n"); return -1; } ux_info("Sahara protocol version: %u\n", version); if (have_serial) ux_info("Chip serial number: 0x%08x\n", serial); if (have_hwid) ux_info("HW ID: 0x%016" PRIx64 " (MSM_ID:0x%08x, OEM_ID:0x%04x, MODEL_ID:0x%04x)\n", hwid, msm_id, oem_id, model_id); if (pkhash) ux_info("OEM PK hash: 0x%s\n", pkhash); return 0; } static void sahara_send_switch_mode(struct qdl_device *qdl, unsigned int mode) { struct sahara_pkt pkt = {}; pkt.cmd = SAHARA_SWITCH_MODE_CMD; pkt.length = SAHARA_SWITCH_MODE_LENGTH; pkt.switch_mode.mode = mode; qdl_write(qdl, &pkt, pkt.length, SAHARA_CMD_TIMEOUT_MS); } /* * Read and print the chip identity (serial, HW ID and OEM PK hash) by entering * Sahara command mode. Unlike the image-transfer path this requests * SAHARA_MODE_COMMAND in the HELLO response and drives EXECUTE transactions. * Command mode is left by switching back to image transfer, which returns the * device to its initial HELLO state. */ int sahara_chipinfo(struct qdl_device *qdl) { unsigned int version = SAHARA_VERSION; struct sahara_pkt *pkt; char buf[4096]; int ret; int n; if (qdl->dev_type == QDL_DEVICE_SIM) return 0; n = qdl_read(qdl, buf, sizeof(buf), SAHARA_CMD_TIMEOUT_MS); /* A Firehose programmer is already running; chip info needs Sahara */ if (n >= 5 && !memcmp(buf, "dev_type != QDL_DEVICE_QUD && qdl->dev_type != QDL_DEVICE_AUTO)) { ux_err_tag(UX_TAG_SAHARA, "failed to read HELLO from device\n"); return -1; } sahara_send_hello_resp(qdl, SAHARA_VERSION, SAHARA_MODE_COMMAND); } else { pkt = (struct sahara_pkt *)buf; if ((uint32_t)n != pkt->length || pkt->cmd != SAHARA_HELLO_CMD) { ux_err_tag(UX_TAG_SAHARA, "unexpected packet 0x%x while waiting for HELLO\n", pkt->cmd); return -1; } version = pkt->hello_req.version; ux_debug_tag(UX_TAG_SAHARA, "%s HELLO version %u mode %u\n", ux_direction(false), version, pkt->hello_req.mode); sahara_send_hello_resp(qdl, version, SAHARA_MODE_COMMAND); } n = qdl_read(qdl, buf, sizeof(buf), SAHARA_CMD_TIMEOUT_MS); if (n < 0) { ux_err_tag(UX_TAG_SAHARA, "no CMD_READY received; device may not support command mode\n"); return -1; } pkt = (struct sahara_pkt *)buf; if (pkt->cmd != SAHARA_CMD_READY_CMD) { if (pkt->cmd == SAHARA_END_OF_IMAGE_CMD) ux_err_tag(UX_TAG_SAHARA, "device rejected command mode: " "end-of-image status 0x%x (%s)\n", pkt->eoi.status, sahara_status_str(pkt->eoi.status)); else ux_err_tag(UX_TAG_SAHARA, "unexpected packet 0x%x while entering command mode\n", pkt->cmd); return -1; } ret = sahara_command_info(qdl, version); /* * Switch back to image-transfer mode so the device re-issues its HELLO * and stays usable for a subsequent query or flash, without a reset. */ sahara_send_switch_mode(qdl, SAHARA_MODE_IMAGE_TX_PENDING); return ret; } /* * Reset the device over the Sahara protocol, from whichever Sahara state * it booted into: fresh EDL (image transfer pending) or crash mode * (memory debug). The HELLO is acknowledged with the mode the device * asked for, so no state transition is forced before the reset command. * * Return: 0 when the reset was issued over Sahara, 1 when a Firehose * programmer is already running (the caller should reset via Firehose * instead), -1 on failure. */ int sahara_device_reset(struct qdl_device *qdl) { struct sahara_pkt *pkt; char buf[4096]; int n; int i; if (qdl->dev_type == QDL_DEVICE_SIM) return 0; n = qdl_read(qdl, buf, sizeof(buf), SAHARA_CMD_TIMEOUT_MS); /* A Firehose programmer is already running; there is no Sahara here */ if (n >= 5 && !memcmp(buf, "= 5 && !memcmp(buf, "length && pkt->cmd == SAHARA_HELLO_CMD) { ux_debug_tag(UX_TAG_SAHARA, "%s HELLO version %u mode %u\n", ux_direction(false), pkt->hello_req.version, pkt->hello_req.mode); /* * The target services a reset only in states where it * is valid: in image-transfer-pending state the PBL * rejects it with an end-of-image error, while command * mode accepts it. Crash-mode devices announce memory * debug and take the reset directly, as the ramdump * teardown has always relied on. */ if (pkt->hello_req.mode == SAHARA_MODE_MEMORY_DEBUG) { sahara_send_hello_resp(qdl, pkt->hello_req.version, SAHARA_MODE_MEMORY_DEBUG); } else { sahara_send_hello_resp(qdl, pkt->hello_req.version, SAHARA_MODE_COMMAND); n = qdl_read(qdl, buf, sizeof(buf), SAHARA_CMD_TIMEOUT_MS); if (n < 0 || pkt->cmd != SAHARA_CMD_READY_CMD) ux_debug_tag(UX_TAG_SAHARA, "no CMD_READY after command mode request\n"); } } /* On any other packet, proceed straight to the reset */ } /* * The reset response is the target's acknowledgment that it accepted * the request, sent just before it resets; the protocol directs the * host to resend the reset until the response arrives. The resend * matters in practice: after the HELLO exchange the target * immediately issues its first READ_DATA, which races the reset * request, so the first answer read back is often not the response. */ for (i = 0; i < SAHARA_RESET_RETRIES; i++) { sahara_send_reset(qdl); n = qdl_read(qdl, buf, sizeof(buf), SAHARA_CMD_TIMEOUT_MS); if (n < 0) { /* * A transport error other than a timeout means the * device already dropped off the bus resetting. */ if (n != -ETIMEDOUT) break; continue; } if ((size_t)n < sizeof(uint32_t) * 2) continue; pkt = (struct sahara_pkt *)buf; if (pkt->cmd == SAHARA_RESET_RESP_CMD) { ux_debug_tag(UX_TAG_SAHARA, "reset acknowledged\n"); break; } /* * An invalid reset request is rejected with an end-of-image * transfer packet carrying the error status. */ if (pkt->cmd == SAHARA_END_OF_IMAGE_CMD) { ux_err_tag(UX_TAG_SAHARA, "device rejected the reset request: " "end-of-image status 0x%x (%s)\n", pkt->eoi.status, sahara_status_str(pkt->eoi.status)); return -1; } /* Some other packet crossed the reset request; resend */ } if (i == SAHARA_RESET_RETRIES) { ux_err_tag(UX_TAG_SAHARA, "no reset response from device\n"); return -1; } /* * The response only acknowledges the request; verify that the device * actually went down. A PBL can tear down its Sahara session on the * reset request without resetting the SoC (observed on QRB2210), * which leaves the transport alive but the session dead. Only the * transport dropping - the handle dying with the re-enumeration - * proves the reset happened. */ n = qdl_read(qdl, buf, sizeof(buf), SAHARA_RESET_VERIFY_TIMEOUT_MS); if (n >= 0 || n == -ETIMEDOUT) { ux_err_tag(UX_TAG_SAHARA, "device acknowledged the reset but did not go down; " "power cycle it or reset through a Firehose programmer: " "qdl reset\n"); return -1; } ux_info_tag(UX_TAG_SAHARA, "device reset\n"); return 0; firehose: ux_info_tag(UX_TAG_SAHARA, "Firehose programmer is running\n"); return 1; } int sahara_run(struct qdl_device *qdl, const struct sahara_image *images, const char *ramdump_path, const char *ramdump_filter) { /* * Auto-detect that the device is already running the Firehose * programmer (e.g. left running by a previous --skip-reset * invocation): if the first read times out or returns a Firehose XML * banner instead of a Sahara HELLO, skip Sahara entirely. Disabled * in ramdump mode, where Sahara is the only valid protocol. */ const bool detect_firehose = !ramdump_path; bool first_read = true; struct sahara_pkt *pkt; char buf[4096]; char tmp[32]; bool done = false; int n; if (images) sahara_debug_list_images(images); /* * Don't need to do anything in simulation mode with Sahara, * we care only about Firehose protocol */ if (qdl->dev_type == QDL_DEVICE_SIM) return 0; while (!done) { n = qdl_read(qdl, buf, sizeof(buf), SAHARA_CMD_TIMEOUT_MS); if (n < 0) { if (first_read && detect_firehose && n == -ETIMEDOUT) { /* * The QUD driver will eat the HELLO request on * many modern targets, so send an unsolicited * HELLO response. * If the device is already in Firehose mode, * the programmer will fail to parse the "XML" * message and report an error, which will * trigger below detection of a dev_type == QDL_DEVICE_QUD || qdl->dev_type == QDL_DEVICE_AUTO) { sahara_send_hello_resp(qdl, SAHARA_VERSION, 0); continue; } ux_info_tag(UX_TAG_SAHARA, "no HELLO received; " "assuming Firehose programmer is already running\n"); return 0; } ux_err_tag(UX_TAG_SAHARA, "failed to read request from device\n"); break; } if (first_read && detect_firehose && n >= 5 && !memcmp(buf, "length) { ux_err_tag(UX_TAG_SAHARA, "request length not matching received request\n"); return -EINVAL; } switch (pkt->cmd) { case SAHARA_HELLO_CMD: if (sahara_hello(qdl, pkt) < 0) return -1; break; case SAHARA_READ_DATA_CMD: if (sahara_read(qdl, pkt, images) < 0) return -1; break; case SAHARA_END_OF_IMAGE_CMD: if (sahara_eoi(qdl, pkt) < 0) return -1; break; case SAHARA_DONE_RESP_CMD: n = sahara_done(qdl, pkt); if (n < 0) return -1; done = n; /* E.g MSM8916 EDL reports done = 0 here */ if (sahara_has_done_pending_quirk(images)) done = true; break; case SAHARA_MEM_DEBUG64_CMD: sahara_debug64(qdl, pkt, ramdump_path, ramdump_filter); break; case SAHARA_READ_DATA64_CMD: if (sahara_read64(qdl, pkt, images) < 0) return -1; break; case SAHARA_RESET_RESP_CMD: if (pkt->length != SAHARA_RESET_LENGTH) { ux_err_tag(UX_TAG_SAHARA, "unexpected RESET_RESP packet length %u\n", pkt->length); return -1; } if (ramdump_path) done = true; break; default: sprintf(tmp, "SAHARA: CMD%x", pkt->cmd); print_hex_dump(tmp, buf, n); break; } } return done ? 0 : -1; } linux-msm-qdl-6e625aa/src/sahara_config.c000066400000000000000000000101541527206113100203520ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2016-2017, Linaro Ltd. * Copyright (c) 2018, The Linux Foundation. All rights reserved. * All rights reserved. */ #include #include #include #include #include #include #include "contents.h" #include "pathbuf.h" #include "qdl.h" #include "oscompat.h" /** * decode_sahara_config() - Attempt to decode a Sahara config XML document * @blob: Loaded image to be decoded as Sahara config * @images: List of Sahara images, with @images[0] populated * * The single blob provided in @images[0] might be a XML blob containing * a sahara_config document with definitions of the various Sahara images that * will be loaded. Attempt to parse this and if possible load each referenced * Sahara image into the @images array. * * The original blob (in @images[0]) is freed once it has been consumed. * * Lives in its own translation unit so that both the qdl binary and the * out-of-tree nbdkit plugin can link it, while the contents unit tests can * still stub it out. * * Returns: 0 if no archive was found, 1 if archive was decoded, -1 on error */ int decode_sahara_config(struct sahara_image *blob, struct sahara_image *images, struct contents_filter *contents_filter) { char image_path_full[PATH_MAX]; struct pathbuf image_full_path = {}; const char *image_path; unsigned int image_id; size_t image_path_len; xmlNode *images_node; xmlNode *image_node; char *blob_name_buf; size_t base_path_len; char *base_path; xmlNode *root; xmlDoc *doc; int errors = 0; int ret; if (blob->len < 5 || memcmp(blob->ptr, "ptr, blob->len, blob->name, NULL, 0); if (!doc) { ux_err("failed to parse sahara_config in \"%s\"\n", blob->name); return -1; } blob_name_buf = strdup(blob->name); base_path = dirname(blob_name_buf); base_path_len = strlen(base_path); root = xmlDocGetRootElement(doc); if (xmlStrcmp(root->name, (xmlChar *)"sahara_config")) { ux_err("specified sahara_config \"%s\" is not a Sahara config\n", blob->name); goto err_free_doc; } for (images_node = root->children; images_node; images_node = images_node->next) { if (images_node->type == XML_ELEMENT_NODE && !xmlStrcmp(images_node->name, (xmlChar *)"images")) break; } if (!images_node) { ux_err("no images definitions found in sahara_config \"%s\"\n", blob->name); goto err_free_doc; } for (image_node = images_node->children; image_node; image_node = image_node->next) { if (image_node->type != XML_ELEMENT_NODE || xmlStrcmp(image_node->name, (xmlChar *)"image")) continue; image_id = attr_as_unsigned(image_node, "image_id", &errors); image_path = attr_as_string(image_node, "image_path", &errors); if (image_id == 0 || image_id >= MAPPING_SZ || errors || !image_path) { ux_err("invalid sahara_config image in \"%s\"\n", blob->name); free((void *)image_path); goto err_free_doc; } image_path_len = strlen(image_path); if (contents_resolve_path(contents_filter, image_path, &image_full_path) == 1) { memcpy(image_path_full, image_full_path.buf, image_full_path.len + 1); } else if (path_is_absolute(image_path)) { if (image_path_len + 1 > PATH_MAX) { free((void *)image_path); goto err_free_doc; } memcpy(image_path_full, image_path, image_path_len + 1); } else { if (base_path_len + 1 + image_path_len + 1 > PATH_MAX) { free((void *)image_path); goto err_free_doc; } memcpy(image_path_full, base_path, base_path_len); image_path_full[base_path_len] = '/'; memcpy(image_path_full + base_path_len + 1, image_path, image_path_len); image_path_full[base_path_len + 1 + image_path_len] = '\0'; } free((void *)image_path); ret = load_sahara_image(NULL, image_path_full, &images[image_id]); if (ret < 0) goto err_free_doc; } xmlFreeDoc(doc); free(blob_name_buf); free(blob->ptr); blob->ptr = NULL; blob->len = 0; return 1; err_free_doc: sahara_images_free(images, MAPPING_SZ); free(blob->ptr); blob->ptr = NULL; blob->len = 0; xmlFreeDoc(doc); free(blob_name_buf); return -1; } linux-msm-qdl-6e625aa/src/sha2.c000066400000000000000000000304421527206113100164250ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* $OpenBSD: sha2.c,v 1.28 2019/07/23 12:35:22 dtucker Exp $ */ /* * FILE: sha2.c * AUTHOR: Aaron D. Gifford * * Copyright (c) 2000-2001, Aaron D. Gifford * All rights reserved. * * $From: sha2.c,v 1.1 2001/11/08 00:01:51 adg Exp adg $ */ #include #include #include "sha2.h" /* * UNROLLED TRANSFORM LOOP NOTE: * You can define SHA2_UNROLL_TRANSFORM to use the unrolled transform * loop version for the hash transform rounds (defined using macros * later in this file). Either define on the command line, for example: * * cc -DSHA2_UNROLL_TRANSFORM -o sha2 sha2.c sha2prog.c * * or define below: * * #define SHA2_UNROLL_TRANSFORM * */ #ifndef SHA2_SMALL #if defined(__amd64__) || defined(__i386__) #define SHA2_UNROLL_TRANSFORM #endif #endif /*** SHA-224/256/384/512 Machine Architecture Definitions *****************/ /* * BYTE_ORDER NOTE: * * Please make sure that your system defines BYTE_ORDER. If your * architecture is little-endian, make sure it also defines * LITTLE_ENDIAN and that the two (BYTE_ORDER and LITTLE_ENDIAN) are * equivalent. * * If your system does not define the above, then you can do so by * hand like this: * * #define LITTLE_ENDIAN 1234 * #define BIG_ENDIAN 4321 * * And for little-endian machines, add: * * #define BYTE_ORDER LITTLE_ENDIAN * * Or for big-endian machines: * * #define BYTE_ORDER BIG_ENDIAN * * The FreeBSD machine this was written on defines BYTE_ORDER * appropriately by including (which in turn includes * where the appropriate definitions are actually * made). */ #if defined(__MINGW32__) || defined(__MINGW64__) #include #endif #if !defined(BYTE_ORDER) || (BYTE_ORDER != LITTLE_ENDIAN && BYTE_ORDER != BIG_ENDIAN) #error Define BYTE_ORDER to be equal to either LITTLE_ENDIAN or BIG_ENDIAN #endif /*** SHA-224/256/384/512 Various Length Definitions ***********************/ /* NOTE: Most of these are in sha2.h */ #define SHA256_SHORT_BLOCK_LENGTH (SHA256_BLOCK_LENGTH - 8) /*** ENDIAN SPECIFIC COPY MACROS **************************************/ #define BE_8_TO_32(dst, cp) do { \ (dst) = (uint32_t)(cp)[3] | ((uint32_t)(cp)[2] << 8) | \ ((uint32_t)(cp)[1] << 16) | ((uint32_t)(cp)[0] << 24); \ } while(0) #define BE_8_TO_64(dst, cp) do { \ (dst) = (uint64_t)(cp)[7] | ((uint64_t)(cp)[6] << 8) | \ ((uint64_t)(cp)[5] << 16) | ((uint64_t)(cp)[4] << 24) | \ ((uint64_t)(cp)[3] << 32) | ((uint64_t)(cp)[2] << 40) | \ ((uint64_t)(cp)[1] << 48) | ((uint64_t)(cp)[0] << 56); \ } while (0) #define BE_64_TO_8(cp, src) do { \ (cp)[0] = (src) >> 56; \ (cp)[1] = (src) >> 48; \ (cp)[2] = (src) >> 40; \ (cp)[3] = (src) >> 32; \ (cp)[4] = (src) >> 24; \ (cp)[5] = (src) >> 16; \ (cp)[6] = (src) >> 8; \ (cp)[7] = (src); \ } while (0) #define BE_32_TO_8(cp, src) do { \ (cp)[0] = (src) >> 24; \ (cp)[1] = (src) >> 16; \ (cp)[2] = (src) >> 8; \ (cp)[3] = (src); \ } while (0) /* * Macro for incrementally adding the unsigned 64-bit integer n to the * unsigned 128-bit integer (represented using a two-element array of * 64-bit words): */ #define ADDINC128(w,n) do { \ (w)[0] += (uint64_t)(n); \ if ((w)[0] < (n)) { \ (w)[1]++; \ } \ } while (0) /*** THE SIX LOGICAL FUNCTIONS ****************************************/ /* * Bit shifting and rotation (used by the six SHA-XYZ logical functions: * * NOTE: The naming of R and S appears backwards here (R is a SHIFT and * S is a ROTATION) because the SHA-224/256/384/512 description document * (see http://csrc.nist.gov/cryptval/shs/sha256-384-512.pdf) uses this * same "backwards" definition. */ /* Shift-right (used in SHA-224, SHA-256, SHA-384, and SHA-512): */ #define R(b,x) ((x) >> (b)) /* 32-bit Rotate-right (used in SHA-224 and SHA-256): */ #define S32(b,x) (((x) >> (b)) | ((x) << (32 - (b)))) /* Two of six logical functions used in SHA-224, SHA-256, SHA-384, and SHA-512: */ #define Ch(x,y,z) (((x) & (y)) ^ ((~(x)) & (z))) #define Maj(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) /* Four of six logical functions used in SHA-224 and SHA-256: */ #define Sigma0_256(x) (S32(2, (x)) ^ S32(13, (x)) ^ S32(22, (x))) #define Sigma1_256(x) (S32(6, (x)) ^ S32(11, (x)) ^ S32(25, (x))) #define sigma0_256(x) (S32(7, (x)) ^ S32(18, (x)) ^ R(3 , (x))) #define sigma1_256(x) (S32(17, (x)) ^ S32(19, (x)) ^ R(10, (x))) /*** SHA-XYZ INITIAL HASH VALUES AND CONSTANTS ************************/ /* Hash constant words K for SHA-224 and SHA-256: */ static const uint32_t K256[64] = { 0x428a2f98UL, 0x71374491UL, 0xb5c0fbcfUL, 0xe9b5dba5UL, 0x3956c25bUL, 0x59f111f1UL, 0x923f82a4UL, 0xab1c5ed5UL, 0xd807aa98UL, 0x12835b01UL, 0x243185beUL, 0x550c7dc3UL, 0x72be5d74UL, 0x80deb1feUL, 0x9bdc06a7UL, 0xc19bf174UL, 0xe49b69c1UL, 0xefbe4786UL, 0x0fc19dc6UL, 0x240ca1ccUL, 0x2de92c6fUL, 0x4a7484aaUL, 0x5cb0a9dcUL, 0x76f988daUL, 0x983e5152UL, 0xa831c66dUL, 0xb00327c8UL, 0xbf597fc7UL, 0xc6e00bf3UL, 0xd5a79147UL, 0x06ca6351UL, 0x14292967UL, 0x27b70a85UL, 0x2e1b2138UL, 0x4d2c6dfcUL, 0x53380d13UL, 0x650a7354UL, 0x766a0abbUL, 0x81c2c92eUL, 0x92722c85UL, 0xa2bfe8a1UL, 0xa81a664bUL, 0xc24b8b70UL, 0xc76c51a3UL, 0xd192e819UL, 0xd6990624UL, 0xf40e3585UL, 0x106aa070UL, 0x19a4c116UL, 0x1e376c08UL, 0x2748774cUL, 0x34b0bcb5UL, 0x391c0cb3UL, 0x4ed8aa4aUL, 0x5b9cca4fUL, 0x682e6ff3UL, 0x748f82eeUL, 0x78a5636fUL, 0x84c87814UL, 0x8cc70208UL, 0x90befffaUL, 0xa4506cebUL, 0xbef9a3f7UL, 0xc67178f2UL }; /* Initial hash value H for SHA-256: */ static const uint32_t sha256_initial_hash_value[8] = { 0x6a09e667UL, 0xbb67ae85UL, 0x3c6ef372UL, 0xa54ff53aUL, 0x510e527fUL, 0x9b05688cUL, 0x1f83d9abUL, 0x5be0cd19UL }; /*** SHA-256: *********************************************************/ void SHA256Init(SHA2_CTX *context) { memcpy(context->state.st32, sha256_initial_hash_value, sizeof(sha256_initial_hash_value)); memset(context->buffer, 0, sizeof(context->buffer)); context->bitcount[0] = 0; } #ifdef SHA2_UNROLL_TRANSFORM /* Unrolled SHA-256 round macros: */ #define ROUND256_0_TO_15(a,b,c,d,e,f,g,h) do { \ BE_8_TO_32(W256[j], data); \ data += 4; \ T1 = (h) + Sigma1_256((e)) + Ch((e), (f), (g)) + K256[j] + W256[j]; \ (d) += T1; \ (h) = T1 + Sigma0_256((a)) + Maj((a), (b), (c)); \ j++; \ } while(0) #define ROUND256(a,b,c,d,e,f,g,h) do { \ s0 = W256[(j+1)&0x0f]; \ s0 = sigma0_256(s0); \ s1 = W256[(j+14)&0x0f]; \ s1 = sigma1_256(s1); \ T1 = (h) + Sigma1_256((e)) + Ch((e), (f), (g)) + K256[j] + \ (W256[j&0x0f] += s1 + W256[(j+9)&0x0f] + s0); \ (d) += T1; \ (h) = T1 + Sigma0_256((a)) + Maj((a), (b), (c)); \ j++; \ } while(0) void SHA256Transform(uint32_t state[8], const uint8_t data[SHA256_BLOCK_LENGTH]) { uint32_t a, b, c, d, e, f, g, h, s0, s1; uint32_t T1, W256[16]; int j; /* Initialize registers with the prev. intermediate value */ a = state[0]; b = state[1]; c = state[2]; d = state[3]; e = state[4]; f = state[5]; g = state[6]; h = state[7]; j = 0; do { /* Rounds 0 to 15 (unrolled): */ ROUND256_0_TO_15(a,b,c,d,e,f,g,h); ROUND256_0_TO_15(h,a,b,c,d,e,f,g); ROUND256_0_TO_15(g,h,a,b,c,d,e,f); ROUND256_0_TO_15(f,g,h,a,b,c,d,e); ROUND256_0_TO_15(e,f,g,h,a,b,c,d); ROUND256_0_TO_15(d,e,f,g,h,a,b,c); ROUND256_0_TO_15(c,d,e,f,g,h,a,b); ROUND256_0_TO_15(b,c,d,e,f,g,h,a); } while (j < 16); /* Now for the remaining rounds up to 63: */ do { ROUND256(a,b,c,d,e,f,g,h); ROUND256(h,a,b,c,d,e,f,g); ROUND256(g,h,a,b,c,d,e,f); ROUND256(f,g,h,a,b,c,d,e); ROUND256(e,f,g,h,a,b,c,d); ROUND256(d,e,f,g,h,a,b,c); ROUND256(c,d,e,f,g,h,a,b); ROUND256(b,c,d,e,f,g,h,a); } while (j < 64); /* Compute the current intermediate hash value */ state[0] += a; state[1] += b; state[2] += c; state[3] += d; state[4] += e; state[5] += f; state[6] += g; state[7] += h; /* Clean up */ a = b = c = d = e = f = g = h = T1 = 0; } #else /* SHA2_UNROLL_TRANSFORM */ void SHA256Transform(uint32_t state[8], const uint8_t data[SHA256_BLOCK_LENGTH]) { uint32_t a, b, c, d, e, f, g, h, s0, s1; uint32_t T1, T2, W256[16]; int j; /* Initialize registers with the prev. intermediate value */ a = state[0]; b = state[1]; c = state[2]; d = state[3]; e = state[4]; f = state[5]; g = state[6]; h = state[7]; j = 0; do { BE_8_TO_32(W256[j], data); data += 4; /* Apply the SHA-256 compression function to update a..h */ T1 = h + Sigma1_256(e) + Ch(e, f, g) + K256[j] + W256[j]; T2 = Sigma0_256(a) + Maj(a, b, c); h = g; g = f; f = e; e = d + T1; d = c; c = b; b = a; a = T1 + T2; j++; } while (j < 16); do { /* Part of the message block expansion: */ s0 = W256[(j+1)&0x0f]; s0 = sigma0_256(s0); s1 = W256[(j+14)&0x0f]; s1 = sigma1_256(s1); /* Apply the SHA-256 compression function to update a..h */ T1 = h + Sigma1_256(e) + Ch(e, f, g) + K256[j] + (W256[j&0x0f] += s1 + W256[(j+9)&0x0f] + s0); T2 = Sigma0_256(a) + Maj(a, b, c); h = g; g = f; f = e; e = d + T1; d = c; c = b; b = a; a = T1 + T2; j++; } while (j < 64); /* Compute the current intermediate hash value */ state[0] += a; state[1] += b; state[2] += c; state[3] += d; state[4] += e; state[5] += f; state[6] += g; state[7] += h; /* Clean up */ a = b = c = d = e = f = g = h = T1 = T2 = 0; } #endif /* SHA2_UNROLL_TRANSFORM */ void SHA256Update(SHA2_CTX *context, const uint8_t *data, size_t len) { uint64_t freespace, usedspace; /* Calling with no data is valid (we do nothing) */ if (len == 0) return; usedspace = (context->bitcount[0] >> 3) % SHA256_BLOCK_LENGTH; if (usedspace > 0) { /* Calculate how much free space is available in the buffer */ freespace = SHA256_BLOCK_LENGTH - usedspace; if (len >= freespace) { /* Fill the buffer completely and process it */ memcpy(&context->buffer[usedspace], data, freespace); context->bitcount[0] += freespace << 3; len -= freespace; data += freespace; SHA256Transform(context->state.st32, context->buffer); } else { /* The buffer is not yet full */ memcpy(&context->buffer[usedspace], data, len); context->bitcount[0] += (uint64_t)len << 3; /* Clean up: */ usedspace = freespace = 0; return; } } while (len >= SHA256_BLOCK_LENGTH) { /* Process as many complete blocks as we can */ SHA256Transform(context->state.st32, data); context->bitcount[0] += SHA256_BLOCK_LENGTH << 3; len -= SHA256_BLOCK_LENGTH; data += SHA256_BLOCK_LENGTH; } if (len > 0) { /* There's left-overs, so save 'em */ memcpy(context->buffer, data, len); context->bitcount[0] += len << 3; } /* Clean up: */ usedspace = freespace = 0; } void SHA256Pad(SHA2_CTX *context) { unsigned int usedspace; usedspace = (context->bitcount[0] >> 3) % SHA256_BLOCK_LENGTH; if (usedspace > 0) { /* Begin padding with a 1 bit: */ context->buffer[usedspace++] = 0x80; if (usedspace <= SHA256_SHORT_BLOCK_LENGTH) { /* Set-up for the last transform: */ memset(&context->buffer[usedspace], 0, SHA256_SHORT_BLOCK_LENGTH - usedspace); } else { if (usedspace < SHA256_BLOCK_LENGTH) { memset(&context->buffer[usedspace], 0, SHA256_BLOCK_LENGTH - usedspace); } /* Do second-to-last transform: */ SHA256Transform(context->state.st32, context->buffer); /* Prepare for last transform: */ memset(context->buffer, 0, SHA256_SHORT_BLOCK_LENGTH); } } else { /* Set-up for the last transform: */ memset(context->buffer, 0, SHA256_SHORT_BLOCK_LENGTH); /* Begin padding with a 1 bit: */ *context->buffer = 0x80; } /* Store the length of input data (in bits) in big endian format: */ BE_64_TO_8(&context->buffer[SHA256_SHORT_BLOCK_LENGTH], context->bitcount[0]); /* Final transform: */ SHA256Transform(context->state.st32, context->buffer); /* Clean up: */ usedspace = 0; } void SHA256Final(uint8_t digest[SHA256_DIGEST_LENGTH], SHA2_CTX *context) { SHA2_CTX *volatile const contextv = context; SHA256Pad(context); #if BYTE_ORDER == LITTLE_ENDIAN int i; /* Convert TO host byte order */ for (i = 0; i < 8; i++) BE_32_TO_8(digest + i * 4, context->state.st32[i]); #else memcpy(digest, context->state.st32, SHA256_DIGEST_LENGTH); #endif memset(contextv, 0, sizeof(*contextv)); }linux-msm-qdl-6e625aa/src/sha2.h000066400000000000000000000021511527206113100164260ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* $OpenBSD: sha2.h,v 1.10 2016/09/03 17:00:29 tedu Exp $ */ /* * FILE: sha2.h * AUTHOR: Aaron D. Gifford * * Copyright (c) 2000-2001, Aaron D. Gifford * All rights reserved. * * $From: sha2.h,v 1.1 2001/11/08 00:02:01 adg Exp adg $ */ #ifndef __SHA2_H__ #define __SHA2_H__ #include "stdint.h" /*** SHA-256/384/512 Various Length Definitions ***********************/ #define SHA256_BLOCK_LENGTH 64 #define SHA256_DIGEST_LENGTH 32 #define SHA256_DIGEST_STRING_LENGTH (SHA256_DIGEST_LENGTH * 2 + 1) /*** SHA-224/256/384/512 Context Structure *******************************/ typedef struct _SHA2_CTX { union { uint32_t st32[8]; uint64_t st64[8]; } state; uint64_t bitcount[2]; uint8_t buffer[SHA256_BLOCK_LENGTH]; } SHA2_CTX; void SHA256Init(SHA2_CTX *); void SHA256Transform(uint32_t state[8], const uint8_t [SHA256_BLOCK_LENGTH]); void SHA256Update(SHA2_CTX *, const uint8_t *, size_t); void SHA256Pad(SHA2_CTX *); void SHA256Final(uint8_t [SHA256_DIGEST_LENGTH], SHA2_CTX *); char *SHA256End(SHA2_CTX *, char *); #endif /* __SHA2_H__ */linux-msm-qdl-6e625aa/src/sim.c000066400000000000000000000511311527206113100163560ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2025, Qualcomm Innovation Center, Inc. All rights reserved. */ #include #include #include #include #include #include #include "oscompat.h" #include "sha2.h" #include "sim.h" /* * Response XML templates sent by the simulated device. * * The configure response echoes back MaxPayloadSizeToTargetInBytes matching * the host's initial value (1 MiB), so firehose_try_configure() never * triggers a second configure exchange. */ #define SIM_ACK \ "" \ "" #define SIM_ACK_RAWMODE \ "" \ "" #define SIM_CONFIGURE_ACK \ "" \ "" /* sim_state tracks whether the sim is in XML command mode or raw binary mode */ enum sim_state { SIM_STATE_XML, /* normal state: dequeue queued XML responses */ SIM_STATE_RAW_IN, /* receiving raw binary data from host (program) */ SIM_STATE_RAW_OUT, /* sending raw binary data to host (read) */ }; struct sim_response { char *data; size_t len; struct sim_response *next; }; /* * VIP hash validation state. * * sim_vip_recv_table() extracts the data-chunk SHA256 hashes from each * received VIP table (signed MBN or raw chained binary) and stores them in * vip_hashes[]. sim_vip_check_chunk() computes SHA256 of each incoming chunk * and compares it against the next stored entry. * * Maximum capacity: * signed table: MAX_DIGESTS_PER_SIGNED_FILE - 1 = 53 chunk hashes * chained tables: MAX_CHAINED_FILES x (MAX_DIGESTS_PER_CHAINED_FILE - 1) * = 32 x 255 = 8160 chunk hashes * total: 8213 entries x SHA256_DIGEST_LENGTH bytes = ~256 KiB * (heap-allocated together with the enclosing struct qdl_device_sim) */ #define SIM_VIP_MAX_HASHES \ ((MAX_DIGESTS_PER_SIGNED_FILE - 1) + \ MAX_CHAINED_FILES * (MAX_DIGESTS_PER_CHAINED_FILE - 1)) struct qdl_device_sim { struct qdl_device base; struct vip_table_generator *vip_gen; bool create_digests; /* Response queue consumed by sim_read() */ struct sim_response *resp_head; struct sim_response *resp_tail; enum sim_state state; size_t raw_remaining; /* bytes of raw data left to transfer */ bool closed; /* set after power command to terminate reads fast */ /* VIP hash validation */ uint8_t vip_hashes[SIM_VIP_MAX_HASHES][SHA256_DIGEST_LENGTH]; size_t vip_hash_count; /* entries populated from received VIP tables */ size_t vip_hash_idx; /* index of the next hash to verify */ /* Chain hash linking each VIP table to the next */ uint8_t vip_chain_hash[SHA256_DIGEST_LENGTH]; bool vip_has_chain_hash; }; static void sim_enqueue(struct qdl_device_sim *qdl_sim, const char *xml) { struct sim_response *resp; resp = malloc(sizeof(*resp)); if (!resp) ux_die(1, "failed to allocate simulator response entry"); resp->data = strdup(xml); if (!resp->data) ux_die(1, "failed to duplicate simulator response string"); resp->len = strlen(xml); resp->next = NULL; if (qdl_sim->resp_tail) qdl_sim->resp_tail->next = resp; else qdl_sim->resp_head = resp; qdl_sim->resp_tail = resp; } static void sim_enqueue_log(struct qdl_device_sim *qdl_sim, const char *handler) { char buf[256]; snprintf(buf, sizeof(buf), "" "", handler); sim_enqueue(qdl_sim, buf); } static unsigned int sim_get_uint_attr(xmlNode *node, const char *name) { xmlChar *val; unsigned int ret; val = xmlGetProp(node, (xmlChar *)name); if (!val) return 0; ret = (unsigned int)strtoul((char *)val, NULL, 10); xmlFree(val); return ret; } /* * Qualcomm MBN header structures (little-endian, packed). * * The signed VIP table (DigestsToSign.bin.mbn) uses one of these layouts. * V5 and V6 share the same image_size/code_size offsets; V7 reorders the * fields. Only V5 and V6 are supported for VIP table parsing today. */ struct mbn_header_v5 { uint32_t image_id; uint32_t header_version; uint32_t qti_signature_size; uint32_t qti_cert_chain_size; uint32_t image_size; uint32_t code_size; uint32_t signature_ptr; uint32_t signature_size; uint32_t cert_chain_ptr; uint32_t cert_chain_size; } __attribute__((packed)); struct mbn_header_v6 { uint32_t image_id; uint32_t header_version; uint32_t qti_signature_size; uint32_t qti_cert_chain_size; uint32_t image_size; uint32_t code_size; uint32_t signature_ptr; uint32_t signature_size; uint32_t cert_chain_ptr; uint32_t cert_chain_size; uint32_t qti_metadata_size; uint32_t metadata_size; } __attribute__((packed)); #define MBN_HEADER_VERSION_5 5 #define MBN_HEADER_VERSION_6 6 /* * sim_vip_signed_payload() - locate the hash payload in a signed VIP MBN table * * DigestsToSign.bin.mbn is a Qualcomm MBN image with a packed header followed * by the hash data, a signature, and certificate chains. The header_version * field identifies the layout; V5 and V6 share the same image_size/code_size * offsets. The payload begins at (file_size - image_size) and spans code_size * bytes. * * Returns a pointer into @buf for the payload, or NULL if not recognised. * Sets @out_len to the payload byte count on success. */ static const uint8_t *sim_vip_signed_payload(const uint8_t *buf, size_t len, size_t *out_len) { struct mbn_header_v6 hdr; size_t offset; if (len < sizeof(struct mbn_header_v5)) return NULL; memcpy(&hdr, buf, sizeof(struct mbn_header_v5)); if (hdr.header_version != MBN_HEADER_VERSION_5 && hdr.header_version != MBN_HEADER_VERSION_6) return NULL; if (hdr.code_size == 0 || hdr.code_size % SHA256_DIGEST_LENGTH != 0 || hdr.code_size > MAX_DIGESTS_PER_SIGNED_FILE * SHA256_DIGEST_LENGTH || hdr.image_size < hdr.code_size || hdr.image_size > len) return NULL; offset = len - hdr.image_size; if (offset < sizeof(struct mbn_header_v5) || offset + hdr.code_size > len) return NULL; *out_len = hdr.code_size; return buf + offset; } /* * sim_vip_recv_table() - parse a received VIP table and store chunk hashes * * @is_signed: true -> DigestsToSign.bin.mbn (MBN-wrapped, signed) * false -> ChainedTableOfDigests.bin (raw binary) * * The last entry of a full signed table and of a non-final chained table is a * chain hash (SHA256 of the following table) rather than a data-chunk hash. * It is excluded from the stored list. * * A chained table is "final" when its raw byte count is not a multiple of * SHA256_DIGEST_LENGTH: vip_gen_finalize() appends a single zero-padding byte * to the final chained table so that it cannot be a USB-512-byte multiple. */ static void sim_vip_recv_table(struct qdl_device_sim *qdl_sim, const void *buf, size_t len, bool is_signed) { const uint8_t *hashes; size_t count, data_count; bool has_chain_hash; if (is_signed) { size_t payload_len; hashes = sim_vip_signed_payload(buf, len, &payload_len); if (!hashes) { ux_err_tag(UX_TAG_SIM, "VIP signed table: unrecognised format " "(%zu bytes)\n", len); return; } count = payload_len / SHA256_DIGEST_LENGTH; /* Full table uses the last slot for the chain hash */ has_chain_hash = (count == MAX_DIGESTS_PER_SIGNED_FILE); /* Save chain hash so the first chained table can be verified */ if (has_chain_hash) { memcpy(qdl_sim->vip_chain_hash, hashes + (count - 1) * SHA256_DIGEST_LENGTH, SHA256_DIGEST_LENGTH); qdl_sim->vip_has_chain_hash = true; } } else { /* Verify this table against the chain hash from the previous table */ if (qdl_sim->vip_has_chain_hash) { uint8_t hash[SHA256_DIGEST_LENGTH]; char got_hex[SHA256_DIGEST_STRING_LENGTH]; char exp_hex[SHA256_DIGEST_STRING_LENGTH]; SHA2_CTX ctx; size_t i; SHA256Init(&ctx); SHA256Update(&ctx, buf, len); SHA256Final(hash, &ctx); if (memcmp(hash, qdl_sim->vip_chain_hash, SHA256_DIGEST_LENGTH) != 0) { for (i = 0; i < SHA256_DIGEST_LENGTH; i++) { sprintf(got_hex + i * 2, "%02x", hash[i]); sprintf(exp_hex + i * 2, "%02x", qdl_sim->vip_chain_hash[i]); } got_hex[SHA256_DIGEST_STRING_LENGTH - 1] = '\0'; exp_hex[SHA256_DIGEST_STRING_LENGTH - 1] = '\0'; ux_err_tag(UX_TAG_SIM, "VIP chained table chain hash mismatch\n" " computed: %s\n" " expected: %s\n", got_hex, exp_hex); } else { ux_debug_tag(UX_TAG_SIM, "VIP chained table chain hash OK\n"); } qdl_sim->vip_has_chain_hash = false; } hashes = buf; if (len % SHA256_DIGEST_LENGTH != 0) { /* * Trailing zero-padding byte -> final chained table. * All floor(len / 32) entries are chunk hashes. */ count = len / SHA256_DIGEST_LENGTH; has_chain_hash = false; } else { /* * Size is a multiple of 32 -> non-final chained table. * The last entry is a chain hash to the next table. */ count = len / SHA256_DIGEST_LENGTH; has_chain_hash = (count == MAX_DIGESTS_PER_CHAINED_FILE); } /* Save chain hash so the next chained table can be verified */ if (has_chain_hash) { memcpy(qdl_sim->vip_chain_hash, hashes + (count - 1) * SHA256_DIGEST_LENGTH, SHA256_DIGEST_LENGTH); qdl_sim->vip_has_chain_hash = true; } } data_count = has_chain_hash ? count - 1 : count; if (qdl_sim->vip_hash_count + data_count > SIM_VIP_MAX_HASHES) { ux_err_tag(UX_TAG_SIM, "VIP table overflow (%zu + %zu > %u), " "truncating\n", qdl_sim->vip_hash_count, data_count, SIM_VIP_MAX_HASHES); data_count = SIM_VIP_MAX_HASHES - qdl_sim->vip_hash_count; } memcpy(qdl_sim->vip_hashes[qdl_sim->vip_hash_count], hashes, data_count * SHA256_DIGEST_LENGTH); qdl_sim->vip_hash_count += data_count; ux_debug_tag(UX_TAG_SIM, "loaded %zu VIP chunk hashes from %s table " "(total: %zu)\n", data_count, is_signed ? "signed" : "chained", qdl_sim->vip_hash_count); } /* * sim_vip_check_chunk() - SHA256 @buf/@len and verify against the next entry * * Called once per VIP chunk (one XML command write or one raw-data write). * A mismatch is reported via ux_err() but does not abort, so that all * mismatches across a full flash run are visible at once. */ static void sim_vip_check_chunk(struct qdl_device_sim *qdl_sim, const void *buf, size_t len) { uint8_t hash[SHA256_DIGEST_LENGTH]; char got_hex[SHA256_DIGEST_STRING_LENGTH]; char exp_hex[SHA256_DIGEST_STRING_LENGTH]; const uint8_t *expected; SHA2_CTX ctx; size_t i; if (qdl_sim->create_digests || !qdl_sim->vip_hash_count) return; SHA256Init(&ctx); SHA256Update(&ctx, buf, len); SHA256Final(hash, &ctx); if (qdl_sim->vip_hash_idx >= qdl_sim->vip_hash_count) { ux_err_tag(UX_TAG_SIM, "VIP chunk %zu has no entry in the digest table " "(table has %zu entries)\n", qdl_sim->vip_hash_idx, qdl_sim->vip_hash_count); qdl_sim->vip_hash_idx++; return; } expected = qdl_sim->vip_hashes[qdl_sim->vip_hash_idx]; if (memcmp(hash, expected, SHA256_DIGEST_LENGTH) == 0) { ux_debug_tag(UX_TAG_SIM, "VIP chunk %zu hash OK\n", qdl_sim->vip_hash_idx); qdl_sim->vip_hash_idx++; return; } for (i = 0; i < SHA256_DIGEST_LENGTH; i++) { sprintf(got_hex + i * 2, "%02x", hash[i]); sprintf(exp_hex + i * 2, "%02x", expected[i]); } got_hex[SHA256_DIGEST_STRING_LENGTH - 1] = '\0'; exp_hex[SHA256_DIGEST_STRING_LENGTH - 1] = '\0'; ux_err_tag(UX_TAG_SIM, "VIP hash mismatch for chunk %zu\n" " computed: %s\n" " expected: %s\n", qdl_sim->vip_hash_idx, got_hex, exp_hex); qdl_sim->vip_hash_idx++; } static int sim_open(struct qdl_device *qdl __unused, const char *serial __unused) { ux_info("This is a dry-run execution of QDL. No actual flashing has been performed\n"); return 0; } static void sim_close(struct qdl_device *qdl __unused) {} /* * sim_read() - serve the next queued XML response or raw-out data * * Returns the number of bytes written to @buf, or a negative errno. * Returns -ETIMEDOUT when the response queue is empty (mirroring the * behaviour of the USB driver when no data arrives within the timeout). * Returns -EIO after a power/reset command to let the caller drain quickly. */ static int sim_read(struct qdl_device *qdl, void *buf, size_t len, unsigned int timeout __unused) { struct qdl_device_sim *qdl_sim = container_of(qdl, struct qdl_device_sim, base); struct sim_response *resp; size_t copy_len; /* * Always drain the response queue first. This matters for read * commands: sim_write() sets SIM_STATE_RAW_OUT and queues the * rawmode=true ACK at the same time, so the pending XML must be * served before we start sending raw sector data. */ resp = qdl_sim->resp_head; if (resp) { copy_len = MIN(len, resp->len); memcpy(buf, resp->data, copy_len); /* * If the caller's buffer could not hold the whole response, * keep the remainder queued for the next read instead of * overflowing @buf or dropping data. */ if (copy_len < resp->len) { memmove(resp->data, resp->data + copy_len, resp->len - copy_len); resp->len -= copy_len; return copy_len; } qdl_sim->resp_head = resp->next; if (!qdl_sim->resp_head) qdl_sim->resp_tail = NULL; free(resp->data); free(resp); return copy_len; } /* * Queue empty and in raw-out mode: the host has received the * rawmode=true ACK and is now reading sector data. Serve zeros * and enqueue the final rawmode=false ACK when all sectors are done. */ if (qdl_sim->state == SIM_STATE_RAW_OUT) { copy_len = MIN(len, qdl_sim->raw_remaining); memset(buf, 0, copy_len); qdl_sim->raw_remaining -= copy_len; if (qdl_sim->raw_remaining == 0) { sim_enqueue(qdl_sim, SIM_ACK); qdl_sim->state = SIM_STATE_XML; } return copy_len; } /* * Queue is empty. Return -EIO after power/reset so that callers * which drain trailing log messages after the final ACK return * immediately instead of spinning until the timeout expires. */ if (qdl_sim->closed) return -EIO; return -ETIMEDOUT; } /* * sim_write() - accept a host write and queue the matching device response(s) * * In SIM_STATE_RAW_IN the simulator counts down the expected raw payload and * queues the final rawmode=false ACK once the last byte arrives. * * In SIM_STATE_XML the XML command is parsed and one or more response messages * are pushed onto the queue for sim_read() to serve back. * * In dry-run mode without digest generation, binary writes that fail XML * parsing are treated as VIP digest tables: sim_vip_recv_table() extracts the * chunk hashes and an ACK is enqueued so that the host's post-send * firehose_read() succeeds. Every subsequent XML command write and every * raw-data write is then verified against the stored hashes by * sim_vip_check_chunk(). */ static int sim_write(struct qdl_device *qdl, const void *buf, size_t len, unsigned int timeout __unused) { struct qdl_device_sim *qdl_sim = container_of(qdl, struct qdl_device_sim, base); unsigned int num_sectors, sector_size; xmlNode *root, *node, *child; xmlDoc *doc; /* * VIP table writes (both the signed MBN and chained raw binaries) may * arrive while SIM_STATE_RAW_IN is active, interleaved between data * chunks of the same program operation. Intercept them here using the * sending_table flag set exclusively by vip_transfer_send_raw(), before * the raw-data accounting branch below. */ if (!qdl_sim->create_digests && qdl_sim->base.vip_data.sending_table) { bool is_signed = (qdl_sim->base.vip_data.state == VIP_INIT); sim_vip_recv_table(qdl_sim, buf, len, is_signed); sim_enqueue(qdl_sim, SIM_ACK); return len; } /* Raw binary payload for an ongoing program operation */ if (qdl_sim->state == SIM_STATE_RAW_IN) { /* * Each qdl_write() call maps to exactly one VIP chunk * (firehose_do_program() loops over max_payload_size chunks, * calling vip_gen_chunk_init/update/store once per iteration). * Hash the entire write and verify against the digest table. */ sim_vip_check_chunk(qdl_sim, buf, len); if (len >= qdl_sim->raw_remaining) { sim_enqueue(qdl_sim, SIM_ACK); qdl_sim->state = SIM_STATE_XML; qdl_sim->raw_remaining = 0; } else { qdl_sim->raw_remaining -= len; } return len; } /* XML command: parse and enqueue appropriate response(s) */ doc = xmlReadMemory(buf, len, NULL, NULL, XML_PARSE_NOWARNING | XML_PARSE_NOERROR); if (!doc) return len; /* * Valid XML command: verify it against the digest table before * dispatching so that the hash index stays in step with the * vip_gen_chunk_store() calls made during the dry-run that * produced the table. */ sim_vip_check_chunk(qdl_sim, buf, len); root = xmlDocGetRootElement(doc); /* Locate the element */ for (node = root; node; node = node->next) { if (node->type == XML_ELEMENT_NODE && xmlStrcmp(node->name, (xmlChar *)"data") == 0) break; } if (!node) goto out; /* First child element is the command */ for (child = node->children; child && child->type != XML_ELEMENT_NODE; child = child->next) ; if (!child) goto out; if (xmlStrcmp(child->name, (xmlChar *)"configure") == 0) { sim_enqueue_log(qdl_sim, "configure"); sim_enqueue(qdl_sim, SIM_CONFIGURE_ACK); } else if (xmlStrcmp(child->name, (xmlChar *)"program") == 0) { num_sectors = sim_get_uint_attr(child, "num_partition_sectors"); sector_size = sim_get_uint_attr(child, "SECTOR_SIZE_IN_BYTES"); sim_enqueue_log(qdl_sim, "program"); sim_enqueue(qdl_sim, SIM_ACK_RAWMODE); qdl_sim->state = SIM_STATE_RAW_IN; qdl_sim->raw_remaining = (size_t)num_sectors * sector_size; } else if (xmlStrcmp(child->name, (xmlChar *)"read") == 0) { num_sectors = sim_get_uint_attr(child, "num_partition_sectors"); sector_size = sim_get_uint_attr(child, "SECTOR_SIZE_IN_BYTES"); sim_enqueue_log(qdl_sim, "read"); sim_enqueue(qdl_sim, SIM_ACK_RAWMODE); qdl_sim->state = SIM_STATE_RAW_OUT; qdl_sim->raw_remaining = (size_t)num_sectors * sector_size; } else if (xmlStrcmp(child->name, (xmlChar *)"erase") == 0) { sim_enqueue_log(qdl_sim, "erase"); sim_enqueue(qdl_sim, SIM_ACK); } else if (xmlStrcmp(child->name, (xmlChar *)"patch") == 0) { sim_enqueue_log(qdl_sim, "patch"); sim_enqueue(qdl_sim, SIM_ACK); } else if (xmlStrcmp(child->name, (xmlChar *)"setbootablestoragedrive") == 0) { sim_enqueue_log(qdl_sim, "setbootablestoragedrive"); sim_enqueue(qdl_sim, SIM_ACK); } else if (xmlStrcmp(child->name, (xmlChar *)"power") == 0) { sim_enqueue_log(qdl_sim, "power"); sim_enqueue(qdl_sim, SIM_ACK); /* * Mark the sim closed so that the trailing drain read in * firehose_reset() returns -EIO immediately rather than * spinning until the timeout expires. */ qdl_sim->closed = true; } else if (xmlStrcmp(child->name, (xmlChar *)"ufs") == 0) { sim_enqueue_log(qdl_sim, "ufs"); sim_enqueue(qdl_sim, SIM_ACK); } else { /* Unknown command: respond with a generic ACK */ sim_enqueue(qdl_sim, SIM_ACK); } out: xmlFreeDoc(doc); return len; } static void sim_set_out_chunk_size(struct qdl_device *qdl __unused, long size __unused) {} struct qdl_device *sim_init(void) { struct qdl_device *qdl = malloc(sizeof(struct qdl_device_sim)); if (!qdl) return NULL; memset(qdl, 0, sizeof(struct qdl_device_sim)); qdl->dev_type = QDL_DEVICE_SIM; qdl->open = sim_open; qdl->read = sim_read; qdl->write = sim_write; qdl->close = sim_close; qdl->set_out_chunk_size = sim_set_out_chunk_size; qdl->max_payload_size = 1048576; /* * Pre-set a non-zero sector size so that firehose_try_configure() * skips the sector-size probe reads. Those probe commands would * otherwise be hashed into the VIP digest table during dry-run * digest generation (--createdigest), but they are never sent * during the real VIP flash -- causing a hash mismatch. */ qdl->sector_size = 4096; return qdl; } struct vip_table_generator *sim_get_vip_generator(struct qdl_device *qdl) { struct qdl_device_sim *qdl_sim; if (qdl->dev_type != QDL_DEVICE_SIM) return NULL; qdl_sim = container_of(qdl, struct qdl_device_sim, base); if (!qdl_sim->create_digests) return NULL; return qdl_sim->vip_gen; } bool sim_set_digest_generation(bool create_digests, struct qdl_device *qdl, struct vip_table_generator *vip_gen) { struct qdl_device_sim *qdl_sim; if (qdl->dev_type != QDL_DEVICE_SIM) return false; qdl_sim = container_of(qdl, struct qdl_device_sim, base); qdl_sim->create_digests = create_digests; qdl_sim->vip_gen = vip_gen; return true; } linux-msm-qdl-6e625aa/src/sim.h000066400000000000000000000006471527206113100163710ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) 2025, Qualcomm Innovation Center, Inc. All rights reserved. */ #ifndef __SIM_H__ #define __SIM_H__ #include "qdl.h" #include "vip.h" struct vip_table_generator *sim_get_vip_generator(struct qdl_device *qdl); bool sim_set_digest_generation(bool create_digests, struct qdl_device *qdl, struct vip_table_generator *vip_gen); #endif /* __SIM_H__ */ linux-msm-qdl-6e625aa/src/sparse.c000066400000000000000000000070241527206113100170650ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2025, Maksim Paimushkin * All rights reserved. */ #ifdef _WIN32 #include #else #include #endif #include #include #include #include #include #include #include #include #include "file.h" #include "sparse.h" #include "qdl.h" int sparse_header_parse(struct qdl_file *file, sparse_header_t *sparse_header) { qdl_file_seek(file, 0, SEEK_SET); if (qdl_file_read(file, sparse_header, sizeof(sparse_header_t)) != sizeof(sparse_header_t)) { ux_err_tag(UX_TAG_SPARSE, "Unable to read sparse header\n"); return -EINVAL; } if (sparse_header->magic != SPARSE_HEADER_MAGIC) { ux_err_tag(UX_TAG_SPARSE, "Invalid magic in sparse header\n"); return -EINVAL; } if (sparse_header->major_version != SPARSE_HEADER_MAJOR_VER) { ux_err_tag(UX_TAG_SPARSE, "Invalid major version in sparse header\n"); return -EINVAL; } if (sparse_header->minor_version != SPARSE_HEADER_MINOR_VER) { ux_err_tag(UX_TAG_SPARSE, "Invalid minor version in sparse header\n"); return -EINVAL; } if (sparse_header->file_hdr_sz > sizeof(sparse_header_t)) qdl_file_seek(file, sparse_header->file_hdr_sz - sizeof(sparse_header_t), SEEK_CUR); return 0; } int sparse_chunk_header_parse(struct qdl_file *file, sparse_header_t *sparse_header, uint64_t *chunk_size, uint32_t *value, off_t *offset) { chunk_header_t chunk_header; uint32_t fill_value = 0; unsigned int type; *chunk_size = 0; *value = 0; if (qdl_file_read(file, &chunk_header, sizeof(chunk_header_t)) != sizeof(chunk_header_t)) { ux_err_tag(UX_TAG_SPARSE, "Unable to read sparse chunk header\n"); return -EINVAL; } if (sparse_header->chunk_hdr_sz > sizeof(chunk_header_t)) { if (qdl_file_seek(file, sparse_header->chunk_hdr_sz - sizeof(chunk_header_t), SEEK_CUR) < 0) { ux_err_tag(UX_TAG_SPARSE, "Unable to seek past extended chunk header\n"); return -EINVAL; } } type = chunk_header.chunk_type; *chunk_size = (uint64_t)chunk_header.chunk_sz * sparse_header->blk_sz; switch (type) { case CHUNK_TYPE_RAW: if (chunk_header.total_sz != (sparse_header->chunk_hdr_sz + *chunk_size)) { ux_err_tag(UX_TAG_SPARSE, "Bogus chunk size, type Raw\n"); return -EINVAL; } /* Save the current file offset in the 'value' variable */ *offset = qdl_file_seek(file, 0, SEEK_CUR); if (*offset < 0) { ux_err_tag(UX_TAG_SPARSE, "Unable to determine chunk offset\n"); return -EINVAL; } /* Move the file cursor forward by the size of the chunk */ if (qdl_file_seek(file, *chunk_size, SEEK_CUR) < 0) { ux_err_tag(UX_TAG_SPARSE, "Unable to seek past raw chunk data\n"); return -EINVAL; } break; case CHUNK_TYPE_DONT_CARE: if (chunk_header.total_sz != sparse_header->chunk_hdr_sz) { ux_err_tag(UX_TAG_SPARSE, "Bogus chunk size, type Don't Care\n"); return -EINVAL; } break; case CHUNK_TYPE_FILL: if (chunk_header.total_sz != (sparse_header->chunk_hdr_sz + sizeof(fill_value))) { ux_err_tag(UX_TAG_SPARSE, "Bogus chunk size, type Fill\n"); return -EINVAL; } if (qdl_file_read(file, &fill_value, sizeof(fill_value)) != sizeof(fill_value)) { ux_err_tag(UX_TAG_SPARSE, "Unable to read fill value\n"); return -EINVAL; } /* Save the current fill value in the 'value' variable */ *value = fill_value; break; default: ux_err_tag(UX_TAG_SPARSE, "Unknown chunk type: %#x\n", type); return -EINVAL; } return type; } linux-msm-qdl-6e625aa/src/sparse.h000066400000000000000000000041661527206113100170760ustar00rootroot00000000000000/* SPDX-License-Identifier: Apache-2.0 */ /* * Copyright (C) 2010 The Android Open Source Project */ #ifndef __SPARSE_H__ #define __SPARSE_H__ #include #include struct qdl_file; typedef struct __attribute__((__packed__)) sparse_header { /* 0xed26ff3a */ uint32_t magic; /* (0x1) - reject images with higher major versions */ uint16_t major_version; /* (0x0) - allow images with higer minor versions */ uint16_t minor_version; /* 28 bytes for first revision of the file format */ uint16_t file_hdr_sz; /* 12 bytes for first revision of the file format */ uint16_t chunk_hdr_sz; /* block size in bytes, must be a multiple of 4 (4096) */ uint32_t blk_sz; /* total blocks in the non-sparse output image */ uint32_t total_blks; /* total chunks in the sparse input image */ uint32_t total_chunks; /* * CRC32 checksum of the original data, counting "don't care" * as 0. Standard 802.3 polynomial, use a Public Domain * table implementation */ uint32_t image_checksum; } sparse_header_t; #define SPARSE_HEADER_MAGIC 0xed26ff3a #define SPARSE_HEADER_MAJOR_VER 0x0001 #define SPARSE_HEADER_MINOR_VER 0x0000 typedef struct __attribute__((__packed__)) chunk_header { uint16_t chunk_type; /* 0xCAC1 -> raw; 0xCAC2 -> fill; 0xCAC3 -> don't care */ uint16_t reserved1; uint32_t chunk_sz; /* in blocks in output image */ uint32_t total_sz; /* in bytes of chunk input file including chunk header and data */ } chunk_header_t; #define CHUNK_TYPE_RAW 0xCAC1 #define CHUNK_TYPE_FILL 0xCAC2 #define CHUNK_TYPE_DONT_CARE 0xCAC3 /* * Parses the sparse image header from the file descriptor. * Returns 0 on success, or an error code otherwise. */ int sparse_header_parse(struct qdl_file *file, sparse_header_t *sparse_header); /* * Parses the sparse image chunk header from the file descriptor. * Sets the chunk size, and value or offset based on the parsed data. * Returns the chunk type on success, or an error code otherwise. */ int sparse_chunk_header_parse(struct qdl_file *file, sparse_header_t *sparse_header, uint64_t *chunk_size, uint32_t *value, off_t *offset); #endif linux-msm-qdl-6e625aa/src/ufs.c000066400000000000000000000203201527206113100163570ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2018, The Linux Foundation. All rights reserved. */ #include #include #include #include #include #include #include #include #include "ufs.h" #include "qdl.h" #include "list.h" #include "patch.h" static const char notice_bconfigdescrlock[] = "\n" "Please pay attention that UFS provisioning is irreversible (OTP) operation unless parameter bConfigDescrLock = 0.\n" "In order to prevent unintentional device locking the tool has the following safety:\n\n" " if you REALLY intend to perform OTP, please ensure that your XML includes property\n" " bConfigDescrLock = 1 AND provide command line parameter --finalize-provisioning.\n\n" " Unless you intend to lock your device, please set bConfigDescrLock = 0 in your XML\n" " and don't use command line parameter --finalize-provisioning.\n\n" "In case of mismatch between CL and XML provisioning is not performed.\n\n"; void ufs_provisioning_init(struct ufs_provisioning *ufs) { ufs->common = NULL; ufs->epilogue = NULL; list_init(&ufs->bodies); } void ufs_provisioning_cleanup(struct ufs_provisioning *ufs) { struct ufs_body *body; struct ufs_body *tmp; free(ufs->common); ufs->common = NULL; list_for_each_entry_safe(body, tmp, &ufs->bodies, node) { list_del(&body->node); free((void *)body->desc); free(body); } free(ufs->epilogue); ufs->epilogue = NULL; } bool ufs_need_provisioning(const struct ufs_provisioning *ufs) { return !!ufs->epilogue; } static struct ufs_common *ufs_parse_common_params(xmlNode *node, bool finalize_provisioning __unused) { struct ufs_common *result; int errors; result = calloc(1, sizeof(struct ufs_common)); errors = 0; result->bNumberLU = attr_as_unsigned(node, "bNumberLU", &errors); result->bBootEnable = !!attr_as_unsigned(node, "bBootEnable", &errors); result->bDescrAccessEn = !!attr_as_unsigned(node, "bDescrAccessEn", &errors); result->bInitPowerMode = attr_as_unsigned(node, "bInitPowerMode", &errors); result->bHighPriorityLUN = attr_as_unsigned(node, "bHighPriorityLUN", &errors); result->bSecureRemovalType = attr_as_unsigned(node, "bSecureRemovalType", &errors); result->bInitActiveICCLevel = attr_as_unsigned(node, "bInitActiveICCLevel", &errors); result->wPeriodicRTCUpdate = attr_as_unsigned(node, "wPeriodicRTCUpdate", &errors); result->bConfigDescrLock = !!attr_as_unsigned(node, "bConfigDescrLock", &errors); if (errors) { ux_err("errors while parsing UFS common tag\n"); free(result); return NULL; } /* These parameters are optional */ errors = 0; result->bWriteBoosterBufferPreserveUserSpaceEn = !!attr_as_unsigned(node, "bWriteBoosterBufferPreserveUserSpaceEn", &errors); result->bWriteBoosterBufferType = !!attr_as_unsigned(node, "bWriteBoosterBufferType", &errors); result->shared_wb_buffer_size_in_kb = attr_as_unsigned(node, "shared_wb_buffer_size_in_kb", &errors); result->wb = !errors; return result; } static struct ufs_body *ufs_parse_body(xmlNode *node) { struct ufs_body *result; int errors; result = calloc(1, sizeof(struct ufs_body)); errors = 0; result->LUNum = attr_as_unsigned(node, "LUNum", &errors); result->bLUEnable = !!attr_as_unsigned(node, "bLUEnable", &errors); result->bBootLunID = attr_as_unsigned(node, "bBootLunID", &errors); result->size_in_kb = attr_as_unsigned(node, "size_in_kb", &errors); result->bDataReliability = attr_as_unsigned(node, "bDataReliability", &errors); result->bLUWriteProtect = attr_as_unsigned(node, "bLUWriteProtect", &errors); result->bMemoryType = attr_as_unsigned(node, "bMemoryType", &errors); result->bLogicalBlockSize = attr_as_unsigned(node, "bLogicalBlockSize", &errors); result->bProvisioningType = attr_as_unsigned(node, "bProvisioningType", &errors); result->wContextCapabilities = attr_as_unsigned(node, "wContextCapabilities", &errors); result->desc = attr_as_string(node, "desc", &errors); if (errors) { ux_err("errors while parsing UFS body tag\n"); free(result); return NULL; } return result; } static struct ufs_epilogue *ufs_parse_epilogue(xmlNode *node) { struct ufs_epilogue *result; int errors = 0; result = calloc(1, sizeof(struct ufs_epilogue)); result->LUNtoGrow = attr_as_unsigned(node, "LUNtoGrow", &errors); if (errors) { ux_err("errors while parsing UFS epilogue tag\n"); free(result); return NULL; } return result; } int ufs_load(struct ufs_provisioning *ufs, const char *ufs_file, bool finalize_provisioning) { xmlNode *node; xmlNode *root; xmlDoc *doc; int retval = 0; struct ufs_body *ufs_body_tmp; if (ufs->common) { ux_err("Only one UFS provisioning XML allowed, \"%s\" ignored\n", ufs_file); return -EEXIST; } doc = xmlReadFile(ufs_file, NULL, 0); if (!doc) { ux_err("failed to parse ufs-type file \"%s\"\n", ufs_file); return -EINVAL; } root = xmlDocGetRootElement(doc); for (node = root->children; node ; node = node->next) { if (node->type != XML_ELEMENT_NODE) continue; if (xmlStrcmp(node->name, (xmlChar *)"ufs")) { ux_err("unrecognized tag \"%s\" in ufs-type file \"%s\", ignoring\n", ufs_file, node->name); continue; } if (xmlGetProp(node, (xmlChar *)"bNumberLU")) { if (!ufs->common) { ufs->common = ufs_parse_common_params(node, finalize_provisioning); } else { ux_err("multiple UFS common tags found in \"%s\"\n", ufs_file); retval = -EINVAL; break; } if (!ufs->common) { ux_err("invalid UFS common tag found in \"%s\"\n", ufs_file); retval = -EINVAL; break; } } else if (xmlGetProp(node, (xmlChar *)"LUNum")) { ufs_body_tmp = ufs_parse_body(node); if (ufs_body_tmp) { list_append(&ufs->bodies, &ufs_body_tmp->node); } else { ux_err("invalid UFS body tag found in \"%s\"\n", ufs_file); retval = -EINVAL; break; } } else if (xmlGetProp(node, (xmlChar *)"commit")) { if (!ufs->epilogue) { ufs->epilogue = ufs_parse_epilogue(node); if (ufs->epilogue) continue; } else { ux_err("multiple UFS finalizing tags found in \"%s\"\n", ufs_file); retval = -EINVAL; break; } if (!ufs->epilogue) { ux_err("invalid UFS finalizing tag found in \"%s\"\n", ufs_file); retval = -EINVAL; break; } } else { ux_err("unknown tag found in ufs-type file \"%s\"\n", ufs_file); retval = -EINVAL; break; } } xmlFreeDoc(doc); if (!retval && (!ufs->common || list_empty(&ufs->bodies) || !ufs->epilogue)) { ux_err("incomplete UFS provisioning information in \"%s\"\n", ufs_file); retval = -EINVAL; } if (!retval && !finalize_provisioning != !ufs->common->bConfigDescrLock) { ux_err("UFS provisioning value bConfigDescrLock %d in file \"%s\" don't match command line parameter --finalize-provisioning %d\n", ufs->common->bConfigDescrLock, ufs_file, finalize_provisioning); ux_err(notice_bconfigdescrlock); retval = -EINVAL; } if (retval) ufs_provisioning_cleanup(ufs); return retval; } int ufs_provisioning_execute(struct ufs_provisioning *ufs, struct qdl_device *qdl, int (*apply_ufs_common)(struct qdl_device *, struct ufs_common*), int (*apply_ufs_body)(struct qdl_device *, struct ufs_body*), int (*apply_ufs_epilogue)(struct qdl_device *, struct ufs_epilogue*, bool)) { int ret; struct ufs_body *body; if (ufs->common->bConfigDescrLock) { int i; ux_warnx("irreversible provisioning will start in 5s"); for (i = 5; i > 0; i--) { ux_info(".\a"); fflush(stdout); sleep(1); } ux_info("\n"); } // Just ask a target to check the XML w/o real provisioning ret = apply_ufs_common(qdl, ufs->common); if (ret) return ret; list_for_each_entry(body, &ufs->bodies, node) { ret = apply_ufs_body(qdl, body); if (ret) return ret; } ret = apply_ufs_epilogue(qdl, ufs->epilogue, false); if (ret) { ux_err("UFS provisioning impossible, provisioning XML may be corrupted\n"); return ret; } // Real provisioning -- target didn't refuse a given XML ret = apply_ufs_common(qdl, ufs->common); if (ret) return ret; list_for_each_entry(body, &ufs->bodies, node) { ret = apply_ufs_body(qdl, body); if (ret) return ret; } return apply_ufs_epilogue(qdl, ufs->epilogue, true); } linux-msm-qdl-6e625aa/src/ufs.h000066400000000000000000000041261527206113100163720ustar00rootroot00000000000000/* SPDX-License-Identifier: BSD-3-Clause */ /* * Copyright (c) 2018, The Linux Foundation. All rights reserved. */ #ifndef __UFS_H__ #define __UFS_H__ #include #include "list.h" struct qdl_device; struct ufs_common { unsigned int bNumberLU; bool bBootEnable; bool bDescrAccessEn; unsigned int bInitPowerMode; unsigned int bHighPriorityLUN; unsigned int bSecureRemovalType; unsigned int bInitActiveICCLevel; unsigned int wPeriodicRTCUpdate; bool bConfigDescrLock; bool wb; bool bWriteBoosterBufferPreserveUserSpaceEn; bool bWriteBoosterBufferType; unsigned int shared_wb_buffer_size_in_kb; }; struct ufs_body { unsigned int LUNum; bool bLUEnable; unsigned int bBootLunID; unsigned int size_in_kb; unsigned int bDataReliability; unsigned int bLUWriteProtect; unsigned int bMemoryType; unsigned int bLogicalBlockSize; unsigned int bProvisioningType; unsigned int wContextCapabilities; const char *desc; struct list_head node; }; struct ufs_epilogue { unsigned int LUNtoGrow; bool commit; }; /* * Parsed UFS provisioning description. Owned by the caller (instead of the * former module globals) so it can be loaded, inspected and freed without * shared state - which also makes the loader independently testable. * Initialise with ufs_provisioning_init() and release with * ufs_provisioning_cleanup(). */ struct ufs_provisioning { struct ufs_common *common; struct ufs_epilogue *epilogue; struct list_head bodies; }; void ufs_provisioning_init(struct ufs_provisioning *ufs); void ufs_provisioning_cleanup(struct ufs_provisioning *ufs); int ufs_load(struct ufs_provisioning *ufs, const char *ufs_file, bool finalize_provisioning); int ufs_provisioning_execute(struct ufs_provisioning *ufs, struct qdl_device *qdl, int (*apply_ufs_common)(struct qdl_device *qdl, struct ufs_common *ufs), int (*apply_ufs_body)(struct qdl_device *qdl, struct ufs_body *ufs), int (*apply_ufs_epilogue)(struct qdl_device *qdl, struct ufs_epilogue *ufs, bool commit)); bool ufs_need_provisioning(const struct ufs_provisioning *ufs); #endif linux-msm-qdl-6e625aa/src/usb.c000066400000000000000000000412751527206113100163670ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* * libusb transport backend. * * Open-path layering, top to bottom: * * auto_open() - meta-backend (auto.c): picks the transport by * alternating usb_open_once() with (on Windows) * QUD probes until one reaches an EDL device * usb_open() - this backend's .open op: wait loop retrying * usb_open_once() every 250 ms * usb_open_once() - one enumeration pass over the bus: selects and * counts the visible EDL devices, tries to open * each * usb_open_device() - opens one already-selected candidate, claiming * its EDL interface * * Candidate selection itself (enumeration plus the EDL identity * policy) lives in usb_enumerate_edl_devices(), shared between the * open path above and usb_list(), which reads descriptors without * claiming anything. */ #include #include #include #include #include #include #include #include #include #include #include "oscompat.h" #include "qdl.h" #define DEFAULT_OUT_CHUNK_SIZE (1024 * 1024) struct qdl_device_usb { struct qdl_device base; struct libusb_device_handle *usb_handle; int in_ep; int out_ep; size_t in_maxpktsize; size_t out_maxpktsize; size_t out_chunk_size; bool expect_zlp; }; /* libusb-typed wrapper around the shared EDL identity check */ static bool usb_is_edl_device(const struct libusb_device_descriptor *desc) { return qdl_is_edl_device(desc->idVendor, desc->idProduct); } typedef int (*usb_edl_device_cb_t)(struct libusb_device *dev, const struct libusb_device_descriptor *desc, void *data); /* * Enumerate the bus and hand every EDL candidate to @cb, stopping * early if @cb returns nonzero. The single place the EDL identity * policy is applied to libusb devices; shared by the open and list * paths, which differ only in what they do with a candidate. The * caller owns the libusb session. * * Returns the number of candidates seen up to and including the one * @cb stopped at, or -EIO if the bus could not be enumerated. */ static int usb_enumerate_edl_devices(usb_edl_device_cb_t cb, void *data) { struct libusb_device_descriptor desc; struct libusb_device **devs; int visible = 0; ssize_t n; int i; n = libusb_get_device_list(NULL, &devs); if (n < 0) { ux_err("failed to list USB devices: %s\n", libusb_strerror(n)); return -EIO; } for (i = 0; devs[i]; i++) { if (libusb_get_device_descriptor(devs[i], &desc) < 0) continue; if (!usb_is_edl_device(&desc)) continue; visible++; if (cb(devs[i], &desc, data)) break; } libusb_free_device_list(devs, 1); return visible; } /* * libusb commit f0cce43f882d ("core: Fix definition and use of enum * libusb_transfer_type") split transfer type and endpoint transfer types. * Provide an alias in order to make the code compile with the old (non-split) * definition. */ #ifndef LIBUSB_ENDPOINT_TRANSFER_TYPE_BULK #define LIBUSB_ENDPOINT_TRANSFER_TYPE_BULK LIBUSB_TRANSFER_TYPE_BULK #endif #define USB_STRING_ATTEMPTS 3 #define USB_STRING_RETRY_US 20000 static bool usb_string_error_transient(int ret) { return ret == LIBUSB_ERROR_IO || ret == LIBUSB_ERROR_PIPE || ret == LIBUSB_ERROR_TIMEOUT; } static int usb_read_string(struct libusb_device_handle *handle, uint8_t index, char *buf, size_t len) { int attempt; int ret; if (!index) return LIBUSB_ERROR_NOT_FOUND; for (attempt = 1;; attempt++) { ret = libusb_get_string_descriptor_ascii(handle, index, (unsigned char *)buf, len); if (ret >= 0 || attempt == USB_STRING_ATTEMPTS || !usb_string_error_transient(ret)) return ret; usleep(USB_STRING_RETRY_US); } } /* * EDL devices carry the serial in iProduct as "_SN:", other * modes report it in iSerialNumber. */ static bool usb_read_serial(struct libusb_device_handle *handle, const struct libusb_device_descriptor *desc, char *out, size_t out_len) { char buf[128]; char *p; int ret; ret = usb_read_string(handle, desc->iProduct, buf, sizeof(buf)); if (ret >= 0) { p = strstr(buf, "_SN:"); if (p) { p += strlen("_SN:"); p[strcspn(p, " _")] = '\0'; snprintf(out, out_len, "%s", p); return true; } } else if (ret != LIBUSB_ERROR_NOT_FOUND) { ux_warnx("failed to read iProduct descriptor: %s", libusb_strerror(ret)); } ret = usb_read_string(handle, desc->iSerialNumber, out, out_len); if (ret < 0) { if (ret != LIBUSB_ERROR_NOT_FOUND) ux_warnx("failed to read iSerialNumber descriptor: %s", libusb_strerror(ret)); return false; } return ret > 0; } static bool usb_match_serial(struct libusb_device_handle *handle, const char *serial, const struct libusb_device_descriptor *desc) { char buf[64]; /* If no serial is requested, consider everything a match */ if (!serial) return true; if (!usb_read_serial(handle, desc, buf, sizeof(buf))) return false; return strcasecmp(buf, serial) == 0; } /* * The EDL interface of a matched device: the bulk endpoint pair the * Sahara/Firehose protocols run over. */ struct usb_edl_interface { int in_ep; int out_ep; size_t in_maxpktsize; size_t out_maxpktsize; }; /* * Check whether one interface descriptor carries the EDL service and, * if so, extract its bulk endpoint pair into @edl. */ static bool usb_match_edl_interface(const struct libusb_interface_descriptor *ifc, struct usb_edl_interface *edl) { const struct libusb_endpoint_descriptor *endpoint; uint8_t type; int l; if (ifc->bInterfaceClass != 0xff) return false; if (ifc->bInterfaceSubClass != 0xff) return false; /* * Sahara is identified by bInterfaceProtocol 0x10, 0x11, or 0x13 * on modern devices, and 0xff on older targets. */ if (ifc->bInterfaceProtocol != 0xff && ifc->bInterfaceProtocol != 0x10 && ifc->bInterfaceProtocol != 0x11 && ifc->bInterfaceProtocol != 0x13) return false; edl->in_ep = -1; edl->out_ep = -1; edl->in_maxpktsize = 0; edl->out_maxpktsize = 0; for (l = 0; l < ifc->bNumEndpoints; l++) { endpoint = &ifc->endpoint[l]; type = endpoint->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK; if (type != LIBUSB_ENDPOINT_TRANSFER_TYPE_BULK) continue; if (endpoint->bEndpointAddress & LIBUSB_ENDPOINT_IN) { edl->in_ep = endpoint->bEndpointAddress; edl->in_maxpktsize = endpoint->wMaxPacketSize; } else { edl->out_ep = endpoint->bEndpointAddress; edl->out_maxpktsize = endpoint->wMaxPacketSize; } } /* * A matching interface is only usable if it exposes both a bulk * IN and a bulk OUT endpoint with a non-zero max packet size. * Without this check a malformed descriptor leads to a division * by zero (out_maxpktsize == 0) or transfers on endpoint -1. */ if (edl->in_ep < 0 || edl->out_ep < 0 || !edl->in_maxpktsize || !edl->out_maxpktsize) return false; return true; } /* * Device-level match, run on an opened EDL candidate: the unit must * carry the serial the user asked for (if any) and expose an EDL * interface. On a match, returns the number of the interface to * claim and its bulk endpoint pair. */ static bool usb_match_device(struct libusb_device_handle *handle, const struct libusb_device_descriptor *desc, const char *serial, int *ifc_num, struct usb_edl_interface *edl) { const struct libusb_interface_descriptor *ifc; struct libusb_config_descriptor *config; bool found = false; int ret; int k; if (!usb_match_serial(handle, serial, desc)) return false; ret = libusb_get_active_config_descriptor(libusb_get_device(handle), &config); if (ret < 0) { ux_warnx("failed to acquire USB device's active config descriptor"); return false; } for (k = 0; k < config->bNumInterfaces && !found; k++) { ifc = config->interface[k].altsetting; if (usb_match_edl_interface(ifc, edl)) { *ifc_num = ifc->bInterfaceNumber; found = true; } } libusb_free_config_descriptor(config); return found; } static int usb_open_device(libusb_device *dev, const struct libusb_device_descriptor *desc, struct qdl_device_usb *qdl, const char *serial) { struct libusb_device_handle *handle; struct usb_edl_interface edl; int ifc_num; int ret; ret = libusb_open(dev, &handle); if (ret < 0) { ux_warnx("unable to open USB device"); return 0; } if (!usb_match_device(handle, desc, serial, &ifc_num, &edl)) goto close; libusb_detach_kernel_driver(handle, ifc_num); ret = libusb_claim_interface(handle, ifc_num); if (ret < 0) { ux_warnx("failed to claim USB interface"); goto close; } qdl->usb_handle = handle; qdl->in_ep = edl.in_ep; qdl->out_ep = edl.out_ep; qdl->in_maxpktsize = edl.in_maxpktsize; qdl->out_maxpktsize = edl.out_maxpktsize; qdl->expect_zlp = false; if (qdl->out_chunk_size && qdl->out_chunk_size % edl.out_maxpktsize) { ux_warnx("requested out-chunk-size must be multiple of the device's wMaxPacketSize %ld, using %ld", edl.out_maxpktsize, edl.out_maxpktsize); qdl->out_chunk_size = edl.out_maxpktsize; } else if (!qdl->out_chunk_size) { qdl->out_chunk_size = DEFAULT_OUT_CHUNK_SIZE; } ux_debug_tag(UX_TAG_USB, "using out-chunk-size of %ld\n", qdl->out_chunk_size); return 1; close: libusb_close(handle); return 0; } /* * usb_open_once() - one libusb scan-and-open pass. * * Used as the single iteration unit by both usb_open() (the --backend usb * wait loop) and auto_open() (the unified Windows libusb+QUD wait loop). * * On success, populates @qdl and emits the "Flashing/Collecting device" * UX line; the caller need not log anything. * * Returns: * 0 - device opened, @qdl is ready * -ENODEV - no EDL device currently visible * -EBUSY - EDL device(s) visible but none could be opened * (typically: another driver, usually the Qualcomm * QDLoader 9008 driver behind the QUD backend, has * claimed the USB interface) * -EIO - libusb itself failed (init/get_device_list) * * @visible_out, when non-NULL, receives the number of EDL devices that * libusb saw on this pass, so callers that loop can print transition * diagnostics without re-enumerating. * * Does its own libusb_init()/libusb_exit() so the next call sees a * fresh enumeration (libusb's udev-less backends otherwise cache the * list, which would mask newly-attached devices in containerised setups). */ struct usb_open_ctx { struct qdl_device_usb *qdl; const char *serial; char matched_serial[64]; uint16_t matched_pid; bool found; }; static int usb_open_cb(libusb_device *dev, const struct libusb_device_descriptor *desc, void *data) { struct usb_open_ctx *ctx = data; if (usb_open_device(dev, desc, ctx->qdl, ctx->serial) != 1) return 0; ctx->found = true; ctx->matched_pid = desc->idProduct; if (!usb_read_serial(ctx->qdl->usb_handle, desc, ctx->matched_serial, sizeof(ctx->matched_serial))) ctx->matched_serial[0] = '\0'; return 1; } int usb_open_once(struct qdl_device *qdl, const char *serial, int *visible_out) { struct qdl_device_usb *qdl_usb = container_of(qdl, struct qdl_device_usb, base); struct usb_open_ctx ctx = { .qdl = qdl_usb, .serial = serial }; int visible; int ret; if (visible_out) *visible_out = 0; ret = libusb_init(NULL); if (ret < 0) { ux_err("failed to initialize libusb: %s\n", libusb_strerror(ret)); return -EIO; } visible = usb_enumerate_edl_devices(usb_open_cb, &ctx); if (visible < 0) { libusb_exit(NULL); return -EIO; } if (visible_out) *visible_out = visible; if (ctx.found) { const char *action = ctx.matched_pid == 0x900e ? "Collecting crash dump from" : "Talking to"; if (ctx.matched_serial[0]) ux_info("%s device (PID 0x%04x, serial: %s)\n", action, ctx.matched_pid, ctx.matched_serial); else ux_info("%s device (PID 0x%04x)\n", action, ctx.matched_pid); return 0; } libusb_exit(NULL); return visible == 0 ? -ENODEV : -EBUSY; } static int usb_open(struct qdl_device *qdl, const char *serial) { int visible_prev = -1; int visible; int ret; for (;;) { ret = usb_open_once(qdl, serial, &visible); if (ret == 0) return 0; if (ret == -EIO) return -1; if (visible != visible_prev) { if (visible == 0) { ux_info("Waiting for EDL device\n"); } else if (serial) { ux_info("%d EDL device(s) visible, none match serial \"%s\"\n", visible, serial); } else { ux_info("%d EDL device(s) visible, none could be opened\n", visible); } visible_prev = visible; } usleep(250000); } } struct usb_list_ctx { struct qdl_device_desc *result; unsigned int capacity; unsigned int count; }; static int usb_list_cb(libusb_device *dev, const struct libusb_device_descriptor *desc, void *data) { struct libusb_device_handle *handle; struct usb_list_ctx *ctx = data; struct qdl_device_desc *entry; if (ctx->count == ctx->capacity) { unsigned int capacity = ctx->capacity ? ctx->capacity * 2 : 8; entry = realloc(ctx->result, capacity * sizeof(*entry)); if (!entry) { ux_err("failed to allocate devices array\n"); return 1; } ctx->result = entry; ctx->capacity = capacity; } entry = &ctx->result[ctx->count]; if (libusb_open(dev, &handle) < 0) return 0; if (!usb_read_serial(handle, desc, entry->serial, sizeof(entry->serial))) memcpy(entry->serial, "(none)", sizeof("(none)")); entry->vid = desc->idVendor; entry->pid = desc->idProduct; libusb_close(handle); ctx->count++; return 0; } struct qdl_device_desc *usb_list(unsigned int *devices_found) { struct usb_list_ctx ctx = { 0 }; int ret; ret = libusb_init(NULL); if (ret < 0) { ux_err("failed to initialize libusb: %s\n", libusb_strerror(ret)); return NULL; } ret = usb_enumerate_edl_devices(usb_list_cb, &ctx); libusb_exit(NULL); if (ret < 0) { free(ctx.result); return NULL; } *devices_found = ctx.count; return ctx.result; } static void usb_close(struct qdl_device *qdl) { struct qdl_device_usb *qdl_usb = container_of(qdl, struct qdl_device_usb, base); libusb_close(qdl_usb->usb_handle); libusb_exit(NULL); } static int usb_read(struct qdl_device *qdl, void *buf, size_t len, unsigned int timeout) { struct qdl_device_usb *qdl_usb = container_of(qdl, struct qdl_device_usb, base); int actual; int ret; /* * On Ubuntu 24.04, an ASMedia ASM2142/ASM3142 xHCI controller completed * a zero-length host request while the Firehose exchange remained stalled. * Instead, remember when a transfer may be followed by a ZLP and consume it * with the caller's next real IN request. If the device omitted the ZLP, * return the received data directly without a speculative wait. */ for (;;) { ret = libusb_bulk_transfer(qdl_usb->usb_handle, qdl_usb->in_ep, buf, len, &actual, timeout); if (ret != 0 && ret != LIBUSB_ERROR_TIMEOUT) { qdl_usb->expect_zlp = false; return ret == LIBUSB_ERROR_NO_DEVICE ? -ENODEV : -EIO; } if (ret == LIBUSB_ERROR_TIMEOUT && actual == 0) return -ETIMEDOUT; if (!qdl_usb->expect_zlp || actual != 0) break; qdl_usb->expect_zlp = false; } qdl_usb->expect_zlp = actual > 0 && len == (size_t)actual && !(actual % qdl_usb->in_maxpktsize); return actual; } static int usb_write(struct qdl_device *qdl, const void *buf, size_t len, unsigned int timeout) { unsigned char *data = (unsigned char *)buf; struct qdl_device_usb *qdl_usb = container_of(qdl, struct qdl_device_usb, base); unsigned int count = 0; size_t len_orig = len; int actual; int xfer; int ret; while (len > 0) { xfer = (len > qdl_usb->out_chunk_size) ? qdl_usb->out_chunk_size : len; ret = libusb_bulk_transfer(qdl_usb->usb_handle, qdl_usb->out_ep, data, xfer, &actual, timeout); if (ret != 0 && ret != LIBUSB_ERROR_TIMEOUT) { ux_warnx("bulk write failed: %s", libusb_strerror(ret)); return -EIO; } if (ret == LIBUSB_ERROR_TIMEOUT && actual == 0) return -ETIMEDOUT; count += actual; len -= actual; data += actual; } if (len_orig % qdl_usb->out_maxpktsize == 0) { ret = libusb_bulk_transfer(qdl_usb->usb_handle, qdl_usb->out_ep, NULL, 0, &actual, timeout); if (ret < 0) return -EIO; } return count; } static void usb_set_out_chunk_size(struct qdl_device *qdl, long size) { struct qdl_device_usb *qdl_usb = container_of(qdl, struct qdl_device_usb, base); qdl_usb->out_chunk_size = size; } struct qdl_device *usb_init(void) { struct qdl_device *qdl = malloc(sizeof(struct qdl_device_usb)); if (!qdl) return NULL; memset(qdl, 0, sizeof(struct qdl_device_usb)); qdl->dev_type = QDL_DEVICE_USB; qdl->open = usb_open; qdl->read = usb_read; qdl->write = usb_write; qdl->close = usb_close; qdl->set_out_chunk_size = usb_set_out_chunk_size; qdl->max_payload_size = 1048576; return qdl; } linux-msm-qdl-6e625aa/src/util.c000066400000000000000000000174121527206113100165470ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) 2016, Bjorn Andersson * All rights reserved. */ #include #include #include #include #include #include #include #include #include #include #include #include "file.h" #include "oscompat.h" #include "qdl.h" static uint8_t to_hex(uint8_t ch) { ch &= 0xf; return ch <= 9 ? '0' + ch : 'a' + ch - 10; } void print_hex_dump(const char *prefix, const void *buf, size_t len) { const uint8_t *ptr = buf; size_t linelen; uint8_t ch; char line[16 * 3 + 16 + 1]; int li; unsigned int i; unsigned int j; for (i = 0; i < len; i += 16) { linelen = MIN(16u, (size_t)(len - i)); li = 0; for (j = 0; j < linelen; j++) { ch = ptr[i + j]; line[li++] = to_hex(ch >> 4); line[li++] = to_hex(ch); line[li++] = ' '; } for (; j < 16; j++) { line[li++] = ' '; line[li++] = ' '; line[li++] = ' '; } for (j = 0; j < linelen; j++) { ch = ptr[i + j]; line[li++] = isprint(ch) ? ch : '.'; } line[li] = '\0'; ux_info("%s %04x: %s\n", prefix, i, line); } } unsigned int attr_as_unsigned(xmlNode *node, const char *attr, int *errors) { unsigned int ret; xmlChar *value; value = xmlGetProp(node, (xmlChar *)attr); if (!value) { (*errors)++; return 0; } ret = (unsigned int)strtoul((char *)value, NULL, 0); xmlFree(value); return ret; } const char *attr_as_string(xmlNode *node, const char *attr, int *errors) { xmlChar *value; char *ret = NULL; value = xmlGetProp(node, (xmlChar *)attr); if (!value) { (*errors)++; return NULL; } if (value[0] != '\0') ret = strdup((char *)value); xmlFree(value); return ret; } bool attr_as_bool(xmlNode *node, const char *attr, int *errors) { xmlChar *value; bool ret = false; if (!xmlHasProp(node, (xmlChar *)attr)) return false; value = xmlGetProp(node, (xmlChar *)attr); if (!value) { (*errors)++; return false; } ret = (xmlStrcmp(value, (xmlChar *)"true") == 0); xmlFree(value); return ret; } static const char * const storage_types[] = { [QDL_STORAGE_EMMC] = "emmc", [QDL_STORAGE_NAND] = "nand", [QDL_STORAGE_NVME] = "nvme", [QDL_STORAGE_SPINOR] = "spinor", [QDL_STORAGE_UFS] = "ufs", }; const char *encode_storage_type(enum qdl_storage_type storage) { if ((unsigned int)storage >= ARRAY_SIZE(storage_types)) return NULL; return storage_types[storage]; } enum qdl_storage_type decode_storage_type(const char *storage) { unsigned int i; if (!storage) return QDL_STORAGE_UNKNOWN; for (i = 0; i < ARRAY_SIZE(storage_types); i++) if (storage_types[i] && !strcmp(storage, storage_types[i])) return i; return QDL_STORAGE_UNKNOWN; } /*** * parse_storage_address() - parse a storage address specifier * @address: specifier to be parsed * @physical_partition: physical partition * @start_sector: start_sector * @num_sectors: number of sectors * @gpt_partition: GPT name * * This function parses the provided address specifier and detects the * following patterns: * * N => physical partition N, sector 0 * N/S => physical partition N, sector S * N/S+L => physical partition N, L sectors at sector S * name => GPT partition name match across all physical partitions * N/name => GPT partition name match within physical partition N * * @physical_partition is either the requested physical partition, or -1 if * none is specified. Either @start_sector and @num_sectors, or @gpt_partition * will represent the requested address, the other(s) will be zeroed. * * Returns: 0 on success, -1 on failure */ int parse_storage_address(const char *address, int *physical_partition, unsigned int *start_sector, unsigned int *num_sectors, char **gpt_partition) { unsigned long length = 0; const char *ptr = address; unsigned long sector = 0; long partition; char *end; char *gpt = NULL; errno = 0; partition = strtol(ptr, &end, 10); if (end == ptr) { partition = -1; gpt = strdup(ptr); goto done; } if ((errno == ERANGE && partition == LONG_MAX) || partition < 0) return -1; if (end[0] == '\0') goto done; if (end[0] != '/') return -1; ptr = end + 1; errno = 0; sector = strtoul(ptr, &end, 10); if (end == ptr) { gpt = strdup(ptr); goto done; } if (errno == ERANGE && sector == ULONG_MAX) return -1; if (end[0] == '\0') goto done; if (end[0] != '+') return -1; ptr = end + 1; errno = 0; length = strtoul(ptr, &end, 10); if (end == ptr) return -1; if (errno == ERANGE && length == ULONG_MAX) return -1; if (length == 0) return -1; if (end[0] != '\0') return -1; done: *physical_partition = partition; *start_sector = sector; *num_sectors = length; *gpt_partition = gpt; return 0; } /* * Parse a --backend= value into an enum. "auto" maps to the meta-backend * QDL_DEVICE_AUTO, which inside its open path runs a unified wait loop * over libusb and (on Windows) the QUD SetupAPI enumeration, binding * whichever first reaches an EDL device. Explicit "usb"/"qud" pin to a * single concrete transport and skip the meta layer entirely. * * QDL_DEVICE_SIM is intentionally not selectable via --backend; --dry-run / * --create-digests pick it implicitly. */ int decode_backend(const char *name, enum QDL_DEVICE_TYPE *out) { if (!name || !strcmp(name, "auto")) { *out = QDL_DEVICE_AUTO; return 0; } if (!strcmp(name, "usb")) { *out = QDL_DEVICE_USB; return 0; } if (!strcmp(name, "qud")) { *out = QDL_DEVICE_QUD; return 0; } return -1; } /* * Decode the value of --color. A bare --color (@name is NULL) means * "always", matching the convention of git, grep and ls. */ int decode_color_mode(const char *name, enum ux_color_mode *out) { if (!name || !strcmp(name, "always")) { *out = UX_COLOR_ALWAYS; return 0; } if (!strcmp(name, "auto")) { *out = UX_COLOR_AUTO; return 0; } if (!strcmp(name, "never")) { *out = UX_COLOR_NEVER; return 0; } return -1; } /* * Split a "" or "::" argument into its filename and * optional selector. Returns a newly allocated filename (which the caller * frees) with @specifier pointing into it (or NULL when absent), or NULL on * a malformed argument (empty, or more than one "::"). */ char *qdl_split_specifier(const char *param, char **specifier) { char *filename; char *tmp; if (!param || !param[0]) return NULL; filename = strdup(param); if (!filename) { ux_err("internal error: unable to allocate memory for argument\n"); return NULL; } *specifier = NULL; tmp = strstr(filename, "::"); if (tmp) { if (strstr(tmp + 2, "::")) { free(filename); return NULL; } *tmp = '\0'; if (!filename[0] || !tmp[2]) { free(filename); return NULL; } *specifier = tmp + 2; } return filename; } /** * load_sahara_image() - Load the content of the given file into the image * @filename: file to be loaded * @image: Sahara image object to be populated * * Read the content of the given @filename into the given @image, update the * @image->len, and then populate the @image->name for debugging purposes. * * Returns: 0 on success, -1 on error */ int load_sahara_image(struct qdl_zip *zip, const char *filename, struct sahara_image *image) { struct qdl_file file; size_t len; void *ptr; int ret; ret = qdl_file_open(zip, filename, &file); if (ret < 0) { ux_err("failed to read \"%s\"\n", filename); return -1; } ptr = qdl_file_load(&file, &len); if (!ptr || len == 0) goto err_close; image->name = strdup(filename); image->ptr = ptr; image->len = len; qdl_file_close(&file); return 0; err_close: qdl_file_close(&file); return -1; } void sahara_images_free(struct sahara_image *images, size_t count) { for (size_t i = 0; i < count; i++) { free(images[i].name); free(images[i].ptr); images[i] = (struct sahara_image){}; } } linux-msm-qdl-6e625aa/src/ux.c000066400000000000000000000460361527206113100162320ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #include #include #include #include #include #include #ifdef _WIN32 #include #include #else #include #endif #include #include #include #include #include "qdl.h" /* libxml2 2.12 const-qualified the xmlError pointer in this callback. */ #if LIBXML_VERSION >= 21200 typedef const xmlError * ux_xml_error_ptr; #else typedef xmlErrorPtr ux_xml_error_ptr; #endif #define UX_PROGRESS_REFRESH_RATE 10 #define UX_PROGRESS_SIZE_MAX 80 #define HASHES "################################################################################" #define DASHES "--------------------------------------------------------------------------------" static const char * const progress_hashes = HASHES; static const char * const progress_dashes = DASHES; static unsigned int ux_width; static unsigned int ux_cur_line_length; /* Name used to prefix diagnostics, set by each executable via ux_set_progname() */ static const char *ux_progname_str = "qdl"; /* Whether the next character written to each stream starts a new line */ static bool ux_stdout_bol = true; static bool ux_stderr_bol = true; /* Color policy from --color, and the per-stream decision (-1: undecided) */ static enum ux_color_mode ux_color = UX_COLOR_AUTO; static int ux_color_stdout = -1; static int ux_color_stderr = -1; /* --quiet: only results, warnings and errors are printed */ static bool ux_quiet; static const char * const ux_styles[] = { [UX_STYLE_NONE] = "", [UX_STYLE_RESET] = "\033[0m", [UX_STYLE_HEADING] = "\033[1m", [UX_STYLE_OPTION] = "\033[1;36m", [UX_STYLE_ERROR] = "\033[31m", [UX_STYLE_WARNING] = "\033[33m", [UX_STYLE_LOG] = "\033[36m", [UX_STYLE_TX] = "\033[32m", [UX_STYLE_RX] = "\033[36m", [UX_STYLE_DIM] = "\033[2m", [UX_STYLE_PROGRESS] = "\033[32m", [UX_STYLE_SUCCESS] = "\033[32m", [UX_STYLE_SKIPPED] = "\033[34m", }; /* Name and color of each subsystem tag */ static const struct { const char *name; const char *sgr; } ux_tags[] = { [UX_TAG_NONE] = { NULL, "" }, [UX_TAG_SAHARA] = { "SAHARA", "\033[1;35m" }, [UX_TAG_FIREHOSE] = { "FIREHOSE", "\033[1;34m" }, [UX_TAG_FIREHOSE_RAW] = { "FIREHOSE_RAW", "\033[34m" }, [UX_TAG_VIP] = { "VIP", "\033[1;32m" }, [UX_TAG_USB] = { "USB", "\033[1;36m" }, [UX_TAG_SPARSE] = { "SPARSE", "\033[1m" }, [UX_TAG_FLASHMAP] = { "FLASHMAP", "\033[1m" }, [UX_TAG_CONTENTS] = { "CONTENTS", "\033[1m" }, [UX_TAG_SIM] = { "SIM", "\033[2m" }, [UX_TAG_QUD] = { "QUD", "\033[1;36m" }, [UX_TAG_MINIDUMP] = { "MINIDUMP", "\033[35m" }, }; /* Severity of a message, printed as "NAME: " at the start of its line */ enum ux_level { UX_LEVEL_NONE, UX_LEVEL_ERR, UX_LEVEL_WARN, UX_LEVEL_INFO, UX_LEVEL_DEBUG, UX_LEVEL_LOG, }; static const struct { const char *name; const char *sgr; } ux_levels[] = { [UX_LEVEL_NONE] = { NULL, "" }, [UX_LEVEL_ERR] = { "ERR", "\033[1;31m" }, [UX_LEVEL_WARN] = { "WARN", "\033[1;33m" }, [UX_LEVEL_INFO] = { "INFO", "\033[1m" }, [UX_LEVEL_DEBUG] = { "DEBUG", "\033[2m" }, [UX_LEVEL_LOG] = { "LOG", "\033[36m" }, }; /* * Levels of output: * * error: used to signal errors to the user * info: used to inform the user about progress * logs: log prints from the device * debug: protocol logs * * Without --debug, messages are written verbatim. With --debug every level * is written in a uniform format, where each message starts with a * timestamp: * * 14:03:21.123 flashed "boot" successfully * * Continuation lines of a multi-line message are indented to the column * where the first line's text starts, so the message body stays aligned. * A message that does not end in a newline is continued by the next call * on the same stream without a new prefix. */ static bool ux_color_enabled(FILE *fp); /* Write the "

--step # # The steps drive the built qdl binary against a real device in EDL mode, so # they are not meant for unattended CI. Run them explicitly with: # # QDL_HIL_BUILD=/path/to/build QDL_HIL_STORAGE=ufs \ # meson test -C build --suite hil # # When the required environment is not set a step exits 77, which meson # reports as SKIP, so a plain "meson test" stays green without hardware. # # Required environment: # QDL_HIL_BUILD Build to flash. Auto-detected: a directory uses classic # flashing (prog_firehose_*.elf + rawprogram*.xml + # patch*.xml); a file (.json/.xml/.zip) uses "qdl flash". # QDL_HIL_STORAGE Target storage type: emmc|nand|nvme|spinor|ufs. # # Optional environment: # QDL_HIL_PROGRAMMER Firehose programmer .elf (needed by the read/write/ # erase/sha256/reset steps). Defaults to the first # prog_firehose_*.elf next to QDL_HIL_BUILD. # QDL_HIL_PARTITION Partition for the read/erase/write steps. The EFI # system partition is small and quick to read and write, # so it defaults to "efi". # QDL_HIL_IMAGE Image the erase/write steps restore. Defaults to the # data the read step reads back, so erase/write # round-trip the partition's own content. # QDL_HIL_SERIAL -S serial, to select one of several attached devices. # QDL_HIL_ENTER_EDL_CMD Command run before the suite's first step to put # the board into EDL mode (e.g. "adb reboot edl" or a # USB relay toggle), for unattended/CI runs. When # unset the board must already be in EDL mode. # QDL_HIL_SETTLE Seconds the list step waits for the device to # enumerate after the enter-EDL command (default 30). # QDL_HIL_DEBUG When set, run every qdl invocation with --debug so # failures leave a full protocol log in the meson # testlog (and the uploaded CI artifacts). # QDL_HIL_CONTENTS contents.xml (optionally with ::specifier) for the # create-zip step; when unset that step is skipped. # QDL_HIL_SPARSE flashmap/contents/zip that flashes sparse images, for # the sparse-flash step; when unset that step is skipped. # # Every step runs with --skip-reset to keep the programmer alive across # invocations; only the final step ends the session with the reset verb. set -e SKIP=77 while [[ $# -gt 0 ]]; do case "$1" in --builddir) builddir="$2"; shift 2 ;; --step) step="$2"; shift 2 ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac done [[ -n "${builddir}" ]] || { echo "Error: --builddir is required." >&2; exit 1; } [[ -n "${step}" ]] || { echo "Error: --step is required." >&2; exit 1; } uname_out="$(uname -s)" case "${uname_out}" in Linux*|Darwin*) QDL=qdl ;; CYGWIN*|MINGW*|MSYS*) QDL=qdl.exe ;; *) exit 1 ;; esac QEXE="${builddir}/${QDL}" shopt -s nullglob # --- fail-fast across the (serialized) suite ------------------------------ # meson runs each step as its own process, so persist a sentinel: the first # step starts a fresh run, any real failure raises it, and every later step # bails out instead of touching the device once it is set. A skipped step # (exit 77) does not count as a failure. ABORT_FILE="${builddir}/meson-logs/hil.aborted" # Keep all HIL scratch (partition readbacks, temporary reads) on the build # disk, never in /tmp - which is commonly a small tmpfs that a full-partition # readback would exhaust. HIL_TMPDIR="${builddir}/hil-tmp" mkdir -p "${HIL_TMPDIR}" # Readback of the target partition captured by the read step and written back # after the erase step, so erase/write restore the exact original bytes. SAVED_IMAGE="${HIL_TMPDIR}/readback.img" on_exit() { local rc=$? if [[ ${rc} -ne 0 && ${rc} -ne ${SKIP} ]]; then mkdir -p "$(dirname "${ABORT_FILE}")" 2>/dev/null || true : > "${ABORT_FILE}" fi } trap on_exit EXIT # The suite's first step (highest meson priority). Suite-level setup - # resetting the fail-fast sentinel, entering EDL mode - must anchor to it: # doing either in a later step breaks the steps running before it. Keep # this in sync with the priorities in tests/meson.build. FIRST_STEP="list" if [[ "${step}" == "${FIRST_STEP}" ]]; then rm -f "${ABORT_FILE}" rm -rf "${HIL_TMPDIR:?}"/* fi # --- resolve configuration from the environment --------------------------- BUILD="${QDL_HIL_BUILD}" STORAGE="${QDL_HIL_STORAGE}" PARTITION="${QDL_HIL_PARTITION:-efi}" SERIAL="${QDL_HIL_SERIAL}" PROGRAMMER="${QDL_HIL_PROGRAMMER}" IMAGE="${QDL_HIL_IMAGE}" if [[ -z "${BUILD}" || -z "${STORAGE}" ]]; then echo "set QDL_HIL_BUILD and QDL_HIL_STORAGE to run the HIL suite" >&2 exit ${SKIP} fi # Fail-fast: if an earlier step already failed, stop the whole suite. No # step is exempt - the first step cleared the sentinel above before this # check, so a set sentinel always means a failure in this run. if [[ -e "${ABORT_FILE}" ]]; then echo "an earlier HIL step failed; stopping the suite" >&2 exit ${SKIP} fi # For unattended runs an environment-provided command puts the board into # EDL mode ahead of the suite's first step. The list step polls for the # device afterwards (see QDL_HIL_SETTLE), so the command only needs to # trigger the mode switch, not wait for enumeration. if [[ "${step}" == "${FIRST_STEP}" && -n "${QDL_HIL_ENTER_EDL_CMD}" ]]; then bash -c "${QDL_HIL_ENTER_EDL_CMD}" || { echo "QDL_HIL_ENTER_EDL_CMD failed" >&2 exit 1 } fi IS_DIR=false if [[ -d "${BUILD}" ]]; then IS_DIR=true BUILDDIR="${BUILD}" else BUILDDIR="$(dirname "${BUILD}")" fi if [[ -z "${PROGRAMMER}" ]]; then progs=( "${BUILDDIR}"/prog_firehose_*.elf ) [[ ${#progs[@]} -gt 0 ]] && PROGRAMMER="${progs[0]}" fi # Image the erase/write steps restore: an explicit QDL_HIL_IMAGE if given, # otherwise the readback captured by the read step. IMAGE="${IMAGE:-${SAVED_IMAGE}}" # Optional debug logging on every qdl invocation, for post-mortems. QDLDBG=() [[ -n "${QDL_HIL_DEBUG}" ]] && QDLDBG=( -d ) # Common qdl prefix. -R suppresses the reset everywhere but the final step. COMMON=( "${QDLDBG[@]}" -s "${STORAGE}" ) [[ -n "${SERIAL}" ]] && COMMON+=( -S "${SERIAL}" ) qdl_noreset() { "${QEXE}" "${COMMON[@]}" -R "$@"; } qdl_reset() { "${QEXE}" "${COMMON[@]}" "$@"; } need_programmer() { [[ -n "${PROGRAMMER}" ]] || { echo "no prog_firehose_*.elf found (set QDL_HIL_PROGRAMMER)" >&2 exit ${SKIP} } } # Print the hex sha256 of a file, using whichever tool is available. sha256_file() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' else echo "no sha256 tool (sha256sum/shasum) available" >&2 exit ${SKIP} fi } # --- step implementations ------------------------------------------------- # a) Flash the whole build. t_flash_full() { if ${IS_DIR}; then need_programmer local raws=( "${BUILDDIR}"/rawprogram*.xml ) local patches=( "${BUILDDIR}"/patch*.xml ) [[ ${#raws[@]} -gt 0 ]] || { echo "no rawprogram*.xml in ${BUILDDIR}" >&2; exit ${SKIP}; } qdl_noreset "${PROGRAMMER}" "${raws[@]}" "${patches[@]}" else qdl_noreset flash "${BUILD}" fi } # b) Read a partition by name (EFI by default). The readback is kept so the # write step can restore it after erase. t_read_partition() { need_programmer qdl_noreset "${PROGRAMMER}" read "${PARTITION}" "${SAVED_IMAGE}" [[ -s "${SAVED_IMAGE}" ]] || { echo "read produced an empty file" >&2; return 1; } } # c) Read only the GPT and confirm it really is one. t_read_gpt() { need_programmer local out; out="$(mktemp -p "${HIL_TMPDIR}")" trap 'rm -f "${out}"' RETURN qdl_noreset "${PROGRAMMER}" read 0/0+34 "${out}" grep -qa "EFI PART" "${out}" || { echo "no GPT signature in readback" >&2; return 1; } } # c') Other supported read address forms. t_read_addr_sectors() { # P/S+L need_programmer local out; out="$(mktemp -p "${HIL_TMPDIR}")" trap 'rm -f "${out}"' RETURN qdl_noreset "${PROGRAMMER}" read 0/0+1 "${out}" [[ -s "${out}" ]] || { echo "empty readback" >&2; return 1; } } t_read_addr_partname() { # P/name need_programmer local out; out="$(mktemp -p "${HIL_TMPDIR}")" trap 'rm -f "${out}"' RETURN qdl_noreset "${PROGRAMMER}" read "0/${PARTITION}" "${out}" [[ -s "${out}" ]] || { echo "empty readback" >&2; return 1; } } # d) Re-flash the build with --skipblock=sha256 (should mostly skip). t_flash_skipblock() { if ${IS_DIR}; then need_programmer local raws=( "${BUILDDIR}"/rawprogram*.xml ) local patches=( "${BUILDDIR}"/patch*.xml ) [[ ${#raws[@]} -gt 0 ]] || { echo "no rawprogram*.xml" >&2; exit ${SKIP}; } qdl_noreset --skipblock=sha256 "${PROGRAMMER}" "${raws[@]}" "${patches[@]}" else qdl_noreset --skipblock=sha256 flash "${BUILD}" fi } # f) sha256 digest command. t_sha256() { need_programmer local out; out="$(mktemp -p "${HIL_TMPDIR}")" trap 'rm -f "${out}"' RETURN qdl_noreset "${PROGRAMMER}" sha256 0/0+34 >"${out}" 2>&1 grep -qiE '[0-9a-f]{64}' "${out}" || { echo "no sha256 digest in output:" >&2; cat "${out}" >&2; return 1; } } # g) Erase the partition. Only erase when there is an image to restore it with # (the readback from the read step, or an explicit QDL_HIL_IMAGE), so the suite # never leaves the partition wiped. t_erase() { need_programmer [[ -f "${IMAGE}" ]] || { echo "no image to restore (run the read step or set QDL_HIL_IMAGE)" >&2; exit ${SKIP}; } qdl_noreset "${PROGRAMMER}" erase "${PARTITION}" } # f') Write the image back to its partition (the readback by default). t_write_image() { need_programmer [[ -f "${IMAGE}" ]] || { echo "no image to write (run the read step or set QDL_HIL_IMAGE)" >&2; exit ${SKIP}; } qdl_noreset "${PROGRAMMER}" write "${PARTITION}" "${IMAGE}" } # Final step: reset the board with the explicit reset verb, ending the # Firehose session that every other step kept alive with --skip-reset. t_reset() { need_programmer qdl_reset "${PROGRAMMER}" reset rm -f "${SAVED_IMAGE}" # done; drop the readback backup } # h) Enumerate EDL devices; the attached device must appear. The enter-EDL # hook may have just power-cycled or rebooted the board, and enumeration # after that takes several seconds - the flash steps wait on their own, # but qdl list is a one-shot, so poll until the device shows up or # QDL_HIL_SETTLE expires. t_list() { local out deadline=$(( SECONDS + ${QDL_HIL_SETTLE:-30} )) out="$(mktemp -p "${HIL_TMPDIR}")" trap 'rm -f "${out}"' RETURN while :; do "${QEXE}" "${QDLDBG[@]}" list >"${out}" 2>&1 || { echo "qdl list failed" >&2; cat "${out}" >&2; return 1; } if ! grep -qi "No devices found" "${out}" && grep -qiE '^[0-9a-f]{4}:[0-9a-f]{4}' "${out}" && { [[ -z "${SERIAL}" ]] || grep -qi "${SERIAL}" "${out}"; }; then return 0 fi if (( SECONDS >= deadline )); then echo "no EDL device${SERIAL:+ with serial ${SERIAL}} appeared within ${QDL_HIL_SETTLE:-30}s:" >&2 cat "${out}" >&2 return 1 fi sleep 1 done } # i) Package a contents.xml into a zip with create-zip, then flash the zip # (the programmer comes from inside the zip). t_create_zip() { [[ -n "${QDL_HIL_CONTENTS}" ]] || { echo "set QDL_HIL_CONTENTS to test create-zip" >&2 exit ${SKIP} } local zip="${HIL_TMPDIR}/hil-created.zip" trap 'rm -f "${zip}"' RETURN "${QEXE}" create-zip "${zip}" "${QDL_HIL_CONTENTS}" || { echo "create-zip failed" >&2; return 1; } [[ -s "${zip}" ]] || { echo "create-zip produced no output" >&2; return 1; } qdl_noreset flash "${zip}" } # j) Flash a build that uses sparse (Android sparse format) images. t_sparse_flash() { [[ -n "${QDL_HIL_SPARSE}" && -f "${QDL_HIL_SPARSE}" ]] || { echo "set QDL_HIL_SPARSE to a flashmap/contents/zip using sparse images" >&2 exit ${SKIP} } qdl_noreset flash "${QDL_HIL_SPARSE}" } # k) Error handling: a read of a nonexistent physical partition must be NAKed # by the device and reported as a failure, not silently succeed. t_nak_read() { need_programmer local out; out="$(mktemp -p "${HIL_TMPDIR}")" trap 'rm -f "${out}"' RETURN if qdl_noreset "${PROGRAMMER}" read 99/0+1 "${out}" >/dev/null 2>&1; then echo "reading nonexistent physical partition 99 unexpectedly succeeded" >&2 return 1 fi } # l) sha256 digest of a partition addressed by GPT name. t_sha256_name() { need_programmer local out; out="$(mktemp -p "${HIL_TMPDIR}")" trap 'rm -f "${out}"' RETURN qdl_noreset "${PROGRAMMER}" sha256 "${PARTITION}" >"${out}" 2>&1 grep -qiE '[0-9a-f]{64}' "${out}" || { echo "no sha256 digest for ${PARTITION}:" >&2; cat "${out}" >&2; return 1; } } # m) Re-write the image with an overridden transfer chunk size. t_write_chunked() { need_programmer [[ -f "${IMAGE}" ]] || { echo "no image to write (run the read step or set QDL_HIL_IMAGE)" >&2 exit ${SKIP} } "${QEXE}" "${COMMON[@]}" -R --out-chunk-size=16384 \ "${PROGRAMMER}" write "${PARTITION}" "${IMAGE}" } # n) Integrity check: the device's digest of the partition must match a local # sha256 of the bytes we read back (and just wrote), proving the erase/write # round-trip preserved the content. t_verify_write() { need_programmer [[ -f "${SAVED_IMAGE}" ]] || { echo "no readback to verify against (the read step did not run)" >&2 exit ${SKIP} } local out; out="$(mktemp -p "${HIL_TMPDIR}")" trap 'rm -f "${out}"' RETURN qdl_noreset "${PROGRAMMER}" sha256 "${PARTITION}" >"${out}" 2>&1 || { echo "sha256 command failed" >&2; cat "${out}" >&2; return 1; } local dev_digest local_digest dev_digest="$(grep -oiE '[0-9a-f]{64}' "${out}" | head -1)" [[ -n "${dev_digest}" ]] || { echo "no device digest:" >&2; cat "${out}" >&2; return 1; } local_digest="$(sha256_file "${SAVED_IMAGE}")" [[ "${dev_digest,,}" == "${local_digest,,}" ]] || { echo "partition digest mismatch: device=${dev_digest} local=${local_digest}" >&2 return 1 } } # o) Chip identity through Sahara command mode. Must run before the # programmer upload: chipinfo is only available while the device still # speaks Sahara. The verb switches the device back to image-transfer mode # on exit, so the following flash step still gets its Sahara HELLO. t_chipinfo() { local out; out="$(mktemp -p "${HIL_TMPDIR}")" trap 'rm -f "${out}"' RETURN local args=() [[ -n "${SERIAL}" ]] && args+=( -S "${SERIAL}" ) # The serial must follow the verb: qdl's verb dispatcher stops # scanning at the first argument without a leading dash, so the # value of a preceding "-S " derails it into the flash # parser. The chipinfo subcommand parses -S itself. if ! "${QEXE}" "${QDLDBG[@]}" chipinfo "${args[@]}" >"${out}" 2>&1; then if grep -qi "already in Firehose mode" "${out}"; then echo "programmer already running; chipinfo needs a fresh Sahara device" >&2 exit ${SKIP} fi echo "chipinfo failed:" >&2 cat "${out}" >&2 return 1 fi grep -qiE 'chip serial number|hw id' "${out}" || { echo "chipinfo reported no chip identity:" >&2; cat "${out}" >&2; return 1; } } # --- dispatch ------------------------------------------------------------- case "${step}" in list) t_list ;; chipinfo) t_chipinfo ;; flash_full) t_flash_full ;; create_zip) t_create_zip ;; sparse_flash) t_sparse_flash ;; read_partition) t_read_partition ;; read_gpt) t_read_gpt ;; read_addr_sectors) t_read_addr_sectors ;; read_addr_partname) t_read_addr_partname ;; nak_read) t_nak_read ;; flash_skipblock) t_flash_skipblock ;; sha256) t_sha256 ;; sha256_name) t_sha256_name ;; erase) t_erase ;; write_image) t_write_image ;; write_chunked) t_write_chunked ;; verify_write) t_verify_write ;; reset) t_reset ;; *) echo "unknown step: ${step}" >&2; exit 99 ;; esac linux-msm-qdl-6e625aa/tests/test_hil_vip.sh000066400000000000000000000130771527206113100210310ustar00rootroot00000000000000#!/bin/bash # SPDX-License-Identifier: BSD-3-Clause # # VIP-mode HIL step: sign the firehose programmer, generate and sign the # VIP digest tables, then flash the build in VIP mode on a secured board. # # Registered as the final step of the "hil-vip" meson suite; the list and # chipinfo steps of that suite are reused from test_hil.sh, so the usual # QDL_HIL_BUILD/QDL_HIL_STORAGE (+ QDL_HIL_SERIAL/QDL_HIL_ENTER_EDL_CMD # for the rig) apply. On top of those, this step requires: # # QDL_HIL_SECTOOLS sectools v2 binary # QDL_HIL_SECPROFILE *_security_profile.xml for the target # QDL_HIL_SECKEYS OEM keys directory from genkeys.sh # (qpsa_rootca0.cer, qpsa_attestca0.cer/.key) # # When any of them is missing the step exits 77 (meson SKIP), so suites # stay green on rigs without signing material. # # Only the programmer and the digest-table mbn are signed: VIP validates # the flashed data stream against the signed tables, so the individual # images need no signatures for this test. The signing recipe mirrors # signimages.sh from the qcom-sec-tools repository, with the image ids # taken from its mapping (DEVICE-PROGRAMMER, VIP) and key index 0. set -e SKIP=77 while [[ $# -gt 0 ]]; do case "$1" in --builddir) builddir="$2"; shift 2 ;; --step) step="$2"; shift 2 ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac done [[ -n "${builddir}" ]] || { echo "Error: --builddir is required." >&2; exit 1; } [[ "${step}" == "sign_flash" ]] || { echo "Unknown step: ${step}" >&2; exit 1; } uname_out="$(uname -s)" case "${uname_out}" in Linux*|Darwin*) QDL=qdl ;; CYGWIN*|MINGW*|MSYS*) QDL=qdl.exe ;; *) exit 1 ;; esac QEXE="${builddir}/${QDL}" shopt -s nullglob # Shared with test_hil.sh: honor the suite-wide fail-fast sentinel and # raise it if this step fails (see the fail-fast block there). ABORT_FILE="${builddir}/meson-logs/hil.aborted" on_exit() { local rc=$? if [[ ${rc} -ne 0 && ${rc} -ne ${SKIP} ]]; then mkdir -p "$(dirname "${ABORT_FILE}")" 2>/dev/null || true : > "${ABORT_FILE}" fi } trap on_exit EXIT BUILD="${QDL_HIL_BUILD}" STORAGE="${QDL_HIL_STORAGE}" SERIAL="${QDL_HIL_SERIAL}" SECTOOLS="${QDL_HIL_SECTOOLS}" SECPROFILE="${QDL_HIL_SECPROFILE}" SECKEYS="${QDL_HIL_SECKEYS}" if [[ -z "${BUILD}" || -z "${STORAGE}" ]]; then echo "set QDL_HIL_BUILD and QDL_HIL_STORAGE to run the HIL suites" >&2 exit ${SKIP} fi if [[ -z "${SECTOOLS}" || -z "${SECPROFILE}" || -z "${SECKEYS}" ]]; then echo "set QDL_HIL_SECTOOLS, QDL_HIL_SECPROFILE and QDL_HIL_SECKEYS" \ "to run the hil-vip suite" >&2 exit ${SKIP} fi if [[ -e "${ABORT_FILE}" ]]; then echo "an earlier HIL step failed; stopping the suite" >&2 exit ${SKIP} fi [[ -x "${SECTOOLS}" ]] || { echo "sectools not executable: ${SECTOOLS}" >&2; exit ${SKIP}; } [[ -f "${SECPROFILE}" ]] || { echo "security profile not found: ${SECPROFILE}" >&2; exit ${SKIP}; } # VIP flashing follows the classic rawprogram flow, so a flat build # directory is required (not a flashmap/zip). if [[ ! -d "${BUILD}" ]]; then echo "hil-vip requires a flat build directory in QDL_HIL_BUILD" >&2 exit ${SKIP} fi progs=( "${BUILD}"/prog_firehose_*.elf ) raws=( "${BUILD}"/rawprogram*.xml ) patches=( "${BUILD}"/patch*.xml ) [[ ${#progs[@]} -gt 0 ]] || { echo "no prog_firehose_*.elf in ${BUILD}" >&2; exit ${SKIP}; } [[ ${#raws[@]} -gt 0 ]] || { echo "no rawprogram*.xml in ${BUILD}" >&2; exit ${SKIP}; } QDLDBG=() [[ -n "${QDL_HIL_DEBUG}" ]] && QDLDBG=( -d ) COMMON=( "${QDLDBG[@]}" -s "${STORAGE}" ) [[ -n "${SERIAL}" ]] && COMMON+=( -S "${SERIAL}" ) # Fresh workspace on the build disk; the pristine build artifacts are # never modified - the programmer is signed on a copy. WORK="${builddir}/hil-vip" TABLES="${WORK}/vip-tables" rm -rf "${WORK:?}" mkdir -p "${TABLES}" PROG="${WORK}/$(basename "${progs[0]}")" cp "${progs[0]}" "${PROG}" # Sign one image in place, following the signimages.sh recipe with # signing key index 0. sign_one() { local file="$1" image_id="$2" local root_cert="${SECKEYS}/qpsa_rootca0.cer" local ca_cert="${SECKEYS}/qpsa_attestca0.cer" local ca_key="${SECKEYS}/qpsa_attestca0.key" if [[ ! -f "${root_cert}" || ! -f "${ca_cert}" || ! -f "${ca_key}" ]]; then echo "OEM keys not found under ${SECKEYS} (expected qpsa_rootca0.cer," \ "qpsa_attestca0.cer, qpsa_attestca0.key)" >&2 exit ${SKIP} fi "${SECTOOLS}" secure-image \ --sign "${file}" --image-id="${image_id}" \ --security-profile "${SECPROFILE}" \ --anti-rollback-version=0x0 \ --signing-mode LOCAL \ --root-certificate "${root_cert}" \ --ca-certificate="${ca_cert}" --ca-key="${ca_key}" \ --outfile "${file}" \ || { echo "signing ${file} as ${image_id} failed" >&2; return 1; } } echo "=== signing programmer $(basename "${PROG}")" sign_one "${PROG}" DEVICE-PROGRAMMER echo "=== generating VIP digest tables" "${QEXE}" "${QDLDBG[@]}" --allow-fusing --create-digests "${TABLES}" \ "${PROG}" "${raws[@]}" "${patches[@]}" [[ -f "${TABLES}/DigestsToSign.bin" ]] || { echo "digest generation produced no DigestsToSign.bin" >&2 exit 1 } echo "=== wrapping digest table into mbn" "${SECTOOLS}" mbn-tool generate --data "${TABLES}/DigestsToSign.bin" \ --mbn-version 6 --outfile "${TABLES}/DigestsToSign.bin.mbn" echo "=== signing digest table mbn" sign_one "${TABLES}/DigestsToSign.bin.mbn" VIP echo "=== flashing in VIP mode" "${QEXE}" "${COMMON[@]}" --allow-fusing --vip-table-path "${TABLES}" \ "${PROG}" "${raws[@]}" "${patches[@]}" linux-msm-qdl-6e625aa/tests/test_input_type.c000066400000000000000000000073371527206113100214110ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* * Unit tests for detect_type(), which classifies a positional qdl * argument as a command verb or an input XML file (by inspecting the XML * root element). XML cases are written to a temporary file. */ #include #include #include #include #include #include #include #include #include "input_type.h" #include "common.h" void ux_err(const char *fmt, ...) { (void)fmt; } /* Write @xml to a temp file and return detect_type() of its path. */ static int detect_xml(const char *xml) { char dir[256]; char path[512]; FILE *f; int ret; assert_int_equal(test_make_temp_dir(dir, sizeof(dir), "qdl-type"), 0); snprintf(path, sizeof(path), "%s/input.xml", dir); f = fopen(path, "w"); assert_non_null(f); fputs(xml, f); fclose(f); ret = detect_type(path); unlink(path); rmdir(dir); return ret; } static void test_verbs(void **state) { (void)state; assert_int_equal(detect_type("read"), QDL_CMD_READ); assert_int_equal(detect_type("write"), QDL_CMD_WRITE); assert_int_equal(detect_type("erase"), QDL_CMD_ERASE); assert_int_equal(detect_type("flash"), QDL_CMD_FLASH); assert_int_equal(detect_type("sha256"), QDL_CMD_SHA256); assert_int_equal(detect_type("reset"), QDL_CMD_RESET); } static void test_not_a_verb_or_file(void **state) { (void)state; /* Neither a verb nor an existing file. */ assert_true(detect_type("definitely-not-a-verb-or-file") < 0); } static void test_xml_roots(void **state) { (void)state; assert_int_equal(detect_xml(""), QDL_FILE_PATCH); assert_int_equal(detect_xml(""), QDL_FILE_PROGRAM); assert_int_equal(detect_xml(""), QDL_FILE_READ); assert_int_equal(detect_xml(""), QDL_FILE_UFS); assert_int_equal(detect_xml(""), QDL_FILE_CONTENTS); } /* * qdl_is_contents_xml() gates whether the sahara-archive subcommand * treats its input as a contents document or an id:file mapping list. */ static void test_is_contents_xml(void **state) { char dir[256]; char path[512]; FILE *f; (void)state; assert_int_equal(test_make_temp_dir(dir, sizeof(dir), "qdl-type"), 0); snprintf(path, sizeof(path), "%s/input.xml", dir); f = fopen(path, "w"); assert_non_null(f); fputs("", f); fclose(f); assert_true(qdl_is_contents_xml(path)); f = fopen(path, "w"); assert_non_null(f); fputs("", f); fclose(f); assert_false(qdl_is_contents_xml(path)); unlink(path); assert_false(qdl_is_contents_xml(path)); rmdir(dir); } static void test_xml_unknown_and_bad(void **state) { (void)state; /* Well-formed XML with an unrecognized root or no known child. */ assert_int_equal(detect_xml(""), QDL_FILE_UNKNOWN); assert_int_equal(detect_xml(""), QDL_FILE_UNKNOWN); /* Not well-formed XML. */ assert_true(detect_xml("this is not xml <<<") < 0); } static void test_verb_precedence_over_data_child(void **state) { (void)state; /* * The verb match happens before any file access, so a data document * whose first recognized child is "read" is classified as a file, not * confused with the read verb. */ assert_int_equal(detect_xml(""), QDL_FILE_READ); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_verbs), cmocka_unit_test(test_not_a_verb_or_file), cmocka_unit_test(test_xml_roots), cmocka_unit_test(test_xml_unknown_and_bad), cmocka_unit_test(test_is_contents_xml), cmocka_unit_test(test_verb_precedence_over_data_child), }; return cmocka_run_group_tests(tests, NULL, NULL); } linux-msm-qdl-6e625aa/tests/test_op_loaders.c000066400000000000000000000137231527206113100213340ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* * Unit tests for the and XML loaders (patch.c, read.c). * They turn XML into firehose op lists; util.c is linked in for the real * attribute accessors, and the device/ux layers are stubbed. */ #include #include #include #include #include #include #include #include #include #include #include "list.h" #include "file.h" #include "firehose.h" #include "patch.h" #include "read.h" #include "qdl.h" #include "common.h" /* --- stubs --- */ void ux_init(void) {} void ux_err(const char *fmt, ...) { (void)fmt; } void ux_info(const char *fmt, ...) { (void)fmt; } void ux_log(const char *fmt, ...) { (void)fmt; } void ux_debug(const char *fmt, ...) { (void)fmt; } void ux_progress(const char *fmt, unsigned int value, unsigned int size, ...) { (void)fmt; (void)value; (void)size; } /* util.c references these via load_sahara_image(), never called here. */ int qdl_file_open(struct qdl_zip *z, const char *f, struct qdl_file *file) { (void)z; (void)f; (void)file; return -1; } void *qdl_file_load(struct qdl_file *file, size_t *len) { (void)file; (void)len; return NULL; } void qdl_file_close(struct qdl_file *file) { (void)file; } struct firehose_op *firehose_alloc_op(int type) { struct firehose_op *op = calloc(1, sizeof(*op)); if (op) op->type = type; return op; } /* --- helpers --- */ static void free_ops(struct list_head *ops) { struct firehose_op *op; struct firehose_op *next; list_for_each_entry_safe(op, next, ops, node) { list_del(&op->node); free((void *)op->filename); free((void *)op->label); free((void *)op->start_sector); free((void *)op->value); free((void *)op->what); free(op); } } static unsigned int count_ops(struct list_head *ops) { struct firehose_op *op; unsigned int n = 0; list_for_each_entry(op, ops, node) n++; return n; } static struct firehose_op *first_op(struct list_head *ops) { return list_entry_first(ops, struct firehose_op, node); } /* --- patch loader --- */ static int load_patch_str(struct list_head *ops, const char *xml) { xmlDoc *doc = xmlReadMemory(xml, (int)strlen(xml), "test.xml", NULL, 0); int ret; assert_non_null(doc); ret = patch_load_xml(ops, doc, "test.xml"); xmlFreeDoc(doc); return ret; } #define PATCH_OK \ "" static void test_patch_valid(void **state) { struct list_head ops = LIST_INIT(ops); struct firehose_op *op; (void)state; assert_int_equal(load_patch_str(&ops, PATCH_OK), 0); assert_int_equal(count_ops(&ops), 1); op = first_op(&ops); assert_int_equal(op->type, FIREHOSE_OP_PATCH); assert_int_equal(op->partition, 1); assert_int_equal(op->size_in_bytes, 4); assert_string_equal(op->filename, "DISK"); assert_string_equal(op->what, "crc"); free_ops(&ops); } static void test_patch_missing_attr_fails(void **state) { struct list_head ops = LIST_INIT(ops); /* Same as PATCH_OK but with the "value" attribute removed. */ const char *xml = ""; (void)state; assert_true(load_patch_str(&ops, xml) < 0); free_ops(&ops); } static void test_patch_unknown_tag_skipped(void **state) { struct list_head ops = LIST_INIT(ops); const char *xml = ""; (void)state; /* An unrecognized child is skipped, the valid patch still loads. */ assert_int_equal(load_patch_str(&ops, xml), 0); assert_int_equal(count_ops(&ops), 1); free_ops(&ops); } /* --- read loader (file based) --- */ static int load_read_file(struct list_head *ops, const char *xml) { char dir[256]; char path[512]; FILE *f; int ret; assert_int_equal(test_make_temp_dir(dir, sizeof(dir), "qdl-read"), 0); snprintf(path, sizeof(path), "%s/read.xml", dir); f = fopen(path, "w"); assert_non_null(f); fputs(xml, f); fclose(f); ret = read_op_load(ops, path, NULL); unlink(path); rmdir(dir); return ret; } static void test_read_valid(void **state) { struct list_head ops = LIST_INIT(ops); struct firehose_op *op; const char *xml = ""; (void)state; assert_int_equal(load_read_file(&ops, xml), 0); assert_int_equal(count_ops(&ops), 1); op = first_op(&ops); assert_int_equal(op->type, FIREHOSE_OP_READ); assert_int_equal(op->num_sectors, 10); assert_string_equal(op->filename, "out.img"); free_ops(&ops); } static void test_read_missing_attr_fails(void **state) { struct list_head ops = LIST_INIT(ops); /* filename attribute removed. */ const char *xml = ""; (void)state; assert_true(load_read_file(&ops, xml) < 0); free_ops(&ops); } static void test_read_missing_file_fails(void **state) { struct list_head ops = LIST_INIT(ops); (void)state; assert_true(read_op_load(&ops, "/nonexistent/qdl/read.xml", NULL) < 0); free_ops(&ops); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_patch_valid), cmocka_unit_test(test_patch_missing_attr_fails), cmocka_unit_test(test_patch_unknown_tag_skipped), cmocka_unit_test(test_read_valid), cmocka_unit_test(test_read_missing_attr_fails), cmocka_unit_test(test_read_missing_file_fails), }; return cmocka_run_group_tests(tests, NULL, NULL); } linux-msm-qdl-6e625aa/tests/test_oscompat.c000066400000000000000000000101441527206113100210240ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #define _FILE_OFFSET_BITS 64 #include #include #include #include #include #include #include #include #include #include #include #include #include "oscompat.h" #define WORKDIR "test_oscompat_workdir" static bool is_dir(const char *path) { struct stat st; return stat(path, &st) == 0 && S_ISDIR(st.st_mode); } static void rmtree(const char *path) { struct dirent *ent; DIR *dir; dir = opendir(path); if (dir) { while ((ent = readdir(dir)) != NULL) { char child[PATH_MAX]; struct stat st; if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) continue; if (snprintf(child, sizeof(child), "%s/%s", path, ent->d_name) >= (int)sizeof(child)) { fprintf(stderr, "skipping overly-long path %s/%s\n", path, ent->d_name); continue; } if (stat(child, &st) == 0 && S_ISDIR(st.st_mode)) rmtree(child); else unlink(child); } closedir(dir); } rmdir(path); } /* ----------------------------- path_is_absolute -------------------------- */ static void test_path_is_absolute(void **state) { (void)state; #ifndef _WIN32 assert_true(path_is_absolute("/")); assert_true(path_is_absolute("/foo/bar")); assert_false(path_is_absolute("foo")); assert_false(path_is_absolute("./foo")); assert_false(path_is_absolute("")); #else assert_true(path_is_absolute("C:\\foo")); assert_true(path_is_absolute("C:/foo")); assert_true(path_is_absolute("c:foo")); /* drive-relative */ assert_true(path_is_absolute("\\\\server\\share")); assert_false(path_is_absolute("foo")); assert_false(path_is_absolute("\\foo")); /* single backslash */ assert_false(path_is_absolute("")); #endif } /* ------------------------------- qdl_mkdir_p ----------------------------- */ static void test_mkdir_p_creates_nested(void **state) { (void)state; assert_int_equal(qdl_mkdir_p(WORKDIR "/a/b/c"), 0); assert_true(is_dir(WORKDIR)); assert_true(is_dir(WORKDIR "/a")); assert_true(is_dir(WORKDIR "/a/b")); assert_true(is_dir(WORKDIR "/a/b/c")); } static void test_mkdir_p_is_idempotent(void **state) { (void)state; assert_int_equal(qdl_mkdir_p(WORKDIR "/idem"), 0); /* An already existing directory is not an error. */ assert_int_equal(qdl_mkdir_p(WORKDIR "/idem"), 0); assert_true(is_dir(WORKDIR "/idem")); } static void test_mkdir_p_trailing_slash(void **state) { (void)state; assert_int_equal(qdl_mkdir_p(WORKDIR "/x/y/"), 0); assert_true(is_dir(WORKDIR "/x/y")); } static void test_mkdir_p_existing_and_dot(void **state) { (void)state; /* The current directory always exists; this must succeed. */ assert_int_equal(qdl_mkdir_p("."), 0); /* An empty path is a no-op. */ assert_int_equal(qdl_mkdir_p(""), 0); } static void test_mkdir_p_too_long(void **state) { char path[PATH_MAX + 16]; (void)state; memset(path, 'a', sizeof(path) - 1); path[sizeof(path) - 1] = '\0'; assert_int_equal(qdl_mkdir_p(path), -1); assert_int_equal(errno, ENAMETOOLONG); } static void test_mkdir_p_component_not_a_dir(void **state) { const char *file = WORKDIR "/afile"; FILE *fp; (void)state; assert_int_equal(qdl_mkdir_p(WORKDIR), 0); fp = fopen(file, "wb"); assert_non_null(fp); fclose(fp); /* A path component that is a regular file cannot be descended into. */ assert_int_equal(qdl_mkdir_p(WORKDIR "/afile/sub"), -1); } static int group_setup(void **state) { (void)state; rmtree(WORKDIR); return 0; } static int group_teardown(void **state) { (void)state; rmtree(WORKDIR); return 0; } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_path_is_absolute), cmocka_unit_test(test_mkdir_p_creates_nested), cmocka_unit_test(test_mkdir_p_is_idempotent), cmocka_unit_test(test_mkdir_p_trailing_slash), cmocka_unit_test(test_mkdir_p_existing_and_dot), cmocka_unit_test(test_mkdir_p_too_long), cmocka_unit_test(test_mkdir_p_component_not_a_dir), }; return cmocka_run_group_tests(tests, group_setup, group_teardown); } linux-msm-qdl-6e625aa/tests/test_pathbuf.c000066400000000000000000000115221527206113100206310ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #define _FILE_OFFSET_BITS 64 #include #include #include #include #include #include #include #include "pathbuf.h" static void init_path(struct pathbuf *path, const char *s, bool set_len) { size_t len = strlen(s); assert_true(len < sizeof(path->buf)); memcpy(path->buf, s, len + 1); path->len = set_len ? len : 0; } static void assert_dirname(const char *in, const char *out, bool set_len) { struct pathbuf path; init_path(&path, in, set_len); qdl_pathbuf_dirname(&path); assert_string_equal(path.buf, out); assert_int_equal(path.len, strlen(out)); } static void test_dirname_posix_root(void **state) { (void)state; assert_dirname("/", "/", true); assert_dirname("/", "/", false); } static void test_dirname_posix_absolute(void **state) { (void)state; assert_dirname("/foo", "/", true); assert_dirname("/foo/bar", "/foo", true); assert_dirname("/foo/bar/", "/foo", true); } static void test_dirname_relative(void **state) { (void)state; assert_dirname("foo", "foo", true); assert_dirname("foo/bar", "foo", true); assert_dirname("foo/bar/", "foo", true); } static void test_reset_and_dup(void **state) { struct pathbuf src; struct pathbuf dst; (void)state; init_path(&src, "/tmp/example", true); qdl_pathbuf_dup(&dst, &src); assert_string_equal(dst.buf, src.buf); assert_int_equal(dst.len, src.len); qdl_pathbuf_reset(&dst); assert_string_equal(dst.buf, ""); assert_int_equal(dst.len, 0); qdl_pathbuf_reset(NULL); } static void test_push_basic_behavior(void **state) { struct pathbuf path = {0}; int ret; (void)state; ret = qdl_pathbuf_push(&path, "foo"); assert_int_equal(ret, 0); assert_string_equal(path.buf, "foo"); assert_int_equal(path.len, strlen("foo")); ret = qdl_pathbuf_push(&path, "bar"); assert_int_equal(ret, 0); assert_string_equal(path.buf, "foo/bar"); assert_int_equal(path.len, strlen("foo/bar")); ret = qdl_pathbuf_push(&path, ""); assert_int_equal(ret, 0); assert_string_equal(path.buf, "foo/bar"); assert_int_equal(path.len, strlen("foo/bar")); ret = qdl_pathbuf_push(&path, "./baz"); assert_int_equal(ret, 0); assert_string_equal(path.buf, "foo/bar/baz"); assert_int_equal(path.len, strlen("foo/bar/baz")); #ifdef _WIN32 ret = qdl_pathbuf_push(&path, ".\\qux"); assert_int_equal(ret, 0); assert_string_equal(path.buf, "foo/bar/baz/qux"); assert_int_equal(path.len, strlen("foo/bar/baz/qux")); ret = qdl_pathbuf_push(&path, "nested\\leaf"); assert_int_equal(ret, 0); assert_string_equal(path.buf, "foo/bar/baz/qux/nested/leaf"); assert_int_equal(path.len, strlen("foo/bar/baz/qux/nested/leaf")); ret = qdl_pathbuf_push(&path, "C:/abs"); assert_int_equal(ret, 0); assert_string_equal(path.buf, "C:/abs"); assert_int_equal(path.len, strlen("C:/abs")); #else ret = qdl_pathbuf_push(&path, "/abs"); assert_int_equal(ret, 0); assert_string_equal(path.buf, "/abs"); assert_int_equal(path.len, strlen("/abs")); #endif } static void test_push_invalid_args(void **state) { struct pathbuf path = {0}; (void)state; assert_int_equal(qdl_pathbuf_push(NULL, "foo"), -EINVAL); assert_int_equal(qdl_pathbuf_push(&path, NULL), -EINVAL); } static void test_push_overflow_keeps_state(void **state) { struct pathbuf path = {0}; char before[PATH_MAX]; size_t max_prefix_len; int ret; (void)state; max_prefix_len = sizeof(path.buf) - 1; memset(path.buf, 'a', max_prefix_len); path.buf[max_prefix_len] = '\0'; path.len = max_prefix_len; memcpy(before, path.buf, sizeof(before)); ret = qdl_pathbuf_push(&path, "b"); assert_int_equal(ret, -ENAMETOOLONG); assert_string_equal(path.buf, before); assert_int_equal(path.len, max_prefix_len); } #ifdef _WIN32 static void test_dirname_windows_drive(void **state) { (void)state; assert_dirname("C:\\", "C:\\", true); assert_dirname("C:\\foo", "C:\\", true); assert_dirname("C:\\foo\\bar", "C:\\foo", true); assert_dirname("C:/foo/bar", "C:/foo", true); } static void test_dirname_windows_unc(void **state) { (void)state; assert_dirname("\\\\server\\share", "\\\\server\\share", true); assert_dirname("\\\\server\\share\\dir", "\\\\server\\share", true); assert_dirname("\\\\server\\share\\dir\\sub", "\\\\server\\share\\dir", true); } #endif int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_dirname_posix_root), cmocka_unit_test(test_dirname_posix_absolute), cmocka_unit_test(test_dirname_relative), cmocka_unit_test(test_reset_and_dup), cmocka_unit_test(test_push_basic_behavior), cmocka_unit_test(test_push_invalid_args), cmocka_unit_test(test_push_overflow_keeps_state), #ifdef _WIN32 cmocka_unit_test(test_dirname_windows_drive), cmocka_unit_test(test_dirname_windows_unc), #endif }; return cmocka_run_group_tests(tests, NULL, NULL); } linux-msm-qdl-6e625aa/tests/test_program_load_xml.c000066400000000000000000000234441527206113100225340ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ #define _FILE_OFFSET_BITS 64 #if defined(__APPLE__) #define _DARWIN_C_SOURCE #endif #define _XOPEN_SOURCE 700 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "file.h" #include "firehose.h" #include "list.h" #include "pathbuf.h" #include "program.h" #include "qdl.h" #include "sparse.h" #include "common.h" #define TEST_INCDIR "/mock/incdir" #define TEST_XMLDIR "/mock/xml" #define TEST_PROGRAM_FILE TEST_XMLDIR "/program.xml" #define TEST_PAYLOAD "payload.bin" #define TEST_INCDIR_PAYLOAD TEST_INCDIR "/" TEST_PAYLOAD #define TEST_XMLDIR_PAYLOAD TEST_XMLDIR "/" TEST_PAYLOAD bool qdl_debug; static const char *existing_paths[4]; static void set_existing_paths(const char *path0, const char *path1, const char *path2) { existing_paths[0] = path0; existing_paths[1] = path1; existing_paths[2] = path2; existing_paths[3] = NULL; } static bool mock_path_exists(const char *path) { for (size_t i = 0; existing_paths[i]; i++) { if (!strcmp(path, existing_paths[i])) return true; } return false; } int mock_access(const char *path, int mode) { assert_int_equal(mode, F_OK); if (mock_path_exists(path)) return 0; errno = ENOENT; return -1; } int qdl_file_open(struct qdl_zip *qzip, const char *filename, struct qdl_file *file) { assert_null(qzip); if (!mock_path_exists(filename)) return -1; file->type = QDL_FILE_TYPE_POSIX; file->size = 1; file->fd = -1; file->zip_file = NULL; return 0; } void qdl_file_close(struct qdl_file *file) { file->type = QDL_FILE_TYPE_UNKNOWN; } void *qdl_file_load(struct qdl_file *file, size_t *len) { (void)file; (void)len; return NULL; } size_t qdl_file_getsize(struct qdl_file *file) { return file->size; } off_t qdl_file_seek(struct qdl_file *file, off_t offset, int whence) { (void)file; (void)offset; (void)whence; return -1; } ssize_t qdl_file_read(struct qdl_file *file, void *buf, size_t len) { (void)file; (void)buf; (void)len; return -1; } struct qdl_zip *qdl_zip_get(struct qdl_zip *qzip) { return qzip; } void qdl_zip_put(struct qdl_zip *qzip) { (void)qzip; } int sparse_header_parse(struct qdl_file *file, sparse_header_t *sparse_header) { (void)file; (void)sparse_header; return -1; } int sparse_chunk_header_parse(struct qdl_file *file, sparse_header_t *sparse_header, uint64_t *chunk_size, uint32_t *value, off_t *offset) { (void)file; (void)sparse_header; (void)chunk_size; (void)value; (void)offset; return -1; } void ux_init(void) { } void ux_err(const char *fmt, ...) { (void)fmt; } void ux_err_tag(enum ux_tag tag, const char *fmt, ...) { (void)tag; (void)fmt; } void ux_info(const char *fmt, ...) { (void)fmt; } void ux_log(const char *fmt, ...) { (void)fmt; } void ux_debug(const char *fmt, ...) { (void)fmt; } void ux_progress(const char *fmt, unsigned int value, unsigned int size, ...) { (void)fmt; (void)value; (void)size; } struct firehose_op *firehose_alloc_op(int type) { struct firehose_op *op; op = calloc(1, sizeof(*op)); if (!op) return NULL; op->type = type; return op; } void firehose_free_ops(struct list_head *ops) { struct firehose_op *next; struct firehose_op *op; list_for_each_entry_safe(op, next, ops, node) { list_del(&op->node); qdl_zip_put(op->zip); free((void *)op->filename); free((void *)op->label); free((void *)op->start_sector); free((void *)op->gpt_partition); free((void *)op->value); free((void *)op->what); free(op); } } int contents_resolve_path(struct contents_filter *filter, const char *filename, struct pathbuf *path) { (void)filter; (void)filename; (void)path; return 0; } static xmlDoc *build_program_doc(const char *filename) { char xml[1024]; int ret; ret = snprintf(xml, sizeof(xml), "" "" "", filename); assert_true(ret > 0 && (size_t)ret < sizeof(xml)); return xmlReadMemory(xml, ret, "program.xml", NULL, 0); } static struct firehose_op *get_only_op(struct list_head *ops) { struct firehose_op *op; struct list_head *it; size_t count = 0; list_for_each(it, ops) count++; if (count != 1) fail_msg("expected exactly one firehose op, got %zu", count); op = list_entry_first(ops, struct firehose_op, node); if (op->type != FIREHOSE_OP_PROGRAM) fail_msg("expected firehose op type PROGRAM, got %d", op->type); return op; } static void test_incdir_takes_precedence(void **state) { struct firehose_op *op; struct list_head ops = LIST_INIT(ops); xmlDoc *doc; int ret; (void)state; set_existing_paths(TEST_INCDIR_PAYLOAD, TEST_XMLDIR_PAYLOAD, NULL); doc = build_program_doc(TEST_PAYLOAD); if (!doc) fail_msg("failed to parse synthetic program XML"); ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR); if (ret) fail_msg("incdir payload should be accepted, got %d", ret); op = get_only_op(&ops); if (!op->filename || strcmp(op->filename, TEST_INCDIR_PAYLOAD)) fail_msg("incdir should take precedence, got '%s'", op->filename ? op->filename : "(null)"); firehose_free_ops(&ops); xmlFreeDoc(doc); } static void test_program_xml_directory_is_used(void **state) { struct firehose_op *op; struct list_head ops = LIST_INIT(ops); xmlDoc *doc; int ret; (void)state; set_existing_paths(TEST_XMLDIR_PAYLOAD, NULL, NULL); doc = build_program_doc(TEST_PAYLOAD); if (!doc) fail_msg("failed to parse synthetic program XML"); ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR); if (ret) fail_msg("XML-adjacent payload should be accepted, got %d", ret); op = get_only_op(&ops); if (!op->filename || strcmp(op->filename, TEST_XMLDIR_PAYLOAD)) fail_msg("program XML directory should be used, got '%s'", op->filename ? op->filename : "(null)"); firehose_free_ops(&ops); xmlFreeDoc(doc); } static void test_program_xml_directory_takes_precedence_over_current_directory(void **state) { struct firehose_op *op; struct list_head ops = LIST_INIT(ops); xmlDoc *doc; int ret; (void)state; set_existing_paths(TEST_XMLDIR_PAYLOAD, TEST_PAYLOAD, NULL); doc = build_program_doc(TEST_PAYLOAD); if (!doc) fail_msg("failed to parse synthetic program XML"); ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR); if (ret) fail_msg("XML-adjacent payload should beat current-directory fallback, got %d", ret); op = get_only_op(&ops); if (!op->filename || strcmp(op->filename, TEST_XMLDIR_PAYLOAD)) fail_msg("program XML directory should take precedence over current directory, got '%s'", op->filename ? op->filename : "(null)"); firehose_free_ops(&ops); xmlFreeDoc(doc); } static void test_current_directory_is_used_when_path_resolution_misses(void **state) { struct firehose_op *op; struct list_head ops = LIST_INIT(ops); xmlDoc *doc; int ret; (void)state; set_existing_paths(TEST_PAYLOAD, NULL, NULL); doc = build_program_doc(TEST_PAYLOAD); if (!doc) fail_msg("failed to parse synthetic program XML"); /* Compatibility fallback: leave the original relative filename intact. */ ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR); if (ret) fail_msg("relative fallback payload should be accepted, got %d", ret); op = get_only_op(&ops); if (!op->filename || strcmp(op->filename, TEST_PAYLOAD)) fail_msg("unresolved relative filename should be preserved, got '%s'", op->filename ? op->filename : "(null)"); firehose_free_ops(&ops); xmlFreeDoc(doc); } static void test_missing_file_fails_without_allow_missing(void **state) { struct list_head ops = LIST_INIT(ops); xmlDoc *doc; int ret; (void)state; set_existing_paths(NULL, NULL, NULL); doc = build_program_doc(TEST_PAYLOAD); if (!doc) fail_msg("failed to parse synthetic program XML"); ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, false, NULL, TEST_INCDIR); if (ret != -1) fail_msg("missing payload should fail without allow_missing, got %d", ret); if (!list_empty(&ops)) fail_msg("missing payload without allow_missing should not append firehose ops"); xmlFreeDoc(doc); } static void test_missing_file_is_tolerated_with_allow_missing(void **state) { struct firehose_op *op; struct list_head ops = LIST_INIT(ops); xmlDoc *doc; int ret; (void)state; set_existing_paths(NULL, NULL, NULL); doc = build_program_doc(TEST_PAYLOAD); if (!doc) fail_msg("failed to parse synthetic program XML"); ret = program_load_xml(&ops, doc, NULL, TEST_PROGRAM_FILE, false, true, NULL, TEST_INCDIR); if (ret) fail_msg("missing payload should be tolerated with allow_missing, got %d", ret); op = get_only_op(&ops); if (op->filename) fail_msg("allow_missing should clear the missing program filename, got '%s'", op->filename); firehose_free_ops(&ops); xmlFreeDoc(doc); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_incdir_takes_precedence), cmocka_unit_test(test_program_xml_directory_is_used), cmocka_unit_test(test_program_xml_directory_takes_precedence_over_current_directory), cmocka_unit_test(test_current_directory_is_used_when_path_resolution_misses), cmocka_unit_test(test_missing_file_fails_without_allow_missing), cmocka_unit_test(test_missing_file_is_tolerated_with_allow_missing), }; return cmocka_run_group_tests(tests, NULL, NULL); } linux-msm-qdl-6e625aa/tests/test_programmer.c000066400000000000000000000146501527206113100213600ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* * Unit tests for the programmer CPIO archive decoder (programmer.c). The * decoder parses an untrusted blob, so the tests craft small newc CPIO * archives and verify both correct decoding and rejection of the * malformed inputs the parser was hardened against. */ #include #include #include #include #include #include #include #include "qdl.h" #include "programmer.h" /* --- stubs --- */ void ux_err(const char *fmt, ...) { (void)fmt; } /* * decode_programmer() references the Sahara image loader; the tests * exercise only the archive decoder, so a failing stub satisfies the * linker without pulling in sahara.c. */ int load_sahara_image(struct qdl_zip *zip, const char *filename, struct sahara_image *image) { (void)zip; (void)filename; (void)image; return -1; } int decode_sahara_config(struct sahara_image *blob, struct sahara_image *images, struct contents_filter *filter) { (void)blob; (void)images; (void)filter; return 0; } /* Fatal logging lives in ux.c, which the test does not link. */ void __attribute__((noreturn)) ux_die(int eval, const char *fmt, ...) { (void)fmt; exit(eval); } void sahara_images_free(struct sahara_image *images, size_t count) { for (size_t i = 0; i < count; i++) { free(images[i].name); free(images[i].ptr); images[i] = (struct sahara_image){0}; } } /* --- newc CPIO archive builder --- */ #define CPIO_HDR_LEN 110 #define OFF_FILESIZE 54 #define OFF_NAMESIZE 94 struct builder { uint8_t *buf; size_t len; size_t cap; }; static void bput(struct builder *b, const void *data, size_t n) { if (b->len + n > b->cap) { b->cap = (b->len + n) * 2 + 64; b->buf = realloc(b->buf, b->cap); assert_non_null(b->buf); } memcpy(b->buf + b->len, data, n); b->len += n; } static void bpad4(struct builder *b) { static const uint8_t zero[4] = {0}; if (b->len % 4) bput(b, zero, 4 - (b->len % 4)); } static void put_hexfield(uint8_t *hdr, size_t off, uint32_t value) { char tmp[9]; snprintf(tmp, sizeof(tmp), "%08x", value); memcpy(hdr + off, tmp, 8); } /* * Append one entry. @name is written verbatim as the CPIO name of * @name_size bytes (so a caller can produce an unterminated name); pass * name_size == 0 to use strlen(name) + 1. */ static void add_entry(struct builder *b, const char *name, size_t name_size, const void *data, size_t data_len) { uint8_t hdr[CPIO_HDR_LEN]; if (name_size == 0) name_size = strlen(name) + 1; memset(hdr, '0', sizeof(hdr)); memcpy(hdr, "070701", 6); put_hexfield(hdr, OFF_FILESIZE, (uint32_t)data_len); put_hexfield(hdr, OFF_NAMESIZE, (uint32_t)name_size); bput(b, hdr, sizeof(hdr)); bput(b, name, name_size); bpad4(b); if (data_len) bput(b, data, data_len); bpad4(b); } static void add_trailer(struct builder *b) { add_entry(b, "TRAILER!!!", 0, NULL, 0); } /* Move the built archive into a heap blob the decoder can consume. */ static void into_blob(struct builder *b, struct sahara_image *blob) { blob->ptr = malloc(b->len); assert_non_null(blob->ptr); memcpy(blob->ptr, b->buf, b->len); blob->len = b->len; blob->name = NULL; free(b->buf); b->buf = NULL; } static struct sahara_image *zero_images(void) { static struct sahara_image images[MAPPING_SZ]; memset(images, 0, sizeof(images)); return images; } /* --- tests --- */ static void test_valid_archive(void **state) { struct builder b = {0}; struct sahara_image blob = {0}; struct sahara_image *images = zero_images(); (void)state; add_entry(&b, "1:prog", 0, "DATA", 4); add_trailer(&b); into_blob(&b, &blob); assert_int_equal(decode_programmer_archive(&blob, images), 1); assert_non_null(images[1].name); assert_string_equal(images[1].name, "prog"); assert_int_equal(images[1].len, 4); assert_memory_equal(images[1].ptr, "DATA", 4); /* The blob is consumed on success. */ assert_null(blob.ptr); sahara_images_free(images, MAPPING_SZ); free(blob.ptr); } static void test_not_an_archive(void **state) { struct sahara_image blob; struct sahara_image *images = zero_images(); (void)state; blob.ptr = strdup("not a cpio archive, definitely longer than a header .............."); blob.len = strlen(blob.ptr); blob.name = NULL; /* No CPIO magic -> not an archive; blob is left for the caller. */ assert_int_equal(decode_programmer_archive(&blob, images), 0); free(blob.ptr); } static void test_reject_bad_image_id(void **state) { struct builder b = {0}; struct sahara_image blob = {0}; struct sahara_image *images = zero_images(); (void)state; /* An id that overflows a long parses negative -> must be rejected. */ add_entry(&b, "0xffffffffffffffff:prog", 0, "DATA", 4); add_trailer(&b); into_blob(&b, &blob); assert_int_equal(decode_programmer_archive(&blob, images), -1); sahara_images_free(images, MAPPING_SZ); } static void test_reject_zero_image_id(void **state) { struct builder b = {0}; struct sahara_image blob = {0}; struct sahara_image *images = zero_images(); (void)state; add_entry(&b, "0:prog", 0, "DATA", 4); add_trailer(&b); into_blob(&b, &blob); assert_int_equal(decode_programmer_archive(&blob, images), -1); sahara_images_free(images, MAPPING_SZ); } static void test_reject_unterminated_name(void **state) { struct builder b = {0}; struct sahara_image blob = {0}; struct sahara_image *images = zero_images(); (void)state; /* Name field with no NUL within its declared size. */ add_entry(&b, "1:prog", 6, "DATA", 4); add_trailer(&b); into_blob(&b, &blob); assert_int_equal(decode_programmer_archive(&blob, images), -1); sahara_images_free(images, MAPPING_SZ); } static void test_reject_truncated(void **state) { struct builder b = {0}; struct sahara_image blob = {0}; struct sahara_image *images = zero_images(); (void)state; add_entry(&b, "1:prog", 0, "DATA", 4); into_blob(&b, &blob); /* Chop the archive so the declared data runs past the end. */ blob.len -= 4; assert_int_equal(decode_programmer_archive(&blob, images), -1); sahara_images_free(images, MAPPING_SZ); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_valid_archive), cmocka_unit_test(test_not_an_archive), cmocka_unit_test(test_reject_bad_image_id), cmocka_unit_test(test_reject_zero_image_id), cmocka_unit_test(test_reject_unterminated_name), cmocka_unit_test(test_reject_truncated), }; return cmocka_run_group_tests(tests, NULL, NULL); } linux-msm-qdl-6e625aa/tests/test_sahara_archive.py000066400000000000000000000241071527206113100223510ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. import argparse import os import shutil import subprocess import sys import tempfile from pathlib import Path def align_up(value, align): return (value + align - 1) & ~(align - 1) def read_hex(data, offset): return int(data[offset:offset + 8], 16) def parse_newc(filename): data = Path(filename).read_bytes() pos = 0 entries = {} while True: header = data[pos:pos + 110] assert len(header) == 110, "truncated archive header" assert header[:6] == b"070701", "bad archive magic" filesize = read_hex(header, 54) namesize = read_hex(header, 94) pos += 110 name = data[pos:pos + namesize] assert len(name) == namesize, "truncated archive name" assert name[-1:] == b"\0", "unterminated archive name" name = name[:-1].decode("ascii") pos = align_up(pos + namesize, 4) payload = data[pos:pos + filesize] assert len(payload) == filesize, "truncated archive payload" pos = align_up(pos + filesize, 4) if name == "TRAILER!!!": break entries[name] = payload return entries def run_qdl(qdl, *args, cwd=None, expect_success=True): result = subprocess.run([qdl, *args], cwd=cwd, capture_output=True, text=True) if expect_success and result.returncode: print(result.stdout) print(result.stderr, file=sys.stderr) raise AssertionError("qdl command failed") if not expect_success and not result.returncode: print(result.stdout) print(result.stderr, file=sys.stderr) raise AssertionError("qdl command succeeded unexpectedly") return result def write_payloads(path): payloads = { "prog.elf": b"programmer\0payload\n", "cfg.elf": b"cfg", "aux.bin": bytes(range(17)), } path.mkdir(parents=True, exist_ok=True) for name, data in payloads.items(): (path / name).write_bytes(data) return payloads def write_sahara_xml(filename, payload_dir, prog_name="prog.elf", cfg_name="cfg.elf"): filename.write_text(f""" test """, encoding="ascii") def write_relative_sahara_xml(filename): filename.write_text(""" test """, encoding="ascii") def write_contents_xml(filename): filename.write_text(""" ./ ./ sahara.xml ./ prog.elf payloads/ cfg.elf payloads/ rawprogram.xml ./ """, encoding="ascii") def write_multi_contents_xml(filename): filename.write_text(""" ./ ./ ufs-sahara.xml ./ emmc-sahara.xml ./ prog.elf ufs/ prog.elf emmc/ rawprogram-ufs.xml ./ rawprogram-emmc.xml ./ """, encoding="ascii") def write_single_image_sahara_xml(filename): filename.write_text(""" test """, encoding="ascii") def assert_archive(filename, expected): entries = parse_newc(filename) assert entries == expected, \ f"archive entries mismatch: {sorted(entries)} != {sorted(expected)}" def assert_no_archive(path): assert not path.exists(), f"unexpected archive created: {path}" def test_mapping(qdl, work, payloads): out = work / "mapping.bin" payload_dir = work / "payloads" run_qdl(qdl, "create-sahara-archive", str(out), f"13:{payload_dir / 'prog.elf'},38:{payload_dir / 'cfg.elf'}") assert_archive(out, { "13:prog.elf": payloads["prog.elf"], "38:cfg.elf": payloads["cfg.elf"], }) def test_sahara_xml(qdl, work, payloads): out = work / "sahara.bin" xml = work / "sahara-absolute.xml" write_sahara_xml(xml, work / "payloads") run_qdl(qdl, "create-sahara-archive", str(out), str(xml)) assert_archive(out, { "13:prog.elf": payloads["prog.elf"], "38:cfg.elf": payloads["cfg.elf"], }) def test_contents_xml(qdl, work, payloads): out = work / "contents.bin" xml = work / "contents.xml" write_relative_sahara_xml(work / "sahara.xml") write_contents_xml(xml) run_qdl(qdl, "create-sahara-archive", str(out), str(xml)) assert_archive(out, { "13:prog.elf": payloads["prog.elf"], "38:cfg.elf": payloads["cfg.elf"], }) def test_contents_xml_selector(qdl, work): xml = work / "multi-contents.xml" ufs = work / "ufs" emmc = work / "emmc" ufs.mkdir() emmc.mkdir() (ufs / "prog.elf").write_bytes(b"ufs programmer") (emmc / "prog.elf").write_bytes(b"emmc programmer") write_single_image_sahara_xml(work / "ufs-sahara.xml") write_single_image_sahara_xml(work / "emmc-sahara.xml") write_multi_contents_xml(xml) for storage, payload in [("ufs", b"ufs programmer"), ("emmc", b"emmc programmer")]: out = work / f"{storage}.bin" run_qdl(qdl, "create-sahara-archive", str(out), f"{xml}::{storage}") assert_archive(out, {"13:prog.elf": payload}) out = work / "unselected.bin" run_qdl(qdl, "create-sahara-archive", str(out), str(xml), expect_success=False) assert_no_archive(out) def test_archive_roundtrip(qdl, work, payloads): first = work / "roundtrip-source.bin" second = work / "roundtrip-result.bin" payload_dir = work / "payloads" run_qdl(qdl, "create-sahara-archive", str(first), f"13:{payload_dir / 'prog.elf'},38:{payload_dir / 'cfg.elf'}") run_qdl(qdl, "create-sahara-archive", str(second), str(first)) assert_archive(second, { "13:prog.elf": payloads["prog.elf"], "38:cfg.elf": payloads["cfg.elf"], }) def test_reject_space_separated(qdl, work): out = work / "space-separated.bin" payload_dir = work / "payloads" run_qdl(qdl, "create-sahara-archive", str(out), f"13:{payload_dir / 'prog.elf'}", f"38:{payload_dir / 'cfg.elf'}", expect_success=False) assert_no_archive(out) def test_reject_selector_on_non_contents(qdl, work): out = work / "selector.bin" xml = work / "sahara-selector.xml" write_sahara_xml(xml, work / "payloads") run_qdl(qdl, "create-sahara-archive", str(out), f"{xml}::ufs", expect_success=False) assert_no_archive(out) def test_bad_inputs(qdl, work): payload_dir = work / "payloads" empty = payload_dir / "empty.bin" empty.write_bytes(b"") for name, spec in [ ("invalid-zero-id.bin", f"0:{payload_dir / 'prog.elf'}"), ("invalid-large-id.bin", f"128:{payload_dir / 'prog.elf'}"), ("missing-payload.bin", f"13:{payload_dir / 'missing.elf'}"), ("empty-payload.bin", f"13:{empty}"), ]: out = work / name run_qdl(qdl, "create-sahara-archive", str(out), spec, expect_success=False) assert_no_archive(out) malformed = work / "malformed-sahara.xml" malformed.write_text("", encoding="ascii") out = work / "malformed.bin" run_qdl(qdl, "create-sahara-archive", str(out), str(malformed), expect_success=False) assert_no_archive(out) def main(): parser = argparse.ArgumentParser() parser.add_argument("--builddir", required=True) args = parser.parse_args() qdl = Path(args.builddir) / ("qdl.exe" if os.name == "nt" else "qdl") if not qdl.exists(): raise AssertionError(f"qdl binary not found: {qdl}") work = Path(tempfile.mkdtemp(prefix="sahara-archive-", dir=Path(args.builddir) / "tests")) try: payloads = write_payloads(work / "payloads") test_mapping(str(qdl), work, payloads) test_sahara_xml(str(qdl), work, payloads) test_contents_xml(str(qdl), work, payloads) test_contents_xml_selector(str(qdl), work) test_archive_roundtrip(str(qdl), work, payloads) test_reject_space_separated(str(qdl), work) test_reject_selector_on_non_contents(str(qdl), work) test_bad_inputs(str(qdl), work) finally: shutil.rmtree(work) if __name__ == "__main__": main() linux-msm-qdl-6e625aa/tests/test_ufs.c000066400000000000000000000130441527206113100177760ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* * Unit tests for the UFS provisioning loader. ufs_load() now operates on a * caller-owned struct ufs_provisioning instead of module globals, which is * what makes it testable here: the tests write a provisioning XML to a temp * file, load it into a fresh context, and assert both correct parsing and * that every error path leaves the context cleaned up. */ #include #include #include #include #include #include #include #include #include #include "qdl.h" #include "file.h" #include "ufs.h" #include "common.h" /* --- stubs --- */ void ux_err(const char *fmt, ...) { (void)fmt; } void ux_info(const char *fmt, ...) { (void)fmt; } void ux_warnx(const char *fmt, ...) { (void)fmt; } int qdl_file_open(struct qdl_zip *z, const char *f, struct qdl_file *file) { (void)z; (void)f; (void)file; return -1; } void *qdl_file_load(struct qdl_file *file, size_t *len) { (void)file; (void)len; return NULL; } void qdl_file_close(struct qdl_file *file) { (void)file; } /* --- XML fragments --- */ #define UFS_COMMON(lock) \ "" #define UFS_BODY \ "" #define UFS_EPILOGUE "" /* --- helper: write @xml to a temp file and load it into @ufs --- */ static int load_xml(struct ufs_provisioning *ufs, const char *xml, bool finalize) { char dir[256]; char path[512]; FILE *f; int ret; assert_int_equal(test_make_temp_dir(dir, sizeof(dir), "qdl-ufs"), 0); snprintf(path, sizeof(path), "%s/ufs.xml", dir); f = fopen(path, "w"); assert_non_null(f); fputs(xml, f); fclose(f); ret = ufs_load(ufs, path, finalize); unlink(path); rmdir(dir); return ret; } static void assert_empty(const struct ufs_provisioning *ufs) { assert_false(ufs_need_provisioning(ufs)); assert_null(ufs->common); assert_null(ufs->epilogue); } static void test_load_valid(void **state) { struct ufs_provisioning ufs; const char *xml = "" UFS_COMMON("0") UFS_BODY UFS_EPILOGUE ""; (void)state; ufs_provisioning_init(&ufs); assert_int_equal(load_xml(&ufs, xml, false), 0); assert_true(ufs_need_provisioning(&ufs)); assert_non_null(ufs.common); assert_int_equal(ufs.common->bNumberLU, 3); assert_false(list_empty(&ufs.bodies)); assert_non_null(ufs.epilogue); ufs_provisioning_cleanup(&ufs); assert_empty(&ufs); } static void test_load_incomplete_cleans_up(void **state) { struct ufs_provisioning ufs; /* No epilogue tag -> incomplete. */ const char *xml = "" UFS_COMMON("0") UFS_BODY ""; (void)state; ufs_provisioning_init(&ufs); assert_true(load_xml(&ufs, xml, false) < 0); /* On any error the context must be left empty, not dangling. */ assert_empty(&ufs); ufs_provisioning_cleanup(&ufs); /* safe no-op after a failed load */ } static void test_load_lock_mismatch_cleans_up(void **state) { struct ufs_provisioning ufs; /* bConfigDescrLock=1 without --finalize-provisioning is a mismatch. */ const char *xml = "" UFS_COMMON("1") UFS_BODY UFS_EPILOGUE ""; (void)state; ufs_provisioning_init(&ufs); assert_true(load_xml(&ufs, xml, false) < 0); /* * This path historically freed nothing and left the globals set; the * context must now be fully cleaned up. */ assert_empty(&ufs); ufs_provisioning_cleanup(&ufs); } static void test_load_finalize_ok(void **state) { struct ufs_provisioning ufs; const char *xml = "" UFS_COMMON("1") UFS_BODY UFS_EPILOGUE ""; (void)state; ufs_provisioning_init(&ufs); assert_int_equal(load_xml(&ufs, xml, true), 0); assert_true(ufs_need_provisioning(&ufs)); ufs_provisioning_cleanup(&ufs); assert_empty(&ufs); } static void test_load_duplicate_common_cleans_up(void **state) { struct ufs_provisioning ufs; const char *xml = "" UFS_COMMON("0") UFS_COMMON("0") UFS_BODY UFS_EPILOGUE ""; (void)state; ufs_provisioning_init(&ufs); assert_true(load_xml(&ufs, xml, false) < 0); assert_empty(&ufs); ufs_provisioning_cleanup(&ufs); } static void test_cleanup_idempotent(void **state) { struct ufs_provisioning ufs; struct ufs_provisioning snapshot; const char *xml = "" UFS_COMMON("0") UFS_BODY UFS_EPILOGUE ""; (void)state; ufs_provisioning_init(&ufs); assert_int_equal(load_xml(&ufs, xml, false), 0); /* * A repeated cleanup must be a bit-exact no-op, not merely leave * the context looking empty: snapshot the context after the first * cleanup and require the second to change nothing. */ ufs_provisioning_cleanup(&ufs); memcpy(&snapshot, &ufs, sizeof(ufs)); ufs_provisioning_cleanup(&ufs); assert_memory_equal(&ufs, &snapshot, sizeof(ufs)); assert_empty(&ufs); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_load_valid), cmocka_unit_test(test_load_incomplete_cleans_up), cmocka_unit_test(test_load_lock_mismatch_cleans_up), cmocka_unit_test(test_load_finalize_ok), cmocka_unit_test(test_load_duplicate_common_cleans_up), cmocka_unit_test(test_cleanup_idempotent), }; return cmocka_run_group_tests(tests, NULL, NULL); } linux-msm-qdl-6e625aa/tests/test_usb.c000066400000000000000000000226701527206113100177770ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* Unit tests for USB bulk-IN ZLP handling and serial discovery. */ #include #include #include #include #include #include #include #include struct mock_transfer { int ret; int actual; const char *data; }; static struct mock_transfer transfers[4]; static size_t transfer_count; static size_t transfer_index; int libusb_bulk_transfer(libusb_device_handle *dev_handle, unsigned char endpoint, unsigned char *data, int length, int *transferred, unsigned int timeout) { struct mock_transfer *transfer; (void)dev_handle; (void)endpoint; (void)timeout; assert_true(transfer_index < transfer_count); transfer = &transfers[transfer_index++]; assert_true(transfer->actual <= length); if (transfer->data) memcpy(data, transfer->data, transfer->actual); *transferred = transfer->actual; return transfer->ret; } struct mock_string { uint8_t index; int ret; const char *str; }; static struct mock_string strings[8]; static size_t string_count; static size_t string_index; int libusb_get_string_descriptor_ascii(libusb_device_handle *dev_handle, uint8_t desc_index, unsigned char *data, int length) { struct mock_string *string; size_t len; (void)dev_handle; assert_true(string_index < string_count); string = &strings[string_index++]; assert_int_equal(desc_index, string->index); if (string->ret < 0) return string->ret; len = strlen(string->str); assert_true(len < (size_t)length); memcpy(data, string->str, len + 1); return (int)len; } void ux_warnx(const char *fmt, ...) { (void)fmt; } #include "usb.c" static void set_strings(const struct mock_string *values, size_t count) { assert_true(count <= ARRAY_SIZE(strings)); memcpy(strings, values, count * sizeof(*values)); string_count = count; string_index = 0; } static void set_transfers(const struct mock_transfer *values, size_t count) { assert_true(count <= ARRAY_SIZE(transfers)); memcpy(transfers, values, count * sizeof(*values)); transfer_count = count; transfer_index = 0; } static void init_usb(struct qdl_device_usb *usb) { memset(usb, 0, sizeof(*usb)); usb->in_ep = 0x81; usb->in_maxpktsize = 512; } static void test_zlp_consumed_by_next_read(void **state) { const struct mock_transfer first[] = { { 0, 512, NULL }, }; const struct mock_transfer second[] = { { 0, 0, NULL }, { 0, 3, "ACK" }, }; struct qdl_device_usb usb; char buf[512]; (void)state; init_usb(&usb); set_transfers(first, ARRAY_SIZE(first)); assert_int_equal(usb_read(&usb.base, buf, sizeof(buf), 1000), 512); assert_true(usb.expect_zlp); assert_int_equal(transfer_index, 1); set_transfers(second, ARRAY_SIZE(second)); assert_int_equal(usb_read(&usb.base, buf, sizeof(buf), 1000), 3); assert_memory_equal(buf, "ACK", 3); assert_false(usb.expect_zlp); assert_int_equal(transfer_index, 2); } static void test_omitted_zlp_returns_data(void **state) { const struct mock_transfer transfer[] = { { 0, 3, "ACK" }, }; struct qdl_device_usb usb; char buf[512]; (void)state; init_usb(&usb); usb.expect_zlp = true; set_transfers(transfer, ARRAY_SIZE(transfer)); assert_int_equal(usb_read(&usb.base, buf, sizeof(buf), 1000), 3); assert_memory_equal(buf, "ACK", 3); assert_false(usb.expect_zlp); assert_int_equal(transfer_index, 1); } static void test_zlp_timeout_preserves_state(void **state) { const struct mock_transfer timeout[] = { { LIBUSB_ERROR_TIMEOUT, 0, NULL }, }; const struct mock_transfer retry[] = { { 0, 0, NULL }, { 0, 3, "ACK" }, }; struct qdl_device_usb usb; char buf[512]; (void)state; init_usb(&usb); usb.expect_zlp = true; set_transfers(timeout, ARRAY_SIZE(timeout)); assert_int_equal(usb_read(&usb.base, buf, sizeof(buf), 1000), -ETIMEDOUT); assert_true(usb.expect_zlp); set_transfers(retry, ARRAY_SIZE(retry)); assert_int_equal(usb_read(&usb.base, buf, sizeof(buf), 1000), 3); assert_false(usb.expect_zlp); } static void test_partial_timeout_returns_data(void **state) { const struct mock_transfer transfer[] = { { LIBUSB_ERROR_TIMEOUT, 3, "ACK" }, }; struct qdl_device_usb usb; char buf[512]; (void)state; init_usb(&usb); set_transfers(transfer, ARRAY_SIZE(transfer)); assert_int_equal(usb_read(&usb.base, buf, sizeof(buf), 1000), 3); assert_memory_equal(buf, "ACK", 3); } static void test_error_clears_zlp_state(void **state) { const struct mock_transfer no_device[] = { { LIBUSB_ERROR_NO_DEVICE, 0, NULL }, }; const struct mock_transfer io_error[] = { { LIBUSB_ERROR_IO, 0, NULL }, }; struct qdl_device_usb usb; char buf[512]; (void)state; init_usb(&usb); usb.expect_zlp = true; set_transfers(no_device, ARRAY_SIZE(no_device)); assert_int_equal(usb_read(&usb.base, buf, sizeof(buf), 1000), -ENODEV); assert_false(usb.expect_zlp); usb.expect_zlp = true; set_transfers(io_error, ARRAY_SIZE(io_error)); assert_int_equal(usb_read(&usb.base, buf, sizeof(buf), 1000), -EIO); assert_false(usb.expect_zlp); } static const struct libusb_device_descriptor edl_desc = { .iProduct = 2, .iSerialNumber = 0, }; static const struct libusb_device_descriptor android_desc = { .iProduct = 2, .iSerialNumber = 3, }; static void test_serial_from_iproduct_token(void **state) { const struct mock_string edl[] = { { 2, 0, "QUSB_BULK_CID:0402_SN:9DBBF967" }, }; char serial[16]; (void)state; set_strings(edl, ARRAY_SIZE(edl)); assert_true(usb_read_serial(NULL, &edl_desc, serial, sizeof(serial))); assert_string_equal(serial, "9DBBF967"); assert_int_equal(string_index, 1); } static void test_serial_falls_back_to_iserial(void **state) { const struct mock_string android[] = { { 2, 0, "qcs6490-rb3gen2-vision-kit" }, { 3, 0, "9dbbf967" }, }; char serial[16]; (void)state; set_strings(android, ARRAY_SIZE(android)); assert_true(usb_read_serial(NULL, &android_desc, serial, sizeof(serial))); assert_string_equal(serial, "9dbbf967"); assert_int_equal(string_index, 2); } static void test_serial_prefers_iproduct_token(void **state) { const struct mock_string both[] = { { 2, 0, "QUSB_BULK_CID:0402_SN:9DBBF967" }, }; char serial[16]; (void)state; set_strings(both, ARRAY_SIZE(both)); assert_true(usb_read_serial(NULL, &android_desc, serial, sizeof(serial))); assert_string_equal(serial, "9DBBF967"); assert_int_equal(string_index, 1); } static void test_serial_missing_without_iserial(void **state) { const struct mock_string plain[] = { { 2, 0, "qcs6490-rb3gen2-vision-kit" }, }; char serial[16]; (void)state; set_strings(plain, ARRAY_SIZE(plain)); assert_false(usb_read_serial(NULL, &edl_desc, serial, sizeof(serial))); assert_int_equal(string_index, 1); } static void test_serial_rejects_empty_iserial(void **state) { const struct mock_string empty[] = { { 2, 0, "qcs6490-rb3gen2-vision-kit" }, { 3, 0, "" }, }; char serial[16]; (void)state; set_strings(empty, ARRAY_SIZE(empty)); assert_false(usb_read_serial(NULL, &android_desc, serial, sizeof(serial))); assert_int_equal(string_index, 2); } static void test_serial_retries_transient_error(void **state) { const struct mock_string flaky[] = { { 2, LIBUSB_ERROR_IO, NULL }, { 2, LIBUSB_ERROR_PIPE, NULL }, { 2, 0, "QUSB_BULK_CID:0402_SN:9DBBF967" }, }; char serial[16]; (void)state; set_strings(flaky, ARRAY_SIZE(flaky)); assert_true(usb_read_serial(NULL, &edl_desc, serial, sizeof(serial))); assert_string_equal(serial, "9DBBF967"); assert_int_equal(string_index, 3); } static void test_serial_gives_up_after_retries(void **state) { const struct mock_string wedged[] = { { 2, LIBUSB_ERROR_IO, NULL }, { 2, LIBUSB_ERROR_IO, NULL }, { 2, LIBUSB_ERROR_IO, NULL }, }; char serial[16]; (void)state; set_strings(wedged, ARRAY_SIZE(wedged)); assert_false(usb_read_serial(NULL, &edl_desc, serial, sizeof(serial))); assert_int_equal(string_index, USB_STRING_ATTEMPTS); } static void test_serial_no_retry_on_fatal_error(void **state) { const struct mock_string gone[] = { { 2, LIBUSB_ERROR_NO_DEVICE, NULL }, }; char serial[16]; (void)state; set_strings(gone, ARRAY_SIZE(gone)); assert_false(usb_read_serial(NULL, &edl_desc, serial, sizeof(serial))); assert_int_equal(string_index, 1); } static void test_serial_match_ignores_case(void **state) { const struct mock_string edl[] = { { 2, 0, "QUSB_BULK_CID:0402_SN:9DBBF967" }, }; (void)state; set_strings(edl, ARRAY_SIZE(edl)); assert_true(usb_match_serial(NULL, "9dbbf967", &edl_desc)); set_strings(edl, ARRAY_SIZE(edl)); assert_false(usb_match_serial(NULL, "9dbbf968", &edl_desc)); set_strings(edl, 0); assert_true(usb_match_serial(NULL, NULL, &edl_desc)); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_serial_match_ignores_case), cmocka_unit_test(test_serial_from_iproduct_token), cmocka_unit_test(test_serial_falls_back_to_iserial), cmocka_unit_test(test_serial_prefers_iproduct_token), cmocka_unit_test(test_serial_missing_without_iserial), cmocka_unit_test(test_serial_rejects_empty_iserial), cmocka_unit_test(test_serial_retries_transient_error), cmocka_unit_test(test_serial_gives_up_after_retries), cmocka_unit_test(test_serial_no_retry_on_fatal_error), cmocka_unit_test(test_zlp_consumed_by_next_read), cmocka_unit_test(test_omitted_zlp_returns_data), cmocka_unit_test(test_zlp_timeout_preserves_state), cmocka_unit_test(test_partial_timeout_returns_data), cmocka_unit_test(test_error_clears_zlp_state), }; return cmocka_run_group_tests(tests, NULL, NULL); } linux-msm-qdl-6e625aa/tests/test_util.c000066400000000000000000000210751527206113100201610ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* * Unit tests for the pure helpers in src/util.c: the storage address * parser, the storage-type encode/decode pair, and the XML attribute * accessors. * * util.c also contains load_sahara_image(), which references the * qdl_file_* and ux_err symbols; the tests below never call it, but the * whole translation unit is linked in, so trivial stubs are provided to * satisfy the linker. */ #include #include #include #include #include #include #include #include #include "qdl.h" #include "file.h" /* --- stubs for symbols referenced by unrelated util.c code --- */ void ux_err(const char *fmt, ...) { (void)fmt; } void ux_info(const char *fmt, ...) { (void)fmt; } int qdl_file_open(struct qdl_zip *z, const char *f, struct qdl_file *file) { (void)z; (void)f; (void)file; return -1; } void *qdl_file_load(struct qdl_file *file, size_t *len) { (void)file; (void)len; return NULL; } void qdl_file_close(struct qdl_file *file) { (void)file; } /* --- parse_storage_address --- */ static void test_parse_addr_partition_only(void **state) { int part = 99; unsigned int start = 99, num = 99; char *gpt = (char *)0x1; (void)state; assert_int_equal(parse_storage_address("3", &part, &start, &num, &gpt), 0); assert_int_equal(part, 3); assert_int_equal(start, 0); assert_int_equal(num, 0); assert_null(gpt); } static void test_parse_addr_partition_sector(void **state) { int part = 0; unsigned int start = 0, num = 99; char *gpt = (char *)0x1; (void)state; assert_int_equal(parse_storage_address("2/100", &part, &start, &num, &gpt), 0); assert_int_equal(part, 2); assert_int_equal(start, 100); assert_int_equal(num, 0); assert_null(gpt); } static void test_parse_addr_sector_length(void **state) { int part = 0; unsigned int start = 0, num = 0; char *gpt = (char *)0x1; (void)state; assert_int_equal(parse_storage_address("1/50+10", &part, &start, &num, &gpt), 0); assert_int_equal(part, 1); assert_int_equal(start, 50); assert_int_equal(num, 10); assert_null(gpt); } static void test_parse_addr_gpt_name(void **state) { int part = 0; unsigned int start = 0, num = 0; char *gpt = NULL; (void)state; /* Bare name: match across all physical partitions. */ assert_int_equal(parse_storage_address("system", &part, &start, &num, &gpt), 0); assert_int_equal(part, -1); assert_non_null(gpt); assert_string_equal(gpt, "system"); free(gpt); /* N/name: match within a physical partition. */ gpt = NULL; assert_int_equal(parse_storage_address("2/boot", &part, &start, &num, &gpt), 0); assert_int_equal(part, 2); assert_non_null(gpt); assert_string_equal(gpt, "boot"); free(gpt); } static void test_parse_addr_errors(void **state) { int part; unsigned int start, num; char *gpt; (void)state; /* Zero length is rejected. */ assert_int_equal(parse_storage_address("1/50+0", &part, &start, &num, &gpt), -1); /* Missing length digits after '+'. */ assert_int_equal(parse_storage_address("1/50+", &part, &start, &num, &gpt), -1); /* Non-numeric length. */ assert_int_equal(parse_storage_address("1/50+abc", &part, &start, &num, &gpt), -1); /* Trailing separator that is neither '+' nor end. */ assert_int_equal(parse_storage_address("1/2/3", &part, &start, &num, &gpt), -1); } /* --- storage type encode/decode --- */ static void test_storage_type_roundtrip(void **state) { const enum qdl_storage_type types[] = { QDL_STORAGE_EMMC, QDL_STORAGE_NAND, QDL_STORAGE_UFS, QDL_STORAGE_NVME, QDL_STORAGE_SPINOR, }; size_t i; (void)state; assert_int_equal(decode_storage_type("emmc"), QDL_STORAGE_EMMC); assert_int_equal(decode_storage_type("ufs"), QDL_STORAGE_UFS); assert_int_equal(decode_storage_type("nvme"), QDL_STORAGE_NVME); assert_int_equal(decode_storage_type("bogus"), QDL_STORAGE_UNKNOWN); assert_int_equal(decode_storage_type(NULL), QDL_STORAGE_UNKNOWN); assert_string_equal(encode_storage_type(QDL_STORAGE_UFS), "ufs"); assert_null(encode_storage_type((enum qdl_storage_type)999)); for (i = 0; i < ARRAY_SIZE(types); i++) assert_int_equal(decode_storage_type(encode_storage_type(types[i])), types[i]); } /* --- attr_as_* --- */ static void test_attr_accessors(void **state) { xmlDoc *doc; xmlNode *node; int errors = 0; const char *s; (void)state; doc = xmlNewDoc((const xmlChar *)"1.0"); node = xmlNewNode(NULL, (const xmlChar *)"n"); xmlNewProp(node, (const xmlChar *)"num", (const xmlChar *)"42"); xmlNewProp(node, (const xmlChar *)"str", (const xmlChar *)"hello"); xmlNewProp(node, (const xmlChar *)"empty", (const xmlChar *)""); xmlNewProp(node, (const xmlChar *)"flag", (const xmlChar *)"true"); xmlNewProp(node, (const xmlChar *)"flag0", (const xmlChar *)"false"); /* Present attributes do not raise errors. */ assert_int_equal(attr_as_unsigned(node, "num", &errors), 42); assert_int_equal(errors, 0); s = attr_as_string(node, "str", &errors); assert_non_null(s); assert_string_equal(s, "hello"); assert_int_equal(errors, 0); free((void *)s); assert_true(attr_as_bool(node, "flag", &errors)); assert_false(attr_as_bool(node, "flag0", &errors)); assert_int_equal(errors, 0); /* * A present-but-empty string attribute returns NULL without flagging * an error - callers must treat NULL as "not provided". This is the * behaviour several load paths depend on. */ assert_null(attr_as_string(node, "empty", &errors)); assert_int_equal(errors, 0); /* A missing attribute raises an error via the accessor. */ assert_null(attr_as_string(node, "missing", &errors)); assert_int_equal(errors, 1); errors = 0; assert_int_equal(attr_as_unsigned(node, "missing", &errors), 0); assert_int_equal(errors, 1); /* A missing bool is simply false and raises no error. */ errors = 0; assert_false(attr_as_bool(node, "missing", &errors)); assert_int_equal(errors, 0); xmlFreeNode(node); xmlFreeDoc(doc); } /* --- decode_backend --- */ static void test_decode_backend(void **state) { enum QDL_DEVICE_TYPE t = QDL_DEVICE_SIM; (void)state; assert_int_equal(decode_backend("auto", &t), 0); assert_int_equal(t, QDL_DEVICE_AUTO); /* A NULL name defaults to auto. */ t = QDL_DEVICE_SIM; assert_int_equal(decode_backend(NULL, &t), 0); assert_int_equal(t, QDL_DEVICE_AUTO); assert_int_equal(decode_backend("usb", &t), 0); assert_int_equal(t, QDL_DEVICE_USB); assert_int_equal(decode_backend("qud", &t), 0); assert_int_equal(t, QDL_DEVICE_QUD); assert_int_equal(decode_backend("bogus", &t), -1); } /* --- decode_color_mode --- */ static void test_decode_color_mode(void **state) { enum ux_color_mode m = UX_COLOR_NEVER; (void)state; assert_int_equal(decode_color_mode("auto", &m), 0); assert_int_equal(m, UX_COLOR_AUTO); assert_int_equal(decode_color_mode("never", &m), 0); assert_int_equal(m, UX_COLOR_NEVER); assert_int_equal(decode_color_mode("always", &m), 0); assert_int_equal(m, UX_COLOR_ALWAYS); /* A bare --color (NULL value) means always. */ m = UX_COLOR_NEVER; assert_int_equal(decode_color_mode(NULL, &m), 0); assert_int_equal(m, UX_COLOR_ALWAYS); assert_int_equal(decode_color_mode("bogus", &m), -1); assert_int_equal(decode_color_mode("", &m), -1); } /* --- qdl_split_specifier --- */ static void test_split_specifier(void **state) { char *spec = (char *)0x1; char *file; (void)state; /* Plain filename: no specifier. */ file = qdl_split_specifier("contents.xml", &spec); assert_non_null(file); assert_string_equal(file, "contents.xml"); assert_null(spec); free(file); /* file::selector splits into the two parts. */ file = qdl_split_specifier("contents.xml::ufs,safe", &spec); assert_non_null(file); assert_string_equal(file, "contents.xml"); assert_non_null(spec); assert_string_equal(spec, "ufs,safe"); free(file); /* Malformed: empty, double "::", empty filename, empty selector. */ assert_null(qdl_split_specifier("", &spec)); assert_null(qdl_split_specifier("a::b::c", &spec)); assert_null(qdl_split_specifier("::ufs", &spec)); assert_null(qdl_split_specifier("file::", &spec)); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_parse_addr_partition_only), cmocka_unit_test(test_parse_addr_partition_sector), cmocka_unit_test(test_parse_addr_sector_length), cmocka_unit_test(test_parse_addr_gpt_name), cmocka_unit_test(test_parse_addr_errors), cmocka_unit_test(test_storage_type_roundtrip), cmocka_unit_test(test_attr_accessors), cmocka_unit_test(test_decode_backend), cmocka_unit_test(test_decode_color_mode), cmocka_unit_test(test_split_specifier), }; return cmocka_run_group_tests(tests, NULL, NULL); } linux-msm-qdl-6e625aa/tests/test_ux.c000066400000000000000000000212331527206113100176340ustar00rootroot00000000000000// SPDX-License-Identifier: BSD-3-Clause /* * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. */ /* * Unit tests for the styled printing in ux.c. The module is included * directly to reach ux_vprint(); output goes to a regular file, which is * never a terminal, so the auto mode must leave it plain while the always * mode forces the escape sequences. */ #include #include #include #include #include #include #include #include "../src/ux.c" #include "common.h" bool qdl_debug; static char out_path[512]; static void print_to(FILE *fp, bool *bol, enum ux_style style, const char *fmt, ...) { va_list ap; va_start(ap, fmt); ux_vprint(fp, bol, style, UX_LEVEL_NONE, UX_TAG_NONE, fmt, ap); va_end(ap); } static void print_tagged(FILE *fp, bool *bol, enum ux_style style, enum ux_tag tag, const char *fmt, ...) { va_list ap; va_start(ap, fmt); ux_vprint(fp, bol, style, UX_LEVEL_NONE, tag, fmt, ap); va_end(ap); } static void print_level(FILE *fp, bool *bol, enum ux_style style, enum ux_level level, enum ux_tag tag, const char *fmt, ...) { va_list ap; va_start(ap, fmt); ux_vprint(fp, bol, style, level, tag, fmt, ap); va_end(ap); } /* Open a fresh output file for one test. */ static FILE *open_out(void) { char dir[256]; FILE *fp; assert_int_equal(test_make_temp_dir(dir, sizeof(dir), "qdl-test-ux"), 0); snprintf(out_path, sizeof(out_path), "%s/out.txt", dir); fp = fopen(out_path, "w+b"); assert_non_null(fp); return fp; } /* Read back everything written to @fp, then close and remove it. */ static char *read_out(FILE *fp) { static char buf[1024]; size_t n; fflush(fp); rewind(fp); n = fread(buf, 1, sizeof(buf) - 1, fp); buf[n] = '\0'; fclose(fp); remove(out_path); return buf; } static int setup(void **state) { (void)state; qdl_debug = false; ux_set_color_mode(UX_COLOR_AUTO); return 0; } static void test_plain_when_not_a_terminal(void **state) { bool bol = true; FILE *fp = open_out(); (void)state; /* Auto mode: a regular file is not a terminal, so no escapes. */ print_to(fp, &bol, UX_STYLE_ERROR, "boom %d\n", 7); assert_string_equal(read_out(fp), "boom 7\n"); } static void test_never(void **state) { bool bol = true; FILE *fp = open_out(); (void)state; ux_set_color_mode(UX_COLOR_NEVER); print_to(fp, &bol, UX_STYLE_WARNING, "careful\n"); assert_string_equal(read_out(fp), "careful\n"); } static void test_always_styles_each_line(void **state) { bool bol = true; FILE *fp = open_out(); (void)state; ux_set_color_mode(UX_COLOR_ALWAYS); print_to(fp, &bol, UX_STYLE_ERROR, "one\ntwo\n\nthree\n"); assert_string_equal(read_out(fp), "\033[31mone\033[0m\n" "\033[31mtwo\033[0m\n" "\n" "\033[31mthree\033[0m\n"); } static void test_always_continuation(void **state) { bool bol = true; FILE *fp = open_out(); (void)state; ux_set_color_mode(UX_COLOR_ALWAYS); print_to(fp, &bol, UX_STYLE_LOG, "abc"); assert_false(bol); print_to(fp, &bol, UX_STYLE_LOG, "def\n"); assert_true(bol); assert_string_equal(read_out(fp), "\033[36mabc\033[0m\033[36mdef\033[0m\n"); } static void test_no_style_stays_plain(void **state) { bool bol = true; FILE *fp = open_out(); (void)state; ux_set_color_mode(UX_COLOR_ALWAYS); print_to(fp, &bol, UX_STYLE_NONE, "info\n"); assert_string_equal(read_out(fp), "info\n"); } static void test_debug_prefix_alignment(void **state) { const char *pad = " "; /* strlen("00:00:00.000 ") */ const char *second; bool bol = true; FILE *fp = open_out(); char *out; (void)state; qdl_debug = true; ux_set_color_mode(UX_COLOR_ALWAYS); print_to(fp, &bol, UX_STYLE_NONE, "first\nsecond\n"); out = read_out(fp); /* The timestamp is dimmed... */ assert_memory_equal(out, "\033[2m", 4); assert_non_null(strstr(out, "\033[0m first\n")); /* ...and continuation lines are padded to its width on screen. */ second = strchr(out, '\n') + 1; assert_memory_equal(second, pad, strlen(pad)); assert_string_equal(second + strlen(pad), "second\n"); } static void test_tag_plain(void **state) { bool bol = true; FILE *fp = open_out(); (void)state; /* Without color the tag is the same text the messages used to carry. */ print_tagged(fp, &bol, UX_STYLE_ERROR, UX_TAG_SAHARA, "bad packet %d\n", 3); print_tagged(fp, &bol, UX_STYLE_NONE, UX_TAG_FIREHOSE_RAW, "WRITE: x\n"); assert_string_equal(read_out(fp), "SAHARA: bad packet 3\n" "FIREHOSE_RAW: WRITE: x\n"); } static void test_tag_colored_once(void **state) { bool bol = true; FILE *fp = open_out(); (void)state; /* The tag keeps its own color and leads only the first line. */ ux_set_color_mode(UX_COLOR_ALWAYS); print_tagged(fp, &bol, UX_STYLE_ERROR, UX_TAG_VIP, "one\ntwo\n"); assert_string_equal(read_out(fp), "\033[1;32mVIP:\033[0m \033[31mone\033[0m\n" "\033[31mtwo\033[0m\n"); } static void test_tag_after_debug_prefix(void **state) { bool bol = true; FILE *fp = open_out(); char *out; (void)state; qdl_debug = true; print_tagged(fp, &bol, UX_STYLE_NONE, UX_TAG_USB, "chunk\n"); out = read_out(fp); /* "HH:MM:SS.mmm USB: chunk" */ assert_int_equal(out[12], ' '); assert_string_equal(out + 13, "USB: chunk\n"); } static void test_level_plain(void **state) { bool bol = true; FILE *fp = open_out(); (void)state; print_level(fp, &bol, UX_STYLE_ERROR, UX_LEVEL_ERR, UX_TAG_NONE, "boom\n"); print_level(fp, &bol, UX_STYLE_ERROR, UX_LEVEL_ERR, UX_TAG_SAHARA, "bad packet\n"); print_level(fp, &bol, UX_STYLE_WARNING, UX_LEVEL_WARN, UX_TAG_NONE, "careful\n"); assert_string_equal(read_out(fp), "ERR: boom\n" "ERR: SAHARA: bad packet\n" "WARN: careful\n"); } static void test_level_once_per_line(void **state) { bool bol = true; FILE *fp = open_out(); (void)state; /* A line assembled from several calls carries the level once. */ print_level(fp, &bol, UX_STYLE_ERROR, UX_LEVEL_ERR, UX_TAG_NONE, "qdl: "); print_level(fp, &bol, UX_STYLE_ERROR, UX_LEVEL_ERR, UX_TAG_NONE, "no device\n"); print_level(fp, &bol, UX_STYLE_ERROR, UX_LEVEL_ERR, UX_TAG_NONE, "next\n"); assert_string_equal(read_out(fp), "ERR: qdl: no device\nERR: next\n"); } static void test_level_colored(void **state) { bool bol = true; FILE *fp = open_out(); (void)state; ux_set_color_mode(UX_COLOR_ALWAYS); print_level(fp, &bol, UX_STYLE_ERROR, UX_LEVEL_ERR, UX_TAG_USB, "x\n"); assert_string_equal(read_out(fp), "\033[1;31mERR:\033[0m \033[1;36mUSB:\033[0m \033[31mx\033[0m\n"); } static void test_quiet_prefix(void **state) { enum ux_tag tag; (void)state; /* Regular output stays bare outside debug mode, tag or not. */ tag = UX_TAG_FIREHOSE; assert_int_equal(ux_quiet_prefix(UX_LEVEL_INFO, &tag), UX_LEVEL_NONE); assert_int_equal(tag, UX_TAG_NONE); /* In debug mode the subsystem tag wins, the level fills in for none. */ qdl_debug = true; tag = UX_TAG_FIREHOSE; assert_int_equal(ux_quiet_prefix(UX_LEVEL_INFO, &tag), UX_LEVEL_NONE); assert_int_equal(tag, UX_TAG_FIREHOSE); tag = UX_TAG_NONE; assert_int_equal(ux_quiet_prefix(UX_LEVEL_DEBUG, &tag), UX_LEVEL_DEBUG); assert_int_equal(tag, UX_TAG_NONE); } static void test_format_size(void **state) { char buf[16]; (void)state; assert_string_equal(ux_format_size(buf, sizeof(buf), 0), "0 B"); assert_string_equal(ux_format_size(buf, sizeof(buf), 1023), "1023 B"); assert_string_equal(ux_format_size(buf, sizeof(buf), 1024), "1.0 KiB"); assert_string_equal(ux_format_size(buf, sizeof(buf), 3355443), "3.2 MiB"); assert_string_equal(ux_format_size(buf, sizeof(buf), 536870912), "512.0 MiB"); assert_string_equal(ux_format_size(buf, sizeof(buf), 2254857830ULL), "2.1 GiB"); assert_string_equal(ux_format_size(buf, sizeof(buf), 5ULL << 40), "5.0 TiB"); /* Beyond the largest unit the value just grows. */ assert_string_equal(ux_format_size(buf, sizeof(buf), 2048ULL << 40), "2048.0 TiB"); } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test_setup(test_plain_when_not_a_terminal, setup), cmocka_unit_test_setup(test_never, setup), cmocka_unit_test_setup(test_always_styles_each_line, setup), cmocka_unit_test_setup(test_always_continuation, setup), cmocka_unit_test_setup(test_no_style_stays_plain, setup), cmocka_unit_test_setup(test_debug_prefix_alignment, setup), cmocka_unit_test_setup(test_tag_plain, setup), cmocka_unit_test_setup(test_tag_colored_once, setup), cmocka_unit_test_setup(test_tag_after_debug_prefix, setup), cmocka_unit_test_setup(test_level_plain, setup), cmocka_unit_test_setup(test_level_once_per_line, setup), cmocka_unit_test_setup(test_level_colored, setup), cmocka_unit_test_setup(test_quiet_prefix, setup), cmocka_unit_test_setup(test_format_size, setup), }; return cmocka_run_group_tests(tests, NULL, NULL); } linux-msm-qdl-6e625aa/tests/test_vip_generation.sh000077500000000000000000000035131527206113100224050ustar00rootroot00000000000000#!/bin/bash # SPDX-License-Identifier: BSD-3-Clause set -e SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" while [[ $# -gt 0 ]]; do case "$1" in --builddir) builddir="$2" shift 2 ;; *) echo "Unknown option: $1" >&2 exit 1 ;; esac done if [[ -z "${builddir}" ]]; then echo "Error: --builddir is required." >&2 exit 1 fi DATA_SRC=${SCRIPT_PATH}/data FLAT_BUILD=${builddir}/tests/data-vip # Generate test fixtures in the build directory ${DATA_SRC}/generate_flat_build.sh "${FLAT_BUILD}" cleanup() { rm -rf "${FLAT_BUILD}" } trap cleanup EXIT QDL_PATH=$builddir VIP_PATH=${FLAT_BUILD}/vip EXPECTED_DIGEST="3ca3c745c7bf60d9f51626ad819dce1e14788bc094c258da6656b0a469afbf15" VIP_TABLE_FILE=${VIP_PATH}/DigestsToSign.bin uname_out="$(uname -s)" case "${uname_out}" in Linux*|Darwin*) QDL=qdl ;; CYGWIN*|MINGW*|MSYS*) QDL=qdl.exe ;; *) exit 1 ;; esac mkdir -p $VIP_PATH cd $FLAT_BUILD ${QDL_PATH}/${QDL} --dry-run --create-digests=${VIP_PATH} \ prog_firehose_ddr.elf rawprogram*.xml patch*.xml if command -v sha256sum >/dev/null 2>&1; then shacmd="sha256sum" elif command -v shasum >/dev/null 2>&1; then shacmd="shasum -a 256" else echo "No SHA-256 checksum tool found (need 'sha256sum' or 'shasum')" exit 1 fi actual_digest=`${shacmd} "${VIP_TABLE_FILE}" | cut -d ' ' -f1` if [ "$actual_digest" != "${EXPECTED_DIGEST}" ]; then echo "Expected SHA256 digest of ${VIP_TABLE_FILE} file is ${EXPECTED_DIGEST}" echo "Calculated SHA256 digest of ${VIP_TABLE_FILE} file is $actual_digest" echo "VIP table folder contents:" ls -la ${VIP_PATH} exit 1 fi echo "VIP tables are generated successfully and validated" rm -r ${VIP_PATH}/*.bin rmdir ${VIP_PATH} linux-msm-qdl-6e625aa/udev/000077500000000000000000000000001527206113100155755ustar00rootroot00000000000000linux-msm-qdl-6e625aa/udev/70-qdl.rules.in000066400000000000000000000015111527206113100202600ustar00rootroot00000000000000# SPDX-License-Identifier: BSD-3-Clause # Grant local users access to Qualcomm EDL/crashdump devices used by qdl, # and keep ModemManager from probing them. SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="05c6", ENV{ID_USB_INTERFACES}=="*:ffff10:*", @UDEV_GROUP_ATTRS@TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1" SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="05c6", ENV{ID_USB_INTERFACES}=="*:ffff11:*", @UDEV_GROUP_ATTRS@TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1" SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="05c6", ENV{ID_USB_INTERFACES}=="*:ffff13:*", @UDEV_GROUP_ATTRS@TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1" SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="05c6", ENV{ID_USB_INTERFACES}=="*:ffffff:*", @UDEV_GROUP_ATTRS@TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1"