├── .gitignore ├── Cargo.toml ├── .github └── workflows │ ├── pr-dependency-check.yml │ ├── build.yml │ └── qualcomm-organization-repolinter.yml ├── qramdump ├── Cargo.toml └── src │ └── main.rs ├── cli ├── Cargo.toml └── src │ ├── flasher.rs │ ├── util.rs │ ├── programfile.rs │ └── main.rs ├── SECURITY.md ├── qdl ├── Cargo.toml └── src │ ├── serial.rs │ ├── parsers.rs │ ├── usb.rs │ ├── types.rs │ ├── lib.rs │ └── sahara.rs ├── LICENSE ├── CONTRIBUTING.md ├── CODE-OF-CONDUCT.md ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | /target 3 | .vscode/ 4 | ramdump/ 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cli", "qramdump", "qdl"] 3 | resolver = "2" 4 | -------------------------------------------------------------------------------- /.github/workflows/pr-dependency-check.yml: -------------------------------------------------------------------------------- 1 | name: 'Pull Request Dependency Review' 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependency-review: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 'Checkout Repository' 12 | uses: actions/checkout@v4 13 | - name: 'Dependency Review' 14 | uses: actions/dependency-review-action@v4 15 | with: 16 | fail-on-severity: high -------------------------------------------------------------------------------- /qramdump/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qramdump" 3 | version = "0.1.0" 4 | edition = "2024" 5 | authors = ["Konrad Dybcio "] 6 | license = "BSD-3-Clause" 7 | # description = "XXXXX" 8 | readme = "README.md" 9 | repository = "https://github.com/qualcomm/qdlrs" 10 | categories = ["command-line-utilities"] 11 | publish = false # TODO 12 | 13 | [badges] 14 | maintenance = { status = "actively-developed" } 15 | 16 | [dependencies] 17 | anyhow = "1.0.89" 18 | clap = { version = "4.5.18", features = ["derive"] } 19 | clap-num = "1.1.1" 20 | qdl = { path = "../qdl/", features = ["serial", "usb"] } 21 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qdl-rs" 3 | version = "0.1.0" 4 | edition = "2024" 5 | authors = ["Konrad Dybcio "] 6 | license = "BSD-3-Clause" 7 | # description = "XXXXX" 8 | readme = "README.md" 9 | repository = "https://github.com/qualcomm/qdlrs" 10 | categories = ["command-line-utilities"] 11 | publish = false # TODO 12 | 13 | [badges] 14 | maintenance = { status = "actively-developed" } 15 | 16 | [dependencies] 17 | anyhow = "1.0.89" 18 | clap = { version = "4.5.18", features = ["derive"] } 19 | clap-num = "1.1.1" 20 | qdl = { path = "../qdl/", features = ["serial", "usb"] } 21 | gptman = "1.1.2" 22 | indexmap = "2.5.0" 23 | owo-colors = "4.1.0" 24 | xmltree = { version = "0.11.0", features = ["attribute-order"] } 25 | itertools = "0.14.0" 26 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | How to Report a Potential Vulnerability? 2 | ======================================== 3 | 4 | If you would like to report a public issue (for example, one with a released 5 | CVE number), please report it as a 6 | [GitHub issue](https://github.com/qualcomm/qdlrs/issues/new). 7 | If you have a patch ready, submit it following the same procedure as any 8 | other patch as described in [CONTRIBUTING.md](CONTRIBUTING.md). 9 | 10 | If you are dealing with a not-yet released or urgent issue, please contact us 11 | via our [Product Security team](mailto:product-security@qualcomm.com) or 12 | see our 13 | [Report a Bug](https://www.qualcomm.com/company/product-security/report-a-bug) 14 | page, including as many details as 15 | possible: including the version, and any example code, if available. 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Sanity checks 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUSTFLAGS: "-Dwarnings" 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ macos-latest, ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm ] 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Rust Cache save/restore 25 | uses: Swatinem/rust-cache@v2.7.5 26 | with: 27 | save-if: ${{ github.ref == 'refs/heads/main' }} 28 | 29 | - name: Run rustfmt 30 | run: cargo fmt -- --check 31 | - name: Run Clippy 32 | run: cargo clippy --all-targets --all-features 33 | - name: Build 34 | run: cargo build --verbose 35 | - name: Run tests 36 | run: cargo test --verbose 37 | -------------------------------------------------------------------------------- /qdl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qdl" 3 | version = "0.1.0" 4 | edition = "2024" 5 | authors = ["Konrad Dybcio "] 6 | license = "BSD-3-Clause" 7 | # description = "XXXXX" 8 | readme = "README.md" 9 | repository = "https://github.com/qualcomm/qdlrs" 10 | categories = ["embedded"] 11 | publish = false # TODO 12 | 13 | [badges] 14 | maintenance = { status = "actively-developed" } 15 | 16 | [lib] 17 | name = "qdl" 18 | path = "src/lib.rs" 19 | 20 | [dependencies] 21 | anstream = "0.6.15" 22 | anyhow = "1.0" 23 | bincode = "1.3.3" 24 | indexmap = "2.5.0" 25 | owo-colors = "4.1.0" 26 | pbr = "1.1.1" 27 | rusb = { version = "0.9.4", optional = true } 28 | serde = { version = "1.0.210", features = ["derive"] } 29 | serde_repr = "0.1.19" 30 | serial2 = { version = "0.2.28", optional = true } 31 | xmltree = { version = "0.11.0", features = ["attribute-order"] } 32 | 33 | [features] 34 | serial = ["dep:serial2"] 35 | usb = ["dep:rusb"] 36 | -------------------------------------------------------------------------------- /.github/workflows/qualcomm-organization-repolinter.yml: -------------------------------------------------------------------------------- 1 | name: Qualcomm Organization Repolinter 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | repolinter: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout Repo 10 | uses: actions/checkout@v4 11 | - name: Verify repolinter config file is present 12 | id: check_files 13 | uses: andstor/file-existence-action@v3 14 | with: 15 | files: "repolint.json" 16 | - name: Run Repolinter with local repolint.json 17 | if: steps.check_files.outputs.files_exists == 'true' 18 | uses: todogroup/repolinter-action@v1 19 | with: 20 | config_file: "repolint.json" 21 | - name: Run Repolinter with default ruleset 22 | if: steps.check_files.outputs.files_exists == 'false' 23 | uses: todogroup/repolinter-action@v1 24 | with: 25 | config_url: "https://raw.githubusercontent.com/qualcomm/.github/main/repolint.json" 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-2017, Linaro Ltd. 3 | * Copyright (c) 2016, Bjorn Andersson 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its contributors 17 | * may be used to endorse or promote products derived from this software without 18 | * specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | */ 32 | -------------------------------------------------------------------------------- /qramdump/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 3 | use std::str::FromStr; 4 | 5 | use anyhow::{Result, bail}; 6 | 7 | use clap::{Parser, command}; 8 | use qdl::{ 9 | self, 10 | sahara::{SaharaMode, sahara_reset, sahara_run}, 11 | setup_target_device, 12 | types::{FirehoseConfiguration, QdlBackend, QdlDevice}, 13 | }; 14 | 15 | #[derive(Parser, Debug)] 16 | #[command(version, about, long_about = None)] 17 | struct Args { 18 | #[arg(long, value_name = "usb/serial")] 19 | backend: Option, 20 | 21 | #[arg(short, long, help = "E.g. COM4 on Windows")] 22 | dev_path: Option, 23 | 24 | #[arg()] 25 | regions_to_dump: Vec, 26 | 27 | // Only applies to the USB backend 28 | #[arg(long)] 29 | serial_no: Option, 30 | 31 | #[arg(long, default_value = "false")] 32 | verbose_sahara: bool, 33 | } 34 | 35 | pub fn main() -> Result<()> { 36 | let args = Args::parse(); 37 | let backend = match args.backend { 38 | Some(b) => QdlBackend::from_str(&b)?, 39 | None => QdlBackend::default(), 40 | }; 41 | 42 | let rw_channel = match setup_target_device(backend, args.serial_no, args.dev_path) { 43 | Ok(c) => c, 44 | Err(e) => bail!("Couldn't set up device: {}", e.to_string()), 45 | }; 46 | 47 | let mut qdl_dev = QdlDevice { 48 | rw: rw_channel, 49 | fh_cfg: FirehoseConfiguration::default(), 50 | reset_on_drop: false, 51 | }; 52 | 53 | sahara_run( 54 | &mut qdl_dev, 55 | SaharaMode::MemoryDebug, 56 | None, 57 | &mut [], 58 | args.regions_to_dump, 59 | args.verbose_sahara, 60 | )?; 61 | 62 | sahara_reset(&mut qdl_dev)?; 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /cli/src/flasher.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 3 | use anyhow::{Result, bail}; 4 | use programfile::parse_program_xml; 5 | use qdl::firehose_set_bootable; 6 | use qdl::types::QdlChan; 7 | 8 | use std::fs::{self}; 9 | use std::path::Path; 10 | 11 | use crate::programfile; 12 | 13 | /// Iterates through program/patch files and executes the instructions therein. 14 | pub(crate) fn run_flash( 15 | channel: &mut T, 16 | program_file_paths: Vec, 17 | patch_file_paths: Vec, 18 | verbose: bool, 19 | ) -> Result<()> { 20 | // Check if the required files are present 21 | let file_paths = [&program_file_paths[..], &patch_file_paths[..]].concat(); 22 | if let Some(f) = file_paths.iter().find(|f| !Path::new(f).is_file()) { 23 | bail!("{} doesn't exist", f); 24 | } 25 | let tmp_path_string = match cfg!(target_os = "windows") { 26 | true => "C:\\Temp\\", 27 | false => "/tmp/out/", 28 | }; 29 | 30 | let mut bootable_part_idx: Option = None; 31 | for program_file_path in file_paths { 32 | let path = Path::new(&program_file_path); 33 | if !path.is_file() { 34 | bail!("Program file doesn't exist"); 35 | } 36 | 37 | // Get the program files that we need 38 | let program_file_dir = path.parent().unwrap(); 39 | let program_file = fs::read(path)?; 40 | let xml = xmltree::Element::parse(&program_file[..])?; 41 | 42 | // Parse the program/patch XMLs and flash away 43 | if let Some(n) = parse_program_xml( 44 | channel, 45 | &xml, 46 | program_file_dir, 47 | Path::new(tmp_path_string), // TODO 48 | true, // TODO 49 | verbose, 50 | )? { 51 | bootable_part_idx = Some(n) 52 | }; 53 | } 54 | 55 | // Mark the correct LUN (or any other kind of physical partition) as bootable 56 | if bootable_part_idx.is_some() { 57 | println!( 58 | "Setting partition {} as bootable!", 59 | bootable_part_idx.unwrap() 60 | ); 61 | firehose_set_bootable(channel, bootable_part_idx.unwrap())?; 62 | } 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /qdl/src/serial.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 3 | use anyhow::{Result, bail}; 4 | use serial2::{self, SerialPort}; 5 | use std::io::{BufRead, Read, Write}; 6 | 7 | use crate::types::QdlReadWrite; 8 | 9 | pub struct QdlSerialConfig { 10 | serport: SerialPort, 11 | buf: Vec, 12 | pos: usize, 13 | cap: usize, 14 | } 15 | 16 | // TODO: timeouts? 17 | impl Write for QdlSerialConfig { 18 | fn write(&mut self, buf: &[u8]) -> Result { 19 | self.serport.write(buf) 20 | } 21 | fn flush(&mut self) -> Result<(), std::io::Error> { 22 | self.serport.flush() 23 | } 24 | } 25 | 26 | impl Read for QdlSerialConfig { 27 | fn read(&mut self, out: &mut [u8]) -> Result { 28 | // Drain internal buffer first 29 | if self.pos < self.cap { 30 | let n = std::cmp::min(out.len(), self.cap - self.pos); 31 | out[..n].copy_from_slice(&self.buf[self.pos..self.pos + n]); 32 | self.pos += n; 33 | return Ok(n); 34 | } 35 | // Otherwise, read directly from serial port 36 | self.serport.read(out) 37 | } 38 | } 39 | 40 | impl BufRead for QdlSerialConfig { 41 | fn fill_buf(&mut self) -> Result<&[u8], std::io::Error> { 42 | if self.pos >= self.cap { 43 | self.pos = 0; 44 | self.cap = 0; 45 | if self.buf.is_empty() { 46 | self.buf.resize(4096, 0); 47 | } 48 | match self.serport.read(&mut self.buf) { 49 | Ok(n) => { 50 | self.cap = n; 51 | } 52 | Err(e) => return Err(e), 53 | } 54 | } 55 | Ok(&self.buf[self.pos..self.cap]) 56 | } 57 | 58 | fn consume(&mut self, amt: usize) { 59 | self.pos = std::cmp::min(self.pos + amt, self.cap); 60 | } 61 | } 62 | 63 | impl QdlReadWrite for QdlSerialConfig {} 64 | 65 | pub fn setup_serial_device(dev_path: Option) -> Result { 66 | if dev_path.is_none() { 67 | bail!("Serial port path unspecified"); 68 | } 69 | 70 | let serport = SerialPort::open(dev_path.unwrap(), |mut settings: serial2::Settings| { 71 | settings.set_raw(); 72 | settings.set_baud_rate(115200)?; 73 | Ok(settings) 74 | })?; 75 | 76 | Ok(QdlSerialConfig { 77 | serport, 78 | buf: Vec::new(), 79 | pos: 0, 80 | cap: 0, 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to qdlrs 2 | 3 | Hi there! 4 | We’re thrilled that you’d like to contribute to this project. 5 | Your help is essential for keeping this project great and for making it better. 6 | 7 | ## Branching Strategy 8 | 9 | In general, contributors should develop on branches based off of `main` and pull requests should be made against `main`. 10 | 11 | ## Submitting a pull request 12 | 13 | 1. Please read our [code of conduct](CODE-OF-CONDUCT.md) and [license](LICENSE.txt). 14 | 1. [Fork](https://github.com/qualcomm/qdlrs/fork) and clone the repository. 15 | 16 | ```bash 17 | git clone https://github.com//qdlrs.git 18 | ``` 19 | 20 | 1. Create a new branch based on `main`: 21 | 22 | ```bash 23 | git checkout -b main 24 | ``` 25 | 26 | 1. Create an upstream `remote` to make it easier to keep your branches up-to-date: 27 | 28 | ```bash 29 | git remote add upstream https://github.com/quic/qdlrs.git 30 | ``` 31 | 32 | 1. Make your changes, add tests, and make sure the tests still pass. 33 | 1. Commit your changes using the [DCO](https://developercertificate.org/). You can attest to the DCO by commiting with the **-s** or **--signoff** options or manually adding the "Signed-off-by": 34 | 35 | ```bash 36 | git commit -s -m "Really useful commit message"` 37 | ``` 38 | 39 | 1. After committing your changes on the topic branch, sync it with the upstream branch: 40 | 41 | ```bash 42 | git pull --rebase upstream main 43 | ``` 44 | 45 | 1. Push to your fork. 46 | 47 | ```bash 48 | git push -u origin 49 | ``` 50 | 51 | The `-u` is shorthand for `--set-upstream`. This will set up the tracking reference so subsequent runs of `git push` or `git pull` can omit the remote and branch. 52 | 53 | 1. [Submit a pull request](https://github.com/qualcomm/qdlrs/pulls) from your branch to `main`. 54 | 1. Pat yourself on the back and wait for your pull request to be reviewed. 55 | 56 | Here are a few things you can do that will increase the likelihood of your pull request to be accepted: 57 | 58 | - Follow the existing style where possible. **INSERT LINK TO STYLE, e.g. PEP8 for python** 59 | - Write tests. 60 | - Keep your change as focused as possible. 61 | If you want to make multiple independent changes, please consider submitting them as separate pull requests. 62 | - Write a [good commit message](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 63 | - It's a good idea to arrange a discussion with other developers to ensure there is consensus on large features, architecture changes, and other core code changes. PR reviews will go much faster when there are no surprises. 64 | -------------------------------------------------------------------------------- /cli/src/util.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 3 | use anyhow::{Result, bail}; 4 | use gptman::{self, GPT, GPTHeader, GPTPartitionEntry}; 5 | use owo_colors::OwoColorize; 6 | use std::io::{Cursor, Error, ErrorKind, Seek, Write}; 7 | 8 | use qdl::{self, firehose_read_storage, types::QdlChan}; 9 | 10 | pub fn read_gpt_from_storage( 11 | channel: &mut T, 12 | slot: u8, 13 | phys_part_idx: u8, 14 | ) -> Result { 15 | let mut buf = Cursor::new(Vec::::new()); 16 | 17 | // First, probe sector 1 to retrieve the GPT size 18 | // Note, sector 0 contains a fake MBR as per the GPT spec ("Protective MBR") 19 | firehose_read_storage(channel, &mut buf, 1, slot, phys_part_idx, 1)?; 20 | 21 | buf.rewind()?; 22 | let header = match GPTHeader::read_from(&mut buf) { 23 | Ok(h) => h, 24 | Err(e) => bail!("Couldn't parse the GPT header: {}", e), 25 | }; 26 | 27 | // The entire primary GPT is located between sectors 0 and first_usable_lba 28 | let gpt_len = header.first_usable_lba as usize; 29 | 30 | // Then, read the entire GPT and parse it 31 | buf.rewind()?; 32 | firehose_read_storage(channel, &mut buf, gpt_len, slot, phys_part_idx, 0)?; 33 | 34 | // Ignore the aforementioned MBR sector 35 | buf.set_position(channel.fh_config().storage_sector_size as u64); 36 | GPT::read_from(&mut buf, channel.fh_config().storage_sector_size as u64).map_err(|e| e.into()) 37 | } 38 | 39 | pub fn find_part( 40 | channel: &mut T, 41 | name: &str, 42 | slot: u8, 43 | phys_part_idx: u8, 44 | ) -> Result { 45 | match read_gpt_from_storage(channel, slot, phys_part_idx)? 46 | .iter() 47 | .find(|(_, p)| p.partition_name.to_string() == name) 48 | { 49 | Some(p) => Ok(p.1.clone()), 50 | None => Err(Error::from(ErrorKind::NotFound).into()), 51 | } 52 | } 53 | 54 | pub fn print_partition_table( 55 | channel: &mut T, 56 | slot: u8, 57 | phys_part_idx: u8, 58 | ) -> Result<()> { 59 | let gpt = read_gpt_from_storage(channel, slot, phys_part_idx)?; 60 | 61 | println!( 62 | "GPT on physical partition {} of {}:", 63 | phys_part_idx.bright_yellow(), 64 | channel.fh_config().storage_type.to_string().bright_yellow() 65 | ); 66 | 67 | for (idx, part) in gpt.iter() { 68 | let size = part.size(); 69 | 70 | println!( 71 | "{}] {}: start_sector = {}, {} bytes ({} kiB)", 72 | idx, 73 | part.partition_name.as_str(), 74 | part.starting_lba, 75 | match size { 76 | Ok(s) => (s * gpt.sector_size).to_string(), 77 | Err(_) => "ERROR".to_string(), 78 | }, 79 | match size { 80 | Ok(s) => (s * gpt.sector_size / 1024).to_string(), 81 | Err(_) => "ERROR".to_string(), 82 | } 83 | ); 84 | } 85 | 86 | Ok(()) 87 | } 88 | 89 | pub fn read_storage_logical_partition( 90 | channel: &mut T, 91 | out: &mut impl Write, 92 | name: &str, 93 | slot: u8, 94 | phys_part_idx: u8, 95 | ) -> Result<()> { 96 | let gpt = read_gpt_from_storage(channel, slot, phys_part_idx)?; 97 | 98 | let part = gpt 99 | .iter() 100 | .find(|(_, p)| p.partition_name.as_str() == name) 101 | .ok_or(Error::from(ErrorKind::NotFound))? 102 | .1; 103 | 104 | firehose_read_storage( 105 | channel, 106 | out, 107 | (part.ending_lba - part.starting_lba + 1) as usize, 108 | slot, 109 | phys_part_idx, 110 | part.starting_lba as u32, 111 | ) 112 | } 113 | -------------------------------------------------------------------------------- /qdl/src/parsers.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 3 | 4 | use indexmap::IndexMap; 5 | 6 | use anyhow::bail; 7 | use owo_colors::OwoColorize; 8 | 9 | use crate::{ 10 | FirehoseResetMode, FirehoseStatus, QdlChan, firehose_configure, firehose_read, firehose_reset, 11 | }; 12 | 13 | /// The highest protocol version currently supported by the library 14 | const FH_PROTO_VERSION_SUPPORTED: u32 = 1; 15 | 16 | // Parsers are kept separate for more flexibility (e.g. log replay analysis) 17 | 18 | /// Check "value" for ack/nak (generic) 19 | pub fn firehose_parser_ack_nak( 20 | _: &mut T, 21 | attrs: &IndexMap, 22 | ) -> Result { 23 | let val = attrs.get("value").to_owned(); 24 | match &val.unwrap()[..] { 25 | "ACK" => Ok(FirehoseStatus::Ack), 26 | "NAK" => Ok(FirehoseStatus::Nak), 27 | _ => bail!("Got malformed data: {:?}", attrs), 28 | } 29 | } 30 | 31 | /// Parse the \ response 32 | pub fn firehose_parser_configure_response( 33 | channel: &mut T, 34 | attrs: &IndexMap, 35 | ) -> Result { 36 | if let Ok(status) = firehose_parser_ack_nak(channel, attrs) { 37 | // The device can't handle that big of a buffer and it auto-reconfigures to the max it can 38 | if status == FirehoseStatus::Nak { 39 | if let Some(val) = attrs.get("MaxPayloadSizeToTargetInBytes").to_owned() { 40 | channel.mut_fh_config().send_buffer_size = val.parse::().unwrap(); 41 | } else { 42 | firehose_reset(channel, &FirehoseResetMode::ResetToEdl, 0)?; 43 | bail!("firehose failed, try again with --verbose-firehose") 44 | } 45 | } 46 | } 47 | 48 | let device_max_write_payload_size = attrs 49 | .get("MaxPayloadSizeToTargetInBytesSupported") 50 | .unwrap() 51 | .parse::() 52 | .unwrap(); 53 | 54 | // TODO: define version of the spec we support and validate it 55 | let version = attrs.get("Version").unwrap(); 56 | let min_version_supported = attrs 57 | .get("MinVersionSupported") 58 | .unwrap() 59 | .parse::() 60 | .unwrap(); 61 | 62 | println!("Found protocol version {}", version.bright_blue()); 63 | 64 | if min_version_supported < FH_PROTO_VERSION_SUPPORTED { 65 | bail!( 66 | "Device requires protocol version >= {}, the library only supports up to v{}", 67 | min_version_supported.bright_red(), 68 | FH_PROTO_VERSION_SUPPORTED.bright_blue() 69 | ); 70 | } 71 | 72 | // TODO: MaxPayloadSizeFromTargetInBytes seems useless when xfers are abstracted through libusb 73 | // TODO: ^ is usually 1kiB (reaaally small), newer (citation needed) devices don't advertise it 74 | 75 | channel.mut_fh_config().xml_buf_size = attrs 76 | .get("MaxXMLSizeInBytes") 77 | .unwrap() 78 | .parse::() 79 | .unwrap(); 80 | channel.mut_fh_config().send_buffer_size = attrs 81 | .get("MaxPayloadSizeToTargetInBytes") 82 | .unwrap() 83 | .parse::() 84 | .unwrap(); 85 | 86 | // If the device can take a larger buffer, reconfigure it. 87 | if channel.fh_config().send_buffer_size < device_max_write_payload_size { 88 | println!( 89 | "Reconfiguring the device to use a larger ({}kB) send buffer", 90 | device_max_write_payload_size / 1024 91 | ); 92 | 93 | channel.mut_fh_config().send_buffer_size = device_max_write_payload_size; 94 | firehose_configure(channel, true)?; 95 | firehose_read(channel, firehose_parser_ack_nak)?; 96 | } 97 | 98 | Ok(FirehoseStatus::Ack) 99 | } 100 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [GitHub.CoC](mailto:github.coc@qti.qualcomm.com?subject=GitHub%20Qualcomm%20Code%20of%20Conduct%20Report). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /qdl/src/usb.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 3 | use anyhow::{Context, Result, bail}; 4 | use rusb::{self, Device, DeviceHandle, GlobalContext}; 5 | use std::{ 6 | io::{BufRead, Error, ErrorKind, Read, Write}, 7 | time::Duration, 8 | }; 9 | 10 | use crate::types::QdlReadWrite; 11 | 12 | pub struct QdlUsbConfig { 13 | dev_handle: rusb::DeviceHandle, 14 | in_ep: u8, 15 | out_ep: u8, 16 | buf: Vec, 17 | pos: usize, 18 | cap: usize, 19 | } 20 | 21 | // TODO: timeouts? 22 | impl Write for QdlUsbConfig { 23 | fn write(&mut self, buf: &[u8]) -> Result { 24 | self.dev_handle 25 | .write_bulk(self.out_ep, buf, Duration::from_secs(10)) 26 | .map_err(rusb_err_xlate) 27 | } 28 | 29 | fn flush(&mut self) -> Result<(), std::io::Error> { 30 | Ok(()) 31 | } 32 | } 33 | impl Read for QdlUsbConfig { 34 | fn read(&mut self, out: &mut [u8]) -> Result { 35 | // Drain internal buffer first 36 | if self.pos < self.cap { 37 | let n = std::cmp::min(out.len(), self.cap - self.pos); 38 | out[..n].copy_from_slice(&self.buf[self.pos..self.pos + n]); 39 | self.pos += n; 40 | return Ok(n); 41 | } 42 | // Otherwise, read directly from USB 43 | self.dev_handle 44 | .read_bulk(self.in_ep, out, Duration::from_secs(10)) 45 | .map_err(rusb_err_xlate) 46 | } 47 | } 48 | 49 | impl BufRead for QdlUsbConfig { 50 | fn fill_buf(&mut self) -> Result<&[u8], std::io::Error> { 51 | if self.pos >= self.cap { 52 | self.pos = 0; 53 | self.cap = 0; 54 | if self.buf.is_empty() { 55 | self.buf.resize(4096, 0); 56 | } 57 | match self 58 | .dev_handle 59 | .read_bulk(self.in_ep, &mut self.buf, Duration::from_secs(10)) 60 | { 61 | Ok(n) => { 62 | self.cap = n; 63 | } 64 | Err(e) => return Err(rusb_err_xlate(e)), 65 | } 66 | } 67 | Ok(&self.buf[self.pos..self.cap]) 68 | } 69 | 70 | fn consume(&mut self, amt: usize) { 71 | self.pos = std::cmp::min(self.pos + amt, self.cap); 72 | } 73 | } 74 | 75 | impl QdlReadWrite for QdlUsbConfig {} 76 | 77 | const USB_VID_QCOM: u16 = 0x05c6; 78 | const USB_PID_EDL: [u16; 2] = [0x9008 /* EDL */, 0x900e /* Ramdump */]; 79 | const INTF_DESC_PROTO_CODES: [u8; 3] = [0x10, 0x11, 0xFF]; 80 | 81 | fn find_usb_handle_by_sn( 82 | devices: &mut dyn Iterator>, 83 | serial_no: String, 84 | ) -> Result> { 85 | let mut dev_handle: Option> = None; 86 | 87 | for d in devices { 88 | let dh = d.open()?; 89 | 90 | let prod_str = dh.read_product_string_ascii(&d.device_descriptor().unwrap())?; 91 | let sn = &prod_str[prod_str.find("_SN:").unwrap() + "_SN:".len()..]; 92 | if sn.eq_ignore_ascii_case(&serial_no) { 93 | dev_handle = Some(dh); 94 | break; 95 | } 96 | } 97 | 98 | match dev_handle { 99 | Some(h) => Ok(h), 100 | None => bail!( 101 | "Found no devices in EDL mode with serial number {}", 102 | serial_no 103 | ), 104 | } 105 | } 106 | 107 | pub fn setup_usb_device(serial_no: Option) -> Result { 108 | let rusb_devices = rusb::devices()?; 109 | let mut devices = rusb_devices 110 | .iter() 111 | .filter(|d: &rusb::Device| { 112 | d.device_descriptor().unwrap().vendor_id() == USB_VID_QCOM 113 | && USB_PID_EDL.contains(&d.device_descriptor().unwrap().product_id()) 114 | }); 115 | 116 | let dev_handle = match serial_no { 117 | Some(s) => find_usb_handle_by_sn(&mut devices, s), 118 | None => { 119 | let Some(d) = devices.next() else { 120 | bail!("Found no devices in EDL mode") 121 | }; 122 | d.open().map_err(|e| rusb_err_xlate(e).into()) 123 | } 124 | }?; 125 | 126 | // TODO: is there always precisely one interface like this? 127 | let cfg_desc = dev_handle.device().active_config_descriptor()?; 128 | let intf_desc = cfg_desc 129 | .interfaces() 130 | .next() 131 | .unwrap() 132 | .descriptors() 133 | .find(|d| { 134 | d.class_code() == 0xFF 135 | && d.sub_class_code() == 0xFF 136 | && INTF_DESC_PROTO_CODES.contains(&d.protocol_code()) 137 | && d.num_endpoints() >= 2 138 | }) 139 | .ok_or::(Error::from(ErrorKind::NotFound).into())?; 140 | 141 | let in_ep = intf_desc 142 | .endpoint_descriptors() 143 | .find(|e| { 144 | e.direction() == rusb::Direction::In && e.transfer_type() == rusb::TransferType::Bulk 145 | }) 146 | .unwrap() 147 | .address(); 148 | let out_ep = intf_desc 149 | .endpoint_descriptors() 150 | .find(|e| { 151 | e.direction() == rusb::Direction::Out && e.transfer_type() == rusb::TransferType::Bulk 152 | }) 153 | .unwrap() 154 | .address(); 155 | 156 | // Make sure we can actually poke at the device 157 | dev_handle.set_auto_detach_kernel_driver(true).ok(); 158 | dev_handle 159 | .claim_interface(intf_desc.interface_number()) 160 | .with_context(|| format!("Couldn't claim interface{}", intf_desc.interface_number()))?; 161 | Ok(QdlUsbConfig { 162 | dev_handle, 163 | in_ep, 164 | out_ep, 165 | buf: Vec::new(), 166 | pos: 0, 167 | cap: 0, 168 | }) 169 | } 170 | 171 | // TODO: fix this upstream? 172 | pub fn rusb_err_xlate(e: rusb::Error) -> std::io::Error { 173 | std::io::Error::from(match e { 174 | rusb::Error::Timeout => std::io::ErrorKind::TimedOut, 175 | rusb::Error::Access => std::io::ErrorKind::PermissionDenied, 176 | rusb::Error::NoDevice => std::io::ErrorKind::NotConnected, 177 | _ => std::io::ErrorKind::Other, 178 | }) 179 | } 180 | -------------------------------------------------------------------------------- /qdl/src/types.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 3 | use std::{ 4 | fmt::Display, 5 | io::{BufRead, ErrorKind, Read, Write}, 6 | str::FromStr, 7 | }; 8 | 9 | use anyhow::{Error, bail}; 10 | use owo_colors::OwoColorize; 11 | 12 | use crate::firehose_reset; 13 | 14 | /// Common respones indicating success/failure respectively 15 | #[derive(Copy, Clone, Debug, PartialEq)] 16 | #[repr(u32)] 17 | pub enum FirehoseStatus { 18 | Ack = 0, 19 | Nak = 1, 20 | } 21 | 22 | #[derive(Clone, Copy, Debug, PartialEq)] 23 | pub enum QdlBackend { 24 | Serial, 25 | Usb, 26 | } 27 | 28 | impl FromStr for QdlBackend { 29 | type Err = anyhow::Error; 30 | 31 | fn from_str(s: &str) -> Result { 32 | match s { 33 | "serial" => Ok(QdlBackend::Serial), 34 | "usb" => Ok(QdlBackend::Usb), 35 | _ => bail!("Unknown backend"), 36 | } 37 | } 38 | } 39 | 40 | impl Default for QdlBackend { 41 | fn default() -> Self { 42 | match cfg!(target_os = "windows") { 43 | true => QdlBackend::Serial, 44 | false => QdlBackend::Usb, 45 | } 46 | } 47 | } 48 | 49 | #[derive(Clone, Copy, Debug)] 50 | pub struct FirehoseConfiguration { 51 | // send/recv are from Host PoV 52 | pub send_buffer_size: usize, 53 | pub recv_buffer_size: usize, 54 | pub xml_buf_size: usize, 55 | 56 | pub storage_sector_size: usize, 57 | pub storage_type: FirehoseStorageType, 58 | 59 | pub bypass_storage: bool, 60 | pub hash_packets: bool, 61 | pub read_back_verify: bool, 62 | 63 | pub backend: QdlBackend, 64 | pub skip_usb_zlp: bool, 65 | pub skip_firehose_log: bool, 66 | pub verbose_firehose: bool, 67 | } 68 | 69 | impl Default for FirehoseConfiguration { 70 | fn default() -> Self { 71 | Self { 72 | send_buffer_size: 1024 * 1024, 73 | recv_buffer_size: 4096, 74 | xml_buf_size: 4096, 75 | storage_sector_size: 512, 76 | storage_type: FirehoseStorageType::Emmc, 77 | bypass_storage: true, 78 | hash_packets: false, 79 | read_back_verify: false, 80 | backend: QdlBackend::default(), 81 | // https://github.com/libusb/libusb/pull/678 82 | skip_usb_zlp: cfg!(target_os = "macos"), 83 | skip_firehose_log: true, 84 | verbose_firehose: false, 85 | } 86 | } 87 | } 88 | pub trait QdlChan: BufRead + Write { 89 | fn fh_config(&self) -> &FirehoseConfiguration; 90 | fn mut_fh_config(&mut self) -> &mut FirehoseConfiguration; 91 | } 92 | 93 | pub trait QdlReadWrite: BufRead + Write + Send + Sync {} 94 | impl QdlReadWrite for &mut T where T: QdlReadWrite + ?Sized {} 95 | 96 | pub struct QdlDevice 97 | where 98 | T: QdlReadWrite + ?Sized, 99 | { 100 | pub rw: Box, 101 | pub fh_cfg: FirehoseConfiguration, 102 | pub reset_on_drop: bool, 103 | } 104 | 105 | impl Read for QdlDevice 106 | where 107 | T: QdlReadWrite + ?Sized, 108 | { 109 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 110 | self.rw.read(buf) 111 | } 112 | } 113 | 114 | impl Write for QdlDevice 115 | where 116 | T: QdlReadWrite + ?Sized, 117 | { 118 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 119 | self.rw.write(buf) 120 | } 121 | 122 | fn flush(&mut self) -> std::io::Result<()> { 123 | self.rw.flush() 124 | } 125 | } 126 | 127 | impl std::io::BufRead for QdlDevice 128 | where 129 | T: QdlReadWrite + ?Sized, 130 | { 131 | fn fill_buf(&mut self) -> std::io::Result<&[u8]> { 132 | self.rw.fill_buf() 133 | } 134 | 135 | fn consume(&mut self, amt: usize) { 136 | self.rw.consume(amt); 137 | } 138 | } 139 | 140 | impl QdlChan for QdlDevice 141 | where 142 | T: QdlReadWrite + ?Sized, 143 | { 144 | fn fh_config(&self) -> &FirehoseConfiguration { 145 | &self.fh_cfg 146 | } 147 | 148 | fn mut_fh_config(&mut self) -> &mut FirehoseConfiguration { 149 | &mut self.fh_cfg 150 | } 151 | } 152 | 153 | impl Drop for QdlDevice 154 | where 155 | T: QdlReadWrite + ?Sized, 156 | { 157 | fn drop(&mut self) { 158 | // Avoid having the board be stuck in EDL limbo in case of errors 159 | // TODO: watch 'rawmode' and adjust accordingly 160 | if self.reset_on_drop { 161 | println!( 162 | "Firehose {}. Resetting the board to {}, try again.", 163 | "failed".bright_red(), 164 | "edl".bright_yellow() 165 | ); 166 | let _ = firehose_reset(self, &FirehoseResetMode::ResetToEdl, 0); 167 | } 168 | } 169 | } 170 | 171 | /// Supported storage media types 172 | #[derive(Clone, Copy, Debug)] 173 | pub enum FirehoseStorageType { 174 | Emmc, 175 | Ufs, 176 | Nand, 177 | Nvme, 178 | Spinor, 179 | } 180 | 181 | impl FromStr for FirehoseStorageType { 182 | type Err = Error; 183 | 184 | fn from_str(input: &str) -> Result { 185 | match input { 186 | "emmc" => Ok(FirehoseStorageType::Emmc), 187 | "ufs" => Ok(FirehoseStorageType::Ufs), 188 | "nand" => Ok(FirehoseStorageType::Nand), 189 | "nvme" => Ok(FirehoseStorageType::Nvme), 190 | "spinor" => Ok(FirehoseStorageType::Spinor), 191 | _ => bail!("Unknown storage type"), 192 | } 193 | } 194 | } 195 | 196 | impl Display for FirehoseStorageType { 197 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 198 | match self { 199 | FirehoseStorageType::Emmc => write!(f, "emmc"), 200 | FirehoseStorageType::Ufs => write!(f, "ufs"), 201 | FirehoseStorageType::Nand => write!(f, "nand"), 202 | FirehoseStorageType::Nvme => write!(f, "nvme"), 203 | FirehoseStorageType::Spinor => write!(f, "spinor"), 204 | } 205 | } 206 | } 207 | 208 | /// List of supported reboot modes, supplied to the \ command 209 | pub enum FirehoseResetMode { 210 | ResetToEdl, 211 | Reset, 212 | Off, 213 | } 214 | 215 | impl FromStr for FirehoseResetMode { 216 | type Err = anyhow::Error; 217 | 218 | fn from_str(s: &str) -> Result { 219 | match s { 220 | "edl" => Ok(FirehoseResetMode::ResetToEdl), 221 | "system" => Ok(FirehoseResetMode::Reset), 222 | "off" => Ok(FirehoseResetMode::Off), 223 | _ => Err(std::io::Error::from(ErrorKind::InvalidInput).into()), 224 | } 225 | } 226 | } 227 | 228 | impl Display for FirehoseResetMode { 229 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 230 | match self { 231 | FirehoseResetMode::ResetToEdl => write!(f, "edl"), 232 | FirehoseResetMode::Reset => write!(f, "system"), 233 | FirehoseResetMode::Off => write!(f, "off"), 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qdl - Sahara / Firehose tools, in Rust! 2 | 3 | Qualcomm SoCs feature the Emergency Download Mode (EDL, widely known as '9008 mode'), a bootrom-initiated device flashing stack. 4 | 5 | `qdl` provides a Rust implementation for the Sahara and Firehose protocols that are used to communicate with a device in that mode. 6 | 7 | ## Contents 8 | ``` 9 | cli/ - A CLI tool used to communicate with devices in EDL mode 10 | qdl/ - Sahara / Firehose library, with USB convenience wrappers 11 | qramdump/ - Tool to receive memory dumps from a crashed device 12 | ``` 13 | 14 | ## Building 15 | You're expected to have a recent installation of Rust. You can acquire one with [rustup](https://rustup.rs). 16 |
If you already have an older installation, try `rustup update`. 17 | 18 | Run `cargo build [--release]` to build all executables within this repo. The binaries will appear in `target/debug/` or `target/release`, respectively. 19 | 20 | Use `cargo run [--release] --bin [-- args]` to quickly build one of the programs from source and run it. 21 | 22 | ## Running the programs 23 |
24 | 25 | qdl-rs 26 | 27 | ``` 28 | Usage: qdl-rs [OPTIONS] --loader-path --storage-type 29 | 30 | Commands: 31 | dump Dump the entire storage 32 | dump-part Dump a single partition 33 | flasher Invoke the flasher 34 | erase Erase a partition 35 | nop Ask the device to do nothing, hopefully successfully 36 | overwrite-storage Overwrite the storage physical partition contents with a raw image Similar to Flasher, but this one only takes a partition dump as input and performs no real validation on the input data 37 | peek Peek at memory 38 | print-gpt Print the GPT table 39 | reset Restart the device 40 | set-bootable-part Mark physical storage partition as bootable 41 | write Write a partition 42 | help Print this message or the help of the given subcommand(s) 43 | 44 | Options: 45 | --backend 46 | 47 | --bypass-storage 48 | Accept storage r/w operations, but make them never actually execute (useful for testing USB throughput) 49 | -d, --dev-path 50 | E.g. COM4 on Windows 51 | -l, --loader-path 52 | 53 | --hash-packets 54 | Validate every packet. Slow. 55 | -L, --phys-part-idx 56 | e.g. LUN index for UFS [default: 0] 57 | --print-firehose-log 58 | 59 | --read-back-verify 60 | Every operation is read back. VERY SLOW! 61 | --reset-mode 62 | WARNING: Will be deprecated in release v1.0.0 [default: edl] 63 | --serial-no 64 | 65 | -A, --skip-hello-wait 66 | Work around missing HELLO packet 67 | -s, --storage-type 68 | 69 | -S, --storage-slot 70 | Index of the physical device (e.g. 1 for secondary UFS) [default: 0] 71 | --sector-size 72 | 73 | --skip-storage-init 74 | Required for unprovisioned storage media. 75 | --verbose-sahara 76 | 77 | --verbose-firehose 78 | 79 | -h, --help 80 | Print help 81 | -V, --version 82 | Print version 83 | ``` 84 | 85 |
86 | 87 |
88 | 89 | qramdump 90 | 91 | ``` 92 | Usage: qramdump [OPTIONS] [REGIONS_TO_DUMP]... 93 | 94 | Arguments: 95 | [REGIONS_TO_DUMP]... 96 | 97 | Options: 98 | --backend 99 | -d, --dev-path E.g. COM4 on Windows 100 | --serial-no 101 | --verbose-sahara 102 | -h, --help Print help 103 | -V, --version Print version 104 | ``` 105 | 106 |
107 | 108 | ### Windows 109 | You'll need to acquire an appropriate driver that exposes the device as a USB serial port, or use [WinUSB](https://learn.microsoft.com/en-us/windows-hardware/drivers/usbcon/winusb-installation). 110 | 111 | Serial is used as the default backend on this platform. 112 | 113 | ### Loader filename on newer platforms 114 | Some newer platforms (e.g. SM8750) require that a file called `xbl_s_devprg_ns.melf` is used instead of `prog_firehose_ddr.elf`. This change may be opaque to you if the file has been renamed as part of the binary delivery process. 115 | 116 | ### LUN handling 117 | Due to how the protocol is constructed, particularly when interfacing with UFS, you ***must*** specify the LUN (physical storage partition) index on which you want to operate. This does not concern the `flasher` command (rawproramN.xml files include that information) and operations that aren't storage-related (e.g. `peek` or `nop`). 118 | 119 | ## Common usage examples 120 | 121 |
122 | Flash a full META image 123 | 124 | ### Example with UFS as primary storage, reboots to OS after flashing ends 125 | ``` 126 | qdl-rs -l prog_firehose_ddr.elf -s ufs --reset-mode system flasher -p rawprogram*.xml -x patch*.xml 127 | 128 | # NOTE: qdl-rs will flash anything you pass as a parameter. Some METAs ship a number of rawprogram0_foo.xml 129 | # files which may be undesirable (e.g. _WIPE_GPT). You can filter those out with e.g.: 130 | find /path/to/build/ -regex '.*/rawprogram[0-9]+\.xml$' 131 | ``` 132 | 133 |
134 | 135 |
136 | Reboot the device back to the OS 137 | 138 | ``` 139 | qdl-rs -l prog_firehose_ddr.elf -s ufs reset system 140 | ``` 141 | 142 |
143 | 144 |
145 | Dump the entire physical storage partition (e.g. LUN) 146 | 147 | ``` 148 | qdl-rs -l prog_firehose_ddr.elf -s ufs --phys-part-idx 2 dump -o lun2/ 149 | ``` 150 | 151 |
152 | 153 |
154 | Fetch a single partition from LUN2 155 | 156 | ``` 157 | qdl-rs -l prog_firehose_ddr.elf -s ufs --phys-part-idx 2 dump-part EFI 158 | ``` 159 | 160 |
161 | 162 |
163 | Overwrite a single partition on LUN0 164 | 165 | ``` 166 | qdl-rs -l prog_firehose_ddr.elf -s ufs --phys-part-idx 0 write boot boot.img 167 | ``` 168 | 169 |
170 | 171 |
172 | Print out the partition table on LUN4 173 | 174 | ``` 175 | qdl-rs -l prog_firehose_ddr.elf -s ufs --phys-part-idx 4 print-gpt 176 | ``` 177 | 178 |
179 | 180 |
181 | Overwrite the entirety of LUN7 (VERY dangerous, may remove device-unique data) 182 | 183 | ``` 184 | qdl-rs -l prog_firehose_ddr.elf -s ufs --phys-part-idx 7 overwrite-storage lun7_dump.img 185 | ``` 186 | 187 |
188 | 189 |
190 | Erase a partition on eMMC (VERY dangerous, may remove device-unique data) 191 | 192 | ``` 193 | qdl-rs -l prog_firehose_ddr.elf -s emmc erase boot 194 | ``` 195 | 196 |
197 | 198 |
199 | Set LUN2 as bootable (i.e. containing xbl) 200 | 201 | ``` 202 | qdl-rs -l prog_firehose_ddr.elf -s ufs set-bootable-part 2 203 | ``` 204 | 205 |
206 | 207 | ## Documentation 208 | 209 | Run `cargo doc --open` to generate and open the latest rustdoc. Learn more [here](https://doc.rust-lang.org/cargo/commands/cargo-doc.html). 210 | 211 | ## Contributing 212 | 213 | See [`CONTRIBUTING.md`](/CONTRIBUTING.md). 214 | Your code is expected to pass `cargo fmt` and `cargo clippy` checks - the CI will be on the lookout for that. 215 | 216 | ## License 217 | 218 | See the [`LICENSE` file](/LICENSE). 219 | 220 | ## Credits 221 | 222 | * [linux-msm/qdl](https://github.com/linux-msm/qdl) for the open C implementation 223 | -------------------------------------------------------------------------------- /cli/src/programfile.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 3 | use anyhow::bail; 4 | use indexmap::IndexMap; 5 | use std::{ 6 | fs, 7 | io::{Seek, SeekFrom}, 8 | path::Path, 9 | }; 10 | use xmltree::{self, Element, XMLNode}; 11 | 12 | use qdl::{ 13 | firehose_checksum_storage, firehose_patch, firehose_program_storage, firehose_read_storage, 14 | types::QdlChan, 15 | }; 16 | 17 | fn parse_read_cmd( 18 | channel: &mut T, 19 | out_dir: &Path, 20 | attrs: &IndexMap, 21 | checksum_only: bool, 22 | ) -> anyhow::Result<()> { 23 | let num_sectors = attrs 24 | .get("num_partition_sectors") 25 | .unwrap() 26 | .parse::() 27 | .unwrap(); 28 | let slot = attrs.get("slot").map_or(0, |a| a.parse::().unwrap()); 29 | let phys_part_idx = attrs 30 | .get("physical_partition_number") 31 | .unwrap() 32 | .parse::() 33 | .unwrap(); 34 | let start_sector = attrs.get("start_sector").unwrap().parse::().unwrap(); 35 | 36 | if checksum_only { 37 | return firehose_checksum_storage(channel, num_sectors, phys_part_idx, start_sector); 38 | } 39 | 40 | if !attrs.contains_key("filename") { 41 | bail!("Got '' tag without a filename"); 42 | } 43 | let mut outfile = fs::File::create(out_dir.join(attrs.get("filename").unwrap()))?; 44 | 45 | firehose_read_storage( 46 | channel, 47 | &mut outfile, 48 | num_sectors, 49 | slot, 50 | phys_part_idx, 51 | start_sector, 52 | ) 53 | } 54 | 55 | fn parse_patch_cmd( 56 | channel: &mut T, 57 | attrs: &IndexMap, 58 | verbose: bool, 59 | ) -> anyhow::Result<()> { 60 | if let Some(filename) = attrs.get("filename") { 61 | if filename != "DISK" && verbose { 62 | println!("Skipping tag trying to alter {filename} on Host filesystem"); 63 | return Ok(()); 64 | } 65 | } else { 66 | bail!("Got '' tag without a filename"); 67 | } 68 | 69 | let byte_off = attrs.get("byte_offset").unwrap().parse::().unwrap(); 70 | let slot = attrs.get("slot").map_or(0, |a| a.parse::().unwrap()); 71 | let phys_part_idx = attrs 72 | .get("physical_partition_number") 73 | .unwrap() 74 | .parse::() 75 | .unwrap(); 76 | let size = attrs.get("size_in_bytes").unwrap().parse::().unwrap(); 77 | let start_sector = attrs.get("start_sector").unwrap(); 78 | let val = attrs.get("value").unwrap(); 79 | 80 | firehose_patch( 81 | channel, 82 | byte_off, 83 | slot, 84 | phys_part_idx, 85 | size, 86 | start_sector, 87 | val, 88 | ) 89 | } 90 | 91 | const BOOTABLE_PART_NAMES: [&str; 3] = ["xbl", "xbl_a", "sbl1"]; 92 | 93 | // TODO: readbackverify 94 | fn parse_program_cmd( 95 | channel: &mut T, 96 | program_file_dir: &Path, 97 | attrs: &IndexMap, 98 | allow_missing_files: bool, 99 | bootable_part_idx: &mut Option, 100 | verbose: bool, 101 | ) -> anyhow::Result<()> { 102 | let sector_size = attrs 103 | .get("SECTOR_SIZE_IN_BYTES") 104 | .unwrap() 105 | .parse::() 106 | .unwrap(); 107 | if sector_size != channel.fh_config().storage_sector_size { 108 | bail!( 109 | "Mismatch in storage sector size! Programfile requests {}", 110 | sector_size 111 | ); 112 | } 113 | let num_sectors = attrs 114 | .get("num_partition_sectors") 115 | .unwrap() 116 | .parse::() 117 | .unwrap(); 118 | let slot = attrs.get("slot").map_or(0, |a| a.parse::().unwrap()); 119 | let phys_part_idx = attrs 120 | .get("physical_partition_number") 121 | .unwrap() 122 | .parse::() 123 | .unwrap(); 124 | let start_sector = attrs.get("start_sector").unwrap(); 125 | let file_sector_offset = attrs 126 | .get("file_sector_offset") 127 | .unwrap_or(&"".to_owned()) 128 | .parse::() 129 | .unwrap_or(0); 130 | 131 | let label = attrs.get("label").unwrap(); 132 | if num_sectors == 0 { 133 | println!("Skipping 0-length entry for {label}"); 134 | return Ok(()); 135 | } 136 | if BOOTABLE_PART_NAMES.contains(&&label[..]) { 137 | *bootable_part_idx = Some(phys_part_idx); 138 | } 139 | 140 | let filename = attrs.get("filename").unwrap(); 141 | let file_path = program_file_dir.join(filename); 142 | if allow_missing_files { 143 | if filename.is_empty() { 144 | if verbose { 145 | println!("Skipping bogus entry for {label}"); 146 | } 147 | return Ok(()); 148 | } else if !file_path.exists() { 149 | if verbose { 150 | println!("Skipping non-existent file {}", file_path.to_str().unwrap()); 151 | } 152 | return Ok(()); 153 | } 154 | } 155 | 156 | let mut buf = fs::File::open(file_path)?; 157 | buf.seek(SeekFrom::Current( 158 | sector_size as i64 * file_sector_offset as i64, 159 | ))?; 160 | 161 | firehose_program_storage( 162 | channel, 163 | &mut buf, 164 | label, 165 | num_sectors, 166 | slot, 167 | phys_part_idx, 168 | start_sector, 169 | ) 170 | } 171 | 172 | // TODO: there's some funny optimizations to make here, such as OoO loading files into memory, or doing things while we're waiting on the device to finish 173 | pub fn parse_program_xml( 174 | channel: &mut T, 175 | xml: &Element, 176 | program_file_dir: &Path, 177 | out_dir: &Path, 178 | allow_missing_files: bool, 179 | verbose: bool, 180 | ) -> anyhow::Result> { 181 | let mut bootable_part_idx: Option = None; 182 | 183 | // First make sure we have all the necessary files (and fail unless specified otherwise) 184 | for node in xml.children.iter() { 185 | if let XMLNode::Element(e) = node { 186 | match e.name.to_lowercase().as_str() { 187 | "program" => { 188 | if !e.attributes.contains_key("filename") { 189 | bail!("Got '' tag without a filename"); 190 | } 191 | 192 | let filename = e.attributes.get("filename").unwrap(); 193 | let file_path = program_file_dir.join(filename); 194 | 195 | if !file_path.exists() && !allow_missing_files { 196 | bail!("{} doesn't exist!", file_path.to_str().unwrap()) 197 | } 198 | } 199 | _ => continue, 200 | } 201 | } 202 | } 203 | 204 | // At last, do the things we're supposed to do 205 | for node in xml.children.iter() { 206 | if let XMLNode::Element(e) = node { 207 | match e.name.to_lowercase().as_str() { 208 | "getsha256digest" => parse_read_cmd(channel, out_dir, &e.attributes, true)?, 209 | "patch" => parse_patch_cmd(channel, &e.attributes, verbose)?, 210 | "program" => parse_program_cmd( 211 | channel, 212 | program_file_dir, 213 | &e.attributes, 214 | allow_missing_files, 215 | &mut bootable_part_idx, 216 | verbose, 217 | )?, 218 | "read" => parse_read_cmd(channel, out_dir, &e.attributes, false)?, 219 | 220 | unknown => bail!( 221 | "Got unknown instruction ({}), failing to prevent damage", 222 | unknown 223 | ), 224 | }; 225 | } 226 | } 227 | 228 | Ok(bootable_part_idx) 229 | } 230 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 3 | use anyhow::{Result, bail}; 4 | use clap::{Parser, Subcommand}; 5 | use clap_num::maybe_hex; 6 | use itertools::Itertools; 7 | use owo_colors::OwoColorize; 8 | use qdl::parsers::{firehose_parser_ack_nak, firehose_parser_configure_response}; 9 | use qdl::sahara::{SaharaCmdModeCmd, SaharaMode, sahara_run, sahara_send_hello_rsp}; 10 | use qdl::types::{FirehoseResetMode, FirehoseStorageType, QdlBackend, QdlDevice}; 11 | use qdl::{firehose_configure, firehose_read, firehose_reset, types::FirehoseConfiguration}; 12 | use qdl::{ 13 | firehose_get_default_sector_size, firehose_nop, firehose_peek, firehose_program_storage, 14 | firehose_set_bootable, setup_target_device, 15 | }; 16 | use util::{ 17 | find_part, print_partition_table, read_gpt_from_storage, read_storage_logical_partition, 18 | }; 19 | 20 | use std::fs::{self, File}; 21 | use std::{path::Path, str::FromStr}; 22 | 23 | mod flasher; 24 | mod programfile; 25 | mod util; 26 | 27 | #[derive(Debug, Subcommand, PartialEq)] 28 | enum Command { 29 | /// Dump the entire storage 30 | Dump { 31 | #[arg(short, default_value = "out/")] 32 | outdir: String, 33 | }, 34 | 35 | /// Dump a single partition 36 | DumpPart { 37 | #[arg()] 38 | name: String, 39 | 40 | #[arg(short, default_value = "out/")] 41 | outdir: String, 42 | }, 43 | 44 | /// Invoke the flasher 45 | Flasher { 46 | #[arg(short, long, num_args = 1..=128, value_name = "FILE")] 47 | program_file_paths: Vec, 48 | 49 | #[arg(short = 'x', long, num_args = 0..=128, value_name = "FILE")] 50 | patch_file_paths: Vec, 51 | 52 | #[arg(long, default_value = "false")] 53 | verbose_flasher: bool, 54 | }, 55 | 56 | /// Erase a partition 57 | Erase { 58 | #[arg()] 59 | name: String, 60 | }, 61 | 62 | /// Ask the device to do nothing, hopefully successfully 63 | Nop, 64 | 65 | /// Overwrite the storage physical partition contents with a raw image 66 | /// Similar to Flasher, but this one only takes a partition dump as input 67 | /// and performs no real validation on the input data 68 | OverwriteStorage { 69 | #[arg()] 70 | file_path: String, 71 | }, 72 | 73 | /// Peek at memory 74 | Peek { 75 | #[arg(value_parser=maybe_hex::)] 76 | base: u64, 77 | 78 | #[arg(default_value = "1", value_parser=maybe_hex::)] 79 | len: u64, 80 | }, 81 | 82 | /// Print the GPT table 83 | PrintGpt, 84 | 85 | /// Restart the device 86 | Reset { 87 | #[arg(default_value = "system", value_name = "edl/off/system")] 88 | reset_mode: String, 89 | }, 90 | 91 | /// Mark physical storage partition as bootable 92 | SetBootablePart { 93 | #[arg()] 94 | idx: u8, 95 | }, 96 | 97 | /// Write a partition 98 | Write { 99 | #[arg()] 100 | part_name: String, 101 | 102 | #[arg()] 103 | file_path: String, 104 | }, 105 | } 106 | 107 | #[derive(Parser, Debug)] 108 | #[command(version, about, long_about = None)] 109 | struct Args { 110 | #[arg(long, value_name = "usb/serial")] 111 | backend: Option, 112 | 113 | /// Accept storage r/w operations, but make them never actually execute (useful for testing USB throughput) 114 | #[arg(long, default_value = "false")] 115 | bypass_storage: bool, 116 | 117 | #[arg(short, long, help = "E.g. COM4 on Windows")] 118 | dev_path: Option, 119 | 120 | #[arg(short, long, value_name = "FILE")] 121 | loader_path: String, 122 | 123 | #[arg(long, default_value = "false", help = "Validate every packet. Slow.")] 124 | hash_packets: bool, 125 | 126 | #[arg( 127 | short = 'L', 128 | long, 129 | default_value = "0", 130 | help = "e.g. LUN index for UFS" 131 | )] 132 | phys_part_idx: u8, 133 | 134 | #[arg(long, default_value = "false")] 135 | print_firehose_log: bool, 136 | 137 | #[arg( 138 | long, 139 | default_value = "false", 140 | help = "Every operation is read back. VERY SLOW!" 141 | )] 142 | read_back_verify: bool, 143 | 144 | /// WARNING: Will be deprecated in release v1.0.0 145 | #[arg(long, default_value = "edl", value_name = "edl/off/system")] 146 | reset_mode: String, 147 | 148 | // Only applies to the USB backend 149 | #[arg(long)] 150 | serial_no: Option, 151 | 152 | #[arg( 153 | short = 'A', 154 | long, 155 | default_value = "false", 156 | help = "Work around missing HELLO packet" 157 | )] 158 | skip_hello_wait: bool, 159 | 160 | #[arg(short, long, value_name = "emmc/ufs/nvme/nand")] 161 | storage_type: String, 162 | 163 | #[arg( 164 | short = 'S', 165 | long, 166 | default_value = "0", 167 | help = "Index of the physical device (e.g. 1 for secondary UFS)" 168 | )] 169 | storage_slot: u8, 170 | 171 | #[arg(long)] 172 | sector_size: Option, 173 | 174 | #[arg( 175 | long, 176 | default_value = "false", 177 | help = "Required for unprovisioned storage media." 178 | )] 179 | skip_storage_init: bool, 180 | 181 | #[arg(long, default_value = "false")] 182 | verbose_sahara: bool, 183 | 184 | #[arg(long, default_value = "false")] 185 | verbose_firehose: bool, 186 | 187 | #[command(subcommand)] 188 | command: Command, 189 | } 190 | 191 | fn main() -> Result<()> { 192 | let args = Args::parse(); 193 | let backend = match args.backend { 194 | Some(b) => QdlBackend::from_str(&b)?, 195 | None => QdlBackend::default(), 196 | }; 197 | let reset_mode = FirehoseResetMode::from_str(&args.reset_mode)?; 198 | 199 | // Get the MBN loader binary 200 | let mbn_loader = match fs::read(args.loader_path) { 201 | Ok(m) => m, 202 | Err(e) => bail!("Couldn't open the programmer binary: {}", e.to_string()), 203 | }; 204 | 205 | println!( 206 | "{} {}", 207 | env!("CARGO_PKG_NAME").green(), 208 | env!("CARGO_PKG_VERSION").yellow() 209 | ); 210 | 211 | // Set up the device 212 | let rw_channel = match setup_target_device(backend, args.serial_no, args.dev_path) { 213 | Ok(c) => c, 214 | Err(e) => bail!("Couldn't set up device: {}", e.to_string()), 215 | }; 216 | let mut qdl_dev = QdlDevice { 217 | rw: rw_channel, 218 | fh_cfg: FirehoseConfiguration { 219 | hash_packets: args.hash_packets, 220 | read_back_verify: args.read_back_verify, 221 | storage_type: FirehoseStorageType::from_str(&args.storage_type)?, 222 | storage_sector_size: match args.sector_size { 223 | Some(n) => n, 224 | None => { 225 | let sector_size = firehose_get_default_sector_size(&args.storage_type); 226 | if let Some(m) = sector_size { 227 | println!("{} {}", "Using a default sector size of".bright_black(), m); 228 | m 229 | } else { 230 | bail!("Specify storage sector size with --sector-size "); 231 | } 232 | } 233 | }, 234 | bypass_storage: args.bypass_storage, 235 | backend, 236 | skip_firehose_log: !args.print_firehose_log, 237 | verbose_firehose: args.verbose_firehose, 238 | // The remaining values are overwritten at runtime through a handshake 239 | ..Default::default() 240 | }, 241 | reset_on_drop: false, 242 | }; 243 | 244 | // In case another program on the system has already consumed the HELLO packet, 245 | // send a HELLO response upfront, to appease the state machine 246 | if args.skip_hello_wait { 247 | sahara_send_hello_rsp(&mut qdl_dev, SaharaMode::Command)?; 248 | } 249 | 250 | // Get some info about the device 251 | let sn = sahara_run( 252 | &mut qdl_dev, 253 | SaharaMode::Command, 254 | Some(SaharaCmdModeCmd::ReadSerialNum), 255 | &mut [], 256 | vec![], 257 | args.verbose_sahara, 258 | )?; 259 | let sn = u32::from_le_bytes([sn[0], sn[1], sn[2], sn[3]]); 260 | println!("Chip serial number: 0x{sn:x}"); 261 | 262 | let key_hash = sahara_run( 263 | &mut qdl_dev, 264 | SaharaMode::Command, 265 | Some(SaharaCmdModeCmd::ReadOemKeyHash), 266 | &mut [], 267 | vec![], 268 | args.verbose_sahara, 269 | )?; 270 | println!( 271 | "OEM Private Key hash: 0x{:02x}", 272 | key_hash[..key_hash.len() / 3].iter().format("") 273 | ); 274 | 275 | // Send the loader (and any other images) 276 | sahara_run( 277 | &mut qdl_dev, 278 | SaharaMode::WaitingForImage, 279 | None, 280 | &mut [mbn_loader], 281 | vec![], 282 | args.verbose_sahara, 283 | )?; 284 | 285 | // If we're past Sahara, activate the Firehose reset-on-drop listener 286 | qdl_dev.reset_on_drop = true; 287 | 288 | // Get any "welcome" logs 289 | firehose_read(&mut qdl_dev, firehose_parser_ack_nak)?; 290 | 291 | // Send the host capabilities to the device 292 | firehose_configure(&mut qdl_dev, args.skip_storage_init)?; 293 | 294 | // Parse some information from the device 295 | firehose_read(&mut qdl_dev, firehose_parser_configure_response)?; 296 | 297 | match args.command { 298 | Command::Dump { outdir } => { 299 | fs::create_dir_all(&outdir)?; 300 | let outpath = Path::new(&outdir); 301 | 302 | for (_, p) in 303 | read_gpt_from_storage(&mut qdl_dev, args.storage_slot, args.phys_part_idx)?.iter() 304 | { 305 | // *sigh* 306 | if p.partition_name.as_str().is_empty() || p.size()? == 0 { 307 | continue; 308 | } 309 | 310 | let mut out = File::create(outpath.join(p.partition_name.to_string()))?; 311 | read_storage_logical_partition( 312 | &mut qdl_dev, 313 | &mut out, 314 | &p.partition_name.to_string(), 315 | args.storage_slot, 316 | args.phys_part_idx, 317 | )? 318 | } 319 | // TODO: create an xml file 320 | } 321 | Command::DumpPart { name, outdir } => { 322 | fs::create_dir_all(&outdir)?; 323 | let outpath = Path::new(&outdir); 324 | let mut out = File::create(outpath.join(&name))?; 325 | 326 | read_storage_logical_partition( 327 | &mut qdl_dev, 328 | &mut out, 329 | &name, 330 | args.storage_slot, 331 | args.phys_part_idx, 332 | )? 333 | } 334 | Command::Erase { name } => { 335 | let part = find_part(&mut qdl_dev, &name, args.storage_slot, args.phys_part_idx)?; 336 | 337 | firehose_program_storage( 338 | &mut qdl_dev, 339 | &mut &[0u8][..], 340 | &name, 341 | (part.ending_lba - part.starting_lba + 1) as usize, 342 | args.storage_slot, 343 | args.phys_part_idx, 344 | &part.starting_lba.to_string(), 345 | )?; 346 | } 347 | Command::Flasher { 348 | program_file_paths, 349 | patch_file_paths, 350 | verbose_flasher, 351 | } => { 352 | flasher::run_flash( 353 | &mut qdl_dev, 354 | program_file_paths, 355 | patch_file_paths, 356 | verbose_flasher, 357 | )?; 358 | } 359 | Command::Nop => println!( 360 | "Your nop was {}", 361 | firehose_nop(&mut qdl_dev) 362 | .map(|_| "successful".bright_green()) 363 | .map_err(|_| "unsuccessful".bright_red()) 364 | .unwrap() 365 | ), 366 | Command::OverwriteStorage { file_path } => { 367 | let mut file = File::open(file_path)?; 368 | let file_len_sectors = file 369 | .metadata()? 370 | .len() 371 | .div_ceil(qdl_dev.fh_cfg.storage_sector_size as u64); 372 | 373 | firehose_program_storage( 374 | &mut qdl_dev, 375 | &mut file, 376 | "", 377 | file_len_sectors as usize, 378 | args.storage_slot, 379 | args.phys_part_idx, 380 | "0", 381 | )?; 382 | } 383 | Command::Peek { base, len } => firehose_peek(&mut qdl_dev, base, len)?, 384 | Command::PrintGpt => { 385 | print_partition_table(&mut qdl_dev, args.storage_slot, args.phys_part_idx)? 386 | } 387 | Command::Reset { reset_mode } => { 388 | firehose_reset(&mut qdl_dev, &FirehoseResetMode::from_str(&reset_mode)?, 0)? 389 | } 390 | Command::SetBootablePart { idx } => firehose_set_bootable(&mut qdl_dev, idx)?, 391 | Command::Write { 392 | part_name, 393 | file_path, 394 | } => { 395 | let part: gptman::GPTPartitionEntry = find_part( 396 | &mut qdl_dev, 397 | &part_name, 398 | args.storage_slot, 399 | args.phys_part_idx, 400 | )?; 401 | let mut file = File::open(file_path)?; 402 | let file_len_sectors = file 403 | .metadata()? 404 | .len() 405 | .div_ceil(qdl_dev.fh_cfg.storage_sector_size as u64); 406 | let part_len_sectors = part.ending_lba - part.starting_lba + 1; 407 | 408 | if file_len_sectors > part_len_sectors { 409 | bail!( 410 | "Partition {} is too small for the specified image ({} > {})", 411 | part_name, 412 | file_len_sectors, 413 | part_len_sectors 414 | ); 415 | } 416 | 417 | firehose_program_storage( 418 | &mut qdl_dev, 419 | &mut file, 420 | &part_name, 421 | file_len_sectors as usize, 422 | args.storage_slot, 423 | args.phys_part_idx, 424 | &part.starting_lba.to_string(), 425 | )?; 426 | } 427 | }; 428 | 429 | // Finally, reset the device 430 | qdl_dev.reset_on_drop = false; 431 | firehose_reset(&mut qdl_dev, &reset_mode, 0)?; 432 | 433 | println!( 434 | "{} {}", 435 | "All went well! Resetting to".green(), 436 | reset_mode.to_string().bright_yellow() 437 | ); 438 | 439 | Ok(()) 440 | } 441 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 49 | dependencies = [ 50 | "anstyle", 51 | "once_cell", 52 | "windows-sys", 53 | ] 54 | 55 | [[package]] 56 | name = "anyhow" 57 | version = "1.0.97" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 60 | 61 | [[package]] 62 | name = "autocfg" 63 | version = "1.4.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 66 | 67 | [[package]] 68 | name = "bincode" 69 | version = "1.3.3" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 72 | dependencies = [ 73 | "serde", 74 | ] 75 | 76 | [[package]] 77 | name = "bitflags" 78 | version = "2.9.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 81 | 82 | [[package]] 83 | name = "cc" 84 | version = "1.2.17" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" 87 | dependencies = [ 88 | "shlex", 89 | ] 90 | 91 | [[package]] 92 | name = "cfg-if" 93 | version = "1.0.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 96 | 97 | [[package]] 98 | name = "clap" 99 | version = "4.5.35" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" 102 | dependencies = [ 103 | "clap_builder", 104 | "clap_derive", 105 | ] 106 | 107 | [[package]] 108 | name = "clap-num" 109 | version = "1.2.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "822c4000301ac390e65995c62207501e3ef800a1fc441df913a5e8e4dc374816" 112 | dependencies = [ 113 | "num-traits", 114 | ] 115 | 116 | [[package]] 117 | name = "clap_builder" 118 | version = "4.5.35" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" 121 | dependencies = [ 122 | "anstream", 123 | "anstyle", 124 | "clap_lex", 125 | "strsim", 126 | ] 127 | 128 | [[package]] 129 | name = "clap_derive" 130 | version = "4.5.32" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 133 | dependencies = [ 134 | "heck", 135 | "proc-macro2", 136 | "quote", 137 | "syn", 138 | ] 139 | 140 | [[package]] 141 | name = "clap_lex" 142 | version = "0.7.4" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 145 | 146 | [[package]] 147 | name = "colorchoice" 148 | version = "1.0.3" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 151 | 152 | [[package]] 153 | name = "crc" 154 | version = "3.2.1" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" 157 | dependencies = [ 158 | "crc-catalog", 159 | ] 160 | 161 | [[package]] 162 | name = "crc-catalog" 163 | version = "2.4.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 166 | 167 | [[package]] 168 | name = "crossbeam-channel" 169 | version = "0.5.15" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 172 | dependencies = [ 173 | "crossbeam-utils", 174 | ] 175 | 176 | [[package]] 177 | name = "crossbeam-utils" 178 | version = "0.8.21" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 181 | 182 | [[package]] 183 | name = "either" 184 | version = "1.15.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 187 | 188 | [[package]] 189 | name = "equivalent" 190 | version = "1.0.2" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 193 | 194 | [[package]] 195 | name = "gptman" 196 | version = "1.1.4" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "d344ce89f41a128e74337a4830507927f1f378b4ae69a26d949d1d1cce55c048" 199 | dependencies = [ 200 | "bincode", 201 | "crc", 202 | "nix", 203 | "serde", 204 | "thiserror", 205 | ] 206 | 207 | [[package]] 208 | name = "hashbrown" 209 | version = "0.15.2" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 212 | 213 | [[package]] 214 | name = "heck" 215 | version = "0.5.0" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 218 | 219 | [[package]] 220 | name = "indexmap" 221 | version = "2.8.0" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" 224 | dependencies = [ 225 | "equivalent", 226 | "hashbrown", 227 | ] 228 | 229 | [[package]] 230 | name = "is_terminal_polyfill" 231 | version = "1.70.1" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 234 | 235 | [[package]] 236 | name = "itertools" 237 | version = "0.14.0" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 240 | dependencies = [ 241 | "either", 242 | ] 243 | 244 | [[package]] 245 | name = "libc" 246 | version = "0.2.171" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 249 | 250 | [[package]] 251 | name = "libusb1-sys" 252 | version = "0.7.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f" 255 | dependencies = [ 256 | "cc", 257 | "libc", 258 | "pkg-config", 259 | "vcpkg", 260 | ] 261 | 262 | [[package]] 263 | name = "nix" 264 | version = "0.27.1" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 267 | dependencies = [ 268 | "bitflags", 269 | "cfg-if", 270 | "libc", 271 | ] 272 | 273 | [[package]] 274 | name = "num-traits" 275 | version = "0.2.19" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 278 | dependencies = [ 279 | "autocfg", 280 | ] 281 | 282 | [[package]] 283 | name = "once_cell" 284 | version = "1.21.3" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 287 | 288 | [[package]] 289 | name = "owo-colors" 290 | version = "4.2.0" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564" 293 | 294 | [[package]] 295 | name = "pbr" 296 | version = "1.1.1" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "ed5827dfa0d69b6c92493d6c38e633bbaa5937c153d0d7c28bf12313f8c6d514" 299 | dependencies = [ 300 | "crossbeam-channel", 301 | "libc", 302 | "winapi", 303 | ] 304 | 305 | [[package]] 306 | name = "pkg-config" 307 | version = "0.3.32" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 310 | 311 | [[package]] 312 | name = "proc-macro2" 313 | version = "1.0.94" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 316 | dependencies = [ 317 | "unicode-ident", 318 | ] 319 | 320 | [[package]] 321 | name = "qdl" 322 | version = "0.1.0" 323 | dependencies = [ 324 | "anstream", 325 | "anyhow", 326 | "bincode", 327 | "indexmap", 328 | "owo-colors", 329 | "pbr", 330 | "rusb", 331 | "serde", 332 | "serde_repr", 333 | "serial2", 334 | "xmltree", 335 | ] 336 | 337 | [[package]] 338 | name = "qdl-rs" 339 | version = "0.1.0" 340 | dependencies = [ 341 | "anyhow", 342 | "clap", 343 | "clap-num", 344 | "gptman", 345 | "indexmap", 346 | "itertools", 347 | "owo-colors", 348 | "qdl", 349 | "xmltree", 350 | ] 351 | 352 | [[package]] 353 | name = "qramdump" 354 | version = "0.1.0" 355 | dependencies = [ 356 | "anyhow", 357 | "clap", 358 | "clap-num", 359 | "qdl", 360 | ] 361 | 362 | [[package]] 363 | name = "quote" 364 | version = "1.0.40" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 367 | dependencies = [ 368 | "proc-macro2", 369 | ] 370 | 371 | [[package]] 372 | name = "rusb" 373 | version = "0.9.4" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4" 376 | dependencies = [ 377 | "libc", 378 | "libusb1-sys", 379 | ] 380 | 381 | [[package]] 382 | name = "serde" 383 | version = "1.0.219" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 386 | dependencies = [ 387 | "serde_derive", 388 | ] 389 | 390 | [[package]] 391 | name = "serde_derive" 392 | version = "1.0.219" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 395 | dependencies = [ 396 | "proc-macro2", 397 | "quote", 398 | "syn", 399 | ] 400 | 401 | [[package]] 402 | name = "serde_repr" 403 | version = "0.1.20" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" 406 | dependencies = [ 407 | "proc-macro2", 408 | "quote", 409 | "syn", 410 | ] 411 | 412 | [[package]] 413 | name = "serial2" 414 | version = "0.2.28" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "8cd0c773455b60177d1abe4c739cbfa316c4f2f0ef37465befcb72e8a15cdd02" 417 | dependencies = [ 418 | "cfg-if", 419 | "libc", 420 | "winapi", 421 | ] 422 | 423 | [[package]] 424 | name = "shlex" 425 | version = "1.3.0" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 428 | 429 | [[package]] 430 | name = "strsim" 431 | version = "0.11.1" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 434 | 435 | [[package]] 436 | name = "syn" 437 | version = "2.0.100" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 440 | dependencies = [ 441 | "proc-macro2", 442 | "quote", 443 | "unicode-ident", 444 | ] 445 | 446 | [[package]] 447 | name = "thiserror" 448 | version = "1.0.69" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 451 | dependencies = [ 452 | "thiserror-impl", 453 | ] 454 | 455 | [[package]] 456 | name = "thiserror-impl" 457 | version = "1.0.69" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 460 | dependencies = [ 461 | "proc-macro2", 462 | "quote", 463 | "syn", 464 | ] 465 | 466 | [[package]] 467 | name = "unicode-ident" 468 | version = "1.0.18" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 471 | 472 | [[package]] 473 | name = "utf8parse" 474 | version = "0.2.2" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 477 | 478 | [[package]] 479 | name = "vcpkg" 480 | version = "0.2.15" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 483 | 484 | [[package]] 485 | name = "winapi" 486 | version = "0.3.9" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 489 | dependencies = [ 490 | "winapi-i686-pc-windows-gnu", 491 | "winapi-x86_64-pc-windows-gnu", 492 | ] 493 | 494 | [[package]] 495 | name = "winapi-i686-pc-windows-gnu" 496 | version = "0.4.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 499 | 500 | [[package]] 501 | name = "winapi-x86_64-pc-windows-gnu" 502 | version = "0.4.0" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 505 | 506 | [[package]] 507 | name = "windows-sys" 508 | version = "0.59.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 511 | dependencies = [ 512 | "windows-targets", 513 | ] 514 | 515 | [[package]] 516 | name = "windows-targets" 517 | version = "0.52.6" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 520 | dependencies = [ 521 | "windows_aarch64_gnullvm", 522 | "windows_aarch64_msvc", 523 | "windows_i686_gnu", 524 | "windows_i686_gnullvm", 525 | "windows_i686_msvc", 526 | "windows_x86_64_gnu", 527 | "windows_x86_64_gnullvm", 528 | "windows_x86_64_msvc", 529 | ] 530 | 531 | [[package]] 532 | name = "windows_aarch64_gnullvm" 533 | version = "0.52.6" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 536 | 537 | [[package]] 538 | name = "windows_aarch64_msvc" 539 | version = "0.52.6" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 542 | 543 | [[package]] 544 | name = "windows_i686_gnu" 545 | version = "0.52.6" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 548 | 549 | [[package]] 550 | name = "windows_i686_gnullvm" 551 | version = "0.52.6" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 554 | 555 | [[package]] 556 | name = "windows_i686_msvc" 557 | version = "0.52.6" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 560 | 561 | [[package]] 562 | name = "windows_x86_64_gnu" 563 | version = "0.52.6" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 566 | 567 | [[package]] 568 | name = "windows_x86_64_gnullvm" 569 | version = "0.52.6" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 572 | 573 | [[package]] 574 | name = "windows_x86_64_msvc" 575 | version = "0.52.6" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 578 | 579 | [[package]] 580 | name = "xml-rs" 581 | version = "0.8.25" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" 584 | 585 | [[package]] 586 | name = "xmltree" 587 | version = "0.11.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6" 590 | dependencies = [ 591 | "indexmap", 592 | "xml-rs", 593 | ] 594 | -------------------------------------------------------------------------------- /qdl/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 3 | use anstream::println; 4 | use anyhow::Result; 5 | use indexmap::IndexMap; 6 | use owo_colors::OwoColorize; 7 | use parsers::firehose_parser_ack_nak; 8 | use serial::setup_serial_device; 9 | use std::cmp::min; 10 | use std::io::{Read, Write}; 11 | use std::str::{self, FromStr}; 12 | use types::FirehoseResetMode; 13 | use types::FirehoseStatus; 14 | use types::FirehoseStorageType; 15 | use types::QdlBackend; 16 | use types::QdlChan; 17 | use types::QdlReadWrite; 18 | use usb::setup_usb_device; 19 | 20 | use anyhow::bail; 21 | use pbr::{ProgressBar, Units}; 22 | use xmltree::{self, Element, XMLNode}; 23 | 24 | pub mod parsers; 25 | pub mod sahara; 26 | #[cfg(feature = "serial")] 27 | pub mod serial; 28 | pub mod types; 29 | #[cfg(feature = "usb")] 30 | pub mod usb; 31 | 32 | pub fn setup_target_device( 33 | backend: QdlBackend, 34 | serial_no: Option, 35 | port: Option, 36 | ) -> Result> { 37 | match backend { 38 | QdlBackend::Serial => match setup_serial_device(port) { 39 | Ok(d) => Ok(Box::new(d)), 40 | Err(e) => Err(e), 41 | }, 42 | QdlBackend::Usb => match setup_usb_device(serial_no) { 43 | Ok(d) => Ok(Box::new(d)), 44 | Err(e) => Err(e), 45 | }, 46 | } 47 | } 48 | 49 | /// Wrapper for easily creating Firehose-y XML packets 50 | fn firehose_xml_setup(op: &str, kvps: &[(&str, &str)]) -> anyhow::Result> { 51 | let mut xml = Element::new("data"); 52 | let mut op_node = Element::new(op); 53 | for kvp in kvps.iter() { 54 | op_node 55 | .attributes 56 | .insert(kvp.0.to_owned(), kvp.1.to_owned()); 57 | } 58 | 59 | xml.children.push(XMLNode::Element(op_node)); 60 | 61 | // TODO: define a more verbose level 62 | // println!("SEND: {}", format!("{:?}", xml).bright_cyan()); 63 | 64 | let mut buf = Vec::::new(); 65 | xml.write(&mut buf)?; 66 | 67 | Ok(buf) 68 | } 69 | 70 | /// Main Firehose XML reading function 71 | pub fn firehose_read( 72 | channel: &mut T, 73 | response_parser: fn(&mut T, &IndexMap) -> Result, 74 | ) -> Result { 75 | let mut got_any_data = false; 76 | let mut pending: Vec = Vec::new(); 77 | 78 | loop { 79 | // Use BufRead to peek at available data 80 | let available = match channel.fill_buf() { 81 | Ok(buf) => buf, 82 | Err(e) => match e.kind() { 83 | // In some cases (like with welcome messages), there's no acking 84 | // and a timeout is the "end of data" marker instead.. 85 | std::io::ErrorKind::TimedOut => { 86 | if got_any_data { 87 | return Ok(FirehoseStatus::Ack); 88 | } else { 89 | return Err(e.into()); 90 | } 91 | } 92 | _ => return Err(e.into()), 93 | }, 94 | }; 95 | 96 | got_any_data = true; 97 | 98 | // When channel is a non-packetized BufRead (e.g. serial) XML documents 99 | // are not separated from each other, or from rawmode data. Search for 100 | // in the BufRead stream to find the end of the current 101 | // message. 102 | let data_end_marker = b""; 103 | 104 | let pending_length = pending.len(); 105 | pending.extend_from_slice(available); 106 | 107 | // Search for the end marker in the pending data 108 | let end_pos = pending 109 | .windows(data_end_marker.len()) 110 | .position(|window| window == data_end_marker); 111 | 112 | if let Some(pos) = end_pos { 113 | let xml_end = pos + data_end_marker.len(); 114 | 115 | // xml_end is relative "pending", we need to consume only new the tail 116 | channel.consume(xml_end - pending_length); 117 | 118 | // Only parse the XML portion 119 | let xml_chunk = &pending[..xml_end]; 120 | let xml = match xmltree::Element::parse(xml_chunk) { 121 | Ok(x) => x, 122 | Err(e) => { 123 | // Consume the bad data and continue 124 | bail!("Failed to parse XML: {}", e); 125 | } 126 | }; 127 | 128 | // The current message might have started in "pending", so clear it 129 | // now. No need to do this if we're bailing above, as it's a local 130 | // resource. 131 | pending.clear(); 132 | 133 | if xml.name != "data" { 134 | // TODO: define a more verbose level 135 | if channel.fh_config().verbose_firehose { 136 | println!("{:?}", xml); 137 | } 138 | bail!("Got a firehose packet without a data tag"); 139 | } 140 | 141 | // The spec expects there's always a single node only 142 | if let Some(XMLNode::Element(e)) = xml.children.first() { 143 | // Check for a 'log' node and print out the message 144 | if e.name == "log" { 145 | if channel.fh_config().skip_firehose_log { 146 | continue; 147 | } 148 | 149 | println!( 150 | "LOG: {}", 151 | e.attributes 152 | .get("value") 153 | .to_owned() 154 | .unwrap_or(&String::from("")) 155 | .bright_black() 156 | ); 157 | 158 | continue; 159 | } 160 | 161 | // DEBUG: "print out incoming packets" 162 | // TODO: define a more verbose level 163 | if channel.fh_config().verbose_firehose { 164 | println!("RECV: {}", format!("{e:?}").magenta()); 165 | } 166 | 167 | // TODO: Use std::intrinsics::unlikely after it exits nightly 168 | if e.attributes.get("AttemptRetry").is_some() { 169 | return firehose_read::(channel, response_parser); 170 | } else if e.attributes.get("AttemptRestart").is_some() { 171 | // TODO: handle this automagically 172 | firehose_reset(channel, &FirehoseResetMode::ResetToEdl, 0)?; 173 | bail!("Firehose requested a restart. Run the program again."); 174 | } 175 | 176 | // Pass other nodes to specialized parsers 177 | return response_parser(channel, &e.attributes); 178 | } 179 | } else { 180 | // Didn't find the tail of the XML document in "pending" + 181 | // "available", consume the data into "pending" to let fill_buf() 182 | // read more data from the underlying Read. 183 | let available_len = available.len(); 184 | channel.consume(available_len); 185 | } 186 | } 187 | } 188 | 189 | /// Send a Firehose packet 190 | pub fn firehose_write(channel: &mut T, buf: &mut [u8]) -> anyhow::Result<()> { 191 | let mut b = buf.to_vec(); 192 | 193 | // XML can't be n * 512 bytes long by fh spec 194 | if !buf.is_empty() && buf.len().is_multiple_of(512) { 195 | println!("{}", "INFO: Appending '\n' to outgoing XML".bright_black()); 196 | b.push(b'\n'); 197 | } 198 | 199 | match channel.write_all(&b) { 200 | Ok(_) => Ok(()), 201 | // Assume FH will hang after NAK.. 202 | Err(_) => firehose_reset(channel, &FirehoseResetMode::ResetToEdl, 0), 203 | } 204 | } 205 | 206 | /// Send a Firehose packet and check for ack/nak 207 | pub fn firehose_write_getack( 208 | channel: &mut T, 209 | buf: &mut [u8], 210 | couldnt_what: String, 211 | ) -> anyhow::Result<()> { 212 | firehose_write(channel, buf)?; 213 | 214 | match firehose_read::(channel, firehose_parser_ack_nak) { 215 | Ok(FirehoseStatus::Ack) => Ok(()), 216 | Ok(FirehoseStatus::Nak) => { 217 | // Assume FH will hang after NAK.. 218 | firehose_reset(channel, &FirehoseResetMode::ResetToEdl, 0)?; 219 | Err(anyhow::Error::msg(format!("Couldn't {couldnt_what}"))) 220 | } 221 | Err(e) => Err(e), 222 | } 223 | } 224 | 225 | /// Test performance without sample data 226 | pub fn firehose_benchmark( 227 | channel: &mut T, 228 | trials: u32, 229 | test_write_perf: bool, 230 | ) -> anyhow::Result<()> { 231 | let mut xml = firehose_xml_setup( 232 | "benchmark", 233 | &[ 234 | ("trials", &trials.to_string()), 235 | ( 236 | "TestWritePerformance", 237 | &(test_write_perf as u32).to_string(), 238 | ), 239 | ( 240 | "TestReadPerformance", 241 | &(!test_write_perf as u32).to_string(), 242 | ), 243 | ], 244 | )?; 245 | 246 | firehose_write_getack(channel, &mut xml, "issue a NOP".to_owned()) 247 | } 248 | 249 | /// Send a "Hello"-type packet to the Device 250 | pub fn firehose_configure( 251 | channel: &mut T, 252 | skip_storage_init: bool, 253 | ) -> anyhow::Result<()> { 254 | let config = channel.fh_config(); 255 | // Spec requirement 256 | assert!( 257 | config 258 | .send_buffer_size 259 | .is_multiple_of(config.storage_sector_size) 260 | ); 261 | // Sanity requirement 262 | assert!( 263 | config 264 | .send_buffer_size 265 | .is_multiple_of(config.storage_sector_size) 266 | ); 267 | let mut xml = firehose_xml_setup( 268 | "configure", 269 | &[ 270 | ("AckRawDataEveryNumPackets", "0"), // TODO: (low prio) 271 | ( 272 | "SkipWrite", 273 | &(channel.fh_config().bypass_storage as u32).to_string(), 274 | ), 275 | ("SkipStorageInit", &(skip_storage_init as u32).to_string()), 276 | ("MemoryName", &config.storage_type.to_string()), 277 | ("AlwaysValidate", &(config.hash_packets as u32).to_string()), 278 | ("Verbose", &(config.verbose_firehose as u32).to_string()), 279 | ("MaxDigestTableSizeInBytes", "8192"), // TODO: (low prio) 280 | ( 281 | "MaxPayloadSizeToTargetInBytes", 282 | &config.send_buffer_size.to_string(), 283 | ), 284 | // Zero-length-packet aware host 285 | ("ZLPAwareHost", "1"), 286 | ], 287 | )?; 288 | 289 | firehose_write(channel, &mut xml) 290 | } 291 | 292 | /// Do nothing, hopefully succesfully 293 | pub fn firehose_nop(channel: &mut T) -> anyhow::Result<()> { 294 | let mut xml = firehose_xml_setup("nop", &[("value", "ping")])?; 295 | 296 | firehose_write_getack(channel, &mut xml, "issue a NOP".to_owned()) 297 | } 298 | 299 | /// Get information about the physical partition of a storage medium (e.g. LUN) 300 | /// Prints to \ only 301 | pub fn firehose_get_storage_info( 302 | channel: &mut T, 303 | phys_part_idx: u8, 304 | ) -> anyhow::Result<()> { 305 | let mut xml = firehose_xml_setup( 306 | "getstorageinfo", 307 | &[("physical_partition_number", &phys_part_idx.to_string())], 308 | )?; 309 | 310 | firehose_write(channel, &mut xml)?; 311 | 312 | firehose_read::(channel, firehose_parser_ack_nak).and(Ok(())) 313 | } 314 | 315 | /// Alter Device (TODO: or Host) storage 316 | pub fn firehose_patch( 317 | channel: &mut T, 318 | byte_off: u64, 319 | slot: u8, 320 | phys_part_idx: u8, 321 | size: u64, 322 | start_sector: &str, 323 | val: &str, 324 | ) -> anyhow::Result<()> { 325 | let mut xml: Vec = firehose_xml_setup( 326 | "patch", 327 | &[ 328 | ( 329 | "SECTOR_SIZE_IN_BYTES", 330 | &channel.fh_config().storage_sector_size.to_string(), 331 | ), 332 | ("byte_offset", &byte_off.to_string()), 333 | ("filename", "DISK"), // DISK means "patch device's storage" 334 | ("slot", &slot.to_string()), 335 | ("physical_partition_number", &phys_part_idx.to_string()), 336 | ("size_in_bytes", &size.to_string()), 337 | ("start_sector", start_sector), 338 | ("value", val), 339 | ], 340 | )?; 341 | 342 | firehose_write_getack(channel, &mut xml, "patch".to_string()) 343 | } 344 | 345 | /// Peek at memory 346 | /// Prints to \ only 347 | pub fn firehose_peek( 348 | channel: &mut T, 349 | addr: u64, 350 | byte_count: u64, 351 | ) -> anyhow::Result<()> { 352 | if channel.fh_config().skip_firehose_log { 353 | println!( 354 | "{}", 355 | "Warning: firehose only prints to , remove --skip-firehose-log" 356 | .bright_red() 357 | ); 358 | } 359 | 360 | let mut xml: Vec = firehose_xml_setup( 361 | "peek", 362 | &[ 363 | ("address64", &addr.to_string()), 364 | ("size_in_bytes", &byte_count.to_string()), 365 | ], 366 | )?; 367 | 368 | firehose_write_getack(channel, &mut xml, format!("peek @ {addr:#x}")) 369 | } 370 | 371 | /// Poke at memory 372 | /// This can lead to lock-ups and resets 373 | // TODO:x 374 | pub fn firehose_poke( 375 | channel: &mut T, 376 | addr: u64, 377 | // TODO: byte count is 1..=8 378 | byte_count: u8, 379 | val: u64, 380 | ) -> anyhow::Result<()> { 381 | let mut xml: Vec = firehose_xml_setup( 382 | "poke", 383 | &[ 384 | ("address64", &addr.to_string()), 385 | ("size_in_bytes", &byte_count.to_string()), 386 | ("value", &val.to_string()), 387 | ], 388 | )?; 389 | 390 | firehose_write_getack(channel, &mut xml, format!("peek @ {addr:#x}")) 391 | } 392 | 393 | /// Write to Device storage 394 | pub fn firehose_program_storage( 395 | channel: &mut T, 396 | data: &mut impl Read, 397 | label: &str, 398 | num_sectors: usize, 399 | slot: u8, 400 | phys_part_idx: u8, 401 | start_sector: &str, 402 | ) -> anyhow::Result<()> { 403 | let mut sectors_left = num_sectors; 404 | let mut xml = firehose_xml_setup( 405 | "program", 406 | &[ 407 | ( 408 | "SECTOR_SIZE_IN_BYTES", 409 | &channel.fh_config().storage_sector_size.to_string(), 410 | ), 411 | ("num_partition_sectors", &num_sectors.to_string()), 412 | ("slot", &slot.to_string()), 413 | ("physical_partition_number", &phys_part_idx.to_string()), 414 | ("start_sector", start_sector), 415 | ( 416 | "read_back_verify", 417 | &(channel.fh_config().read_back_verify as u32).to_string(), 418 | ), 419 | ], 420 | )?; 421 | 422 | firehose_write(channel, &mut xml)?; 423 | 424 | if firehose_read::(channel, firehose_parser_ack_nak)? != FirehoseStatus::Ack { 425 | bail!(" was NAKed. Did you set sector-size correctly?"); 426 | } 427 | 428 | let mut pb = ProgressBar::new((sectors_left * channel.fh_config().storage_sector_size) as u64); 429 | pb.show_time_left = true; 430 | pb.message(&format!("Sending partition {label}: ")); 431 | pb.set_units(Units::Bytes); 432 | 433 | while sectors_left > 0 { 434 | let chunk_size_sectors = min( 435 | sectors_left, 436 | channel.fh_config().send_buffer_size / channel.fh_config().storage_sector_size, 437 | ); 438 | let mut buf = vec![ 439 | 0u8; 440 | min( 441 | channel.fh_config().send_buffer_size, 442 | chunk_size_sectors * channel.fh_config().storage_sector_size, 443 | ) 444 | ]; 445 | let _ = data.read(&mut buf).unwrap(); 446 | 447 | let n = channel.write(&buf).expect("Error sending data"); 448 | if n != chunk_size_sectors * channel.fh_config().storage_sector_size { 449 | bail!("Wrote an unexpected number of bytes ({})", n); 450 | } 451 | 452 | sectors_left -= chunk_size_sectors; 453 | pb.add((chunk_size_sectors * channel.fh_config().storage_sector_size) as u64); 454 | } 455 | 456 | // Send a Zero-Length Packet to indicate end of stream 457 | if channel.fh_config().backend == QdlBackend::Usb && !channel.fh_config().skip_usb_zlp { 458 | let _ = channel.write(&[]).expect("Error sending ZLP"); 459 | } 460 | 461 | if firehose_read::(channel, firehose_parser_ack_nak)? != FirehoseStatus::Ack { 462 | bail!("Failed to complete 'write' op"); 463 | } 464 | 465 | Ok(()) 466 | } 467 | 468 | /// Get a SHA256 digest of a portion of Device storage 469 | pub fn firehose_checksum_storage( 470 | channel: &mut T, 471 | num_sectors: usize, 472 | phys_part_idx: u8, 473 | start_sector: u32, 474 | ) -> anyhow::Result<()> { 475 | let mut xml = firehose_xml_setup( 476 | "getsha256digest", 477 | &[ 478 | ( 479 | "SECTOR_SIZE_IN_BYTES", 480 | &channel.fh_config().storage_sector_size.to_string(), 481 | ), 482 | ("num_partition_sectors", &num_sectors.to_string()), 483 | ("physical_partition_number", &phys_part_idx.to_string()), 484 | ("start_sector", &start_sector.to_string()), 485 | ], 486 | )?; 487 | 488 | firehose_write(channel, &mut xml)?; 489 | 490 | // TODO: figure out some sane way to figure out the timeout 491 | if firehose_read::(channel, firehose_parser_ack_nak)? != FirehoseStatus::Ack { 492 | bail!("Checksum request was NAKed"); 493 | } 494 | 495 | Ok(()) 496 | } 497 | 498 | /// Read (sector-aligned) parts of storage. 499 | pub fn firehose_read_storage( 500 | channel: &mut impl QdlChan, 501 | out: &mut impl Write, 502 | num_sectors: usize, 503 | slot: u8, 504 | phys_part_idx: u8, 505 | start_sector: u32, 506 | ) -> anyhow::Result<()> { 507 | let mut bytes_left = num_sectors * channel.fh_config().storage_sector_size; 508 | let mut xml = firehose_xml_setup( 509 | "read", 510 | &[ 511 | ( 512 | "SECTOR_SIZE_IN_BYTES", 513 | &channel.fh_config().storage_sector_size.to_string(), 514 | ), 515 | ("num_partition_sectors", &num_sectors.to_string()), 516 | ("slot", &slot.to_string()), 517 | ("physical_partition_number", &phys_part_idx.to_string()), 518 | ("start_sector", &start_sector.to_string()), 519 | ], 520 | )?; 521 | 522 | firehose_write(channel, &mut xml)?; 523 | if firehose_read(channel, firehose_parser_ack_nak)? != FirehoseStatus::Ack { 524 | bail!("Read request was NAKed"); 525 | } 526 | 527 | let mut pb = ProgressBar::new(bytes_left as u64); 528 | pb.set_units(Units::Bytes); 529 | 530 | let mut last_read_was_zero_len = false; 531 | while bytes_left > 0 { 532 | let chunk_size_bytes = min(bytes_left, channel.fh_config().recv_buffer_size); 533 | let mut buf = vec![0; chunk_size_bytes]; 534 | 535 | let n = channel.read(&mut buf).expect("Error receiving data"); 536 | if n == 0 { 537 | // TODO: need more robustness here 538 | /* Every 2 or 3 packets should be empty? */ 539 | last_read_was_zero_len = true; 540 | continue; 541 | } 542 | 543 | last_read_was_zero_len = false; 544 | let _ = out.write(&buf[..n])?; 545 | 546 | bytes_left -= n; 547 | pb.add(n as u64); 548 | } 549 | 550 | if !last_read_was_zero_len && channel.fh_config().backend == QdlBackend::Usb { 551 | // Issue a dummy read to drain the queue 552 | let _ = channel.read(&mut [])?; 553 | } 554 | 555 | if firehose_read(channel, firehose_parser_ack_nak)? != FirehoseStatus::Ack { 556 | bail!("Failed to complete 'read' op"); 557 | } 558 | 559 | Ok(()) 560 | } 561 | 562 | /// Reboot or power off the Device 563 | pub fn firehose_reset( 564 | channel: &mut T, 565 | mode: &FirehoseResetMode, 566 | delay_in_sec: u32, 567 | ) -> anyhow::Result<()> { 568 | let mut xml = firehose_xml_setup( 569 | "power", 570 | &[ 571 | ( 572 | "value", 573 | match mode { 574 | FirehoseResetMode::ResetToEdl => "reset_to_edl", 575 | FirehoseResetMode::Reset => "reset", 576 | FirehoseResetMode::Off => "off", 577 | }, 578 | ), 579 | ("DelayInSeconds", &delay_in_sec.to_string()), 580 | ], 581 | )?; 582 | 583 | firehose_write_getack(channel, &mut xml, "reset the Device".to_owned()) 584 | } 585 | 586 | /// Mark a physical storage partition as bootable 587 | pub fn firehose_set_bootable(channel: &mut T, drive_idx: u8) -> anyhow::Result<()> { 588 | let mut xml = firehose_xml_setup( 589 | "setbootablestoragedrive", 590 | &[("value", &drive_idx.to_string())], 591 | )?; 592 | 593 | firehose_write_getack( 594 | channel, 595 | &mut xml, 596 | format!("set partition {drive_idx} as bootable"), 597 | ) 598 | } 599 | 600 | pub fn firehose_get_default_sector_size(t: &str) -> Option { 601 | match FirehoseStorageType::from_str(t).unwrap() { 602 | FirehoseStorageType::Emmc => Some(512), 603 | FirehoseStorageType::Nand => Some(4096), 604 | FirehoseStorageType::Nvme => Some(512), 605 | FirehoseStorageType::Ufs => Some(4096), 606 | FirehoseStorageType::Spinor => Some(4096), 607 | } 608 | } 609 | -------------------------------------------------------------------------------- /qdl/src/sahara.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | // Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. 3 | use anstream::println; 4 | use owo_colors::OwoColorize; 5 | use pbr::{ProgressBar, Units}; 6 | use std::{ 7 | cmp::min, 8 | ffi::CStr, 9 | fs::File, 10 | io::{Read, Write}, 11 | mem::{self, size_of_val}, 12 | }; 13 | 14 | use anyhow::{Result, anyhow, bail}; 15 | 16 | use bincode::serialize; 17 | use serde::{self, Deserialize, Serialize}; 18 | use serde_repr::{Deserialize_repr, Serialize_repr}; 19 | 20 | use crate::types::{QdlBackend, QdlChan}; 21 | 22 | const SAHARA_STATUS_SUCCESS: u32 = 0; 23 | 24 | #[derive(Copy, Clone, Debug, PartialEq, Deserialize_repr, Serialize_repr)] 25 | #[repr(u32)] 26 | pub enum SaharaMode { 27 | WaitingForImage = 0x0, 28 | MemoryDebug = 0x2, 29 | Command = 0x3, 30 | } 31 | 32 | #[derive(Copy, Clone, Debug, Deserialize_repr, Serialize_repr)] 33 | #[repr(u32)] 34 | pub enum SaharaCmdModeCmd { 35 | Nop = 0x0, 36 | ReadSerialNum = 0x1, 37 | ReadHwId = 0x2, 38 | ReadOemKeyHash = 0x3, 39 | } 40 | 41 | // (De)serialize_repr works on C-like enums (match by value instead of entry index) 42 | #[derive(Copy, Clone, Debug, PartialEq, Deserialize_repr, Serialize_repr)] 43 | #[repr(u32)] 44 | pub enum SaharaCmd { 45 | SaharaHello = 0x1, /* Device sends HELLO at init */ 46 | SaharaHelloResp = 0x2, /* Host responds with a version number */ 47 | SaharaReadData = 0x3, /* Device requests an image to read */ 48 | SaharaEndOfImage = 0x4, /* Device signals EOF */ 49 | SaharaDone = 0x5, /* Host reassures the Device EOF was understood */ 50 | SaharaDoneResp = 0x6, /* Device requires more images if status == 0 */ 51 | SaharaReset = 0x7, /* Host asks Device to stop the current process */ 52 | SaharaResetResp = 0x8, /* Device acks the reset */ 53 | /* Proto >= 2.0 */ 54 | SaharaMemDebug = 0x9, 55 | SaharaMemRead = 0xa, 56 | /* Proto >= 2.1 */ 57 | SaharaCommandReady = 0xb, 58 | SaharaSwitchMode = 0xc, 59 | SaharaExecute = 0xd, 60 | SaharaExecuteResp = 0xe, 61 | SaharaExecuteData = 0xf, 62 | /* Proto >= 2.5 */ 63 | SaharaMemDebug64 = 0x10, 64 | SaharaMemRead64 = 0x11, 65 | /* Proto >= 2.8 */ 66 | SaharaReadData64 = 0x12, 67 | /* Proto >= 2.9 */ 68 | SaharaResetState = 0x13, 69 | /* Proto >= 3.0 */ 70 | SaharaWriteData = 0x14, 71 | 72 | /* This isn't part of spec, but rather "( 221 | channel: &mut T, 222 | img_arr: &mut [Vec], 223 | image_idx: u64, 224 | image_offset: u64, 225 | image_len: u64, 226 | ) -> Result { 227 | let image = if img_arr.len() == 1 { 0 } else { image_idx }; 228 | let buf = &mut img_arr[image as usize]; 229 | if (image_offset + image_len) as usize > buf.len() { 230 | bail!( 231 | "Attempted OOB read {} > {}", 232 | image_offset + image_len, 233 | buf.len() 234 | ); 235 | } 236 | 237 | channel 238 | .write(&buf[image_offset as usize..(image_offset + image_len) as usize]) 239 | .map_err(|e| e.into()) 240 | } 241 | 242 | fn sahara_send_generic( 243 | channel: &mut T, 244 | cmd: SaharaCmd, 245 | body: SaharaPacketBody, 246 | body_len: usize, 247 | ) -> Result { 248 | let pkt = SaharaPacket { 249 | cmd, 250 | len: (size_of_val(&cmd) + size_of::() + body_len) as u32, 251 | body, 252 | }; 253 | 254 | channel 255 | .write(&serialize(&pkt).expect("Error serializing packet")) 256 | .map_err(|e| e.into()) 257 | } 258 | 259 | const SAHARA_VERSION: u32 = 2; 260 | pub fn sahara_send_hello_rsp(channel: &mut T, mode: SaharaMode) -> Result { 261 | let data = HelloResp { 262 | ver: SAHARA_VERSION, 263 | compatible: 1, 264 | status: SAHARA_STATUS_SUCCESS, 265 | mode, 266 | unk0: 0, 267 | unk1: 0, 268 | unk2: 0, 269 | unk3: 0, 270 | unk4: 0, 271 | unk5: 0, 272 | }; 273 | 274 | sahara_send_generic( 275 | channel, 276 | SaharaCmd::SaharaHelloResp, 277 | SaharaPacketBody::HelloResp(data), 278 | size_of_val(&data), 279 | ) 280 | } 281 | 282 | pub fn sahara_send_done(channel: &mut T) -> Result { 283 | let data = DoneReq {}; 284 | 285 | sahara_send_generic( 286 | channel, 287 | SaharaCmd::SaharaDone, 288 | SaharaPacketBody::DoneReq(data), 289 | size_of_val(&data), 290 | ) 291 | } 292 | 293 | pub fn sahara_send_cmd_exec( 294 | channel: &mut T, 295 | command: SaharaCmdModeCmd, 296 | ) -> Result { 297 | sahara_send_generic( 298 | channel, 299 | SaharaCmd::SaharaExecute, 300 | SaharaPacketBody::Command(command), 301 | size_of_val(&command), 302 | ) 303 | } 304 | 305 | pub fn sahara_send_cmd_data( 306 | channel: &mut T, 307 | command: SaharaCmdModeCmd, 308 | ) -> Result { 309 | sahara_send_generic( 310 | channel, 311 | SaharaCmd::SaharaExecuteData, 312 | SaharaPacketBody::Command(command), 313 | size_of_val(&command), 314 | ) 315 | } 316 | 317 | pub fn sahara_reset(channel: &mut T) -> Result { 318 | let data = ResetReq {}; 319 | 320 | sahara_send_generic( 321 | channel, 322 | SaharaCmd::SaharaReset, 323 | SaharaPacketBody::ResetReq(data), 324 | size_of_val(&data), 325 | ) 326 | } 327 | 328 | pub fn sahara_switch_mode( 329 | channel: &mut T, 330 | mode: SaharaMode, 331 | ) -> Result { 332 | let data = SwitchMode { mode }; 333 | 334 | sahara_send_generic( 335 | channel, 336 | SaharaCmd::SaharaSwitchMode, 337 | SaharaPacketBody::SwitchMode(data), 338 | size_of_val(&data), 339 | ) 340 | } 341 | 342 | pub fn sahara_get_ramdump_tbl( 343 | channel: &mut T, 344 | addr: u64, 345 | len: u64, 346 | verbose: bool, 347 | ) -> Result, anyhow::Error> { 348 | let data = ReadMem64Req { addr, len }; 349 | 350 | sahara_send_generic( 351 | channel, 352 | SaharaCmd::SaharaMemRead64, 353 | SaharaPacketBody::ReadMem64Req(data), 354 | size_of_val(&data), 355 | )?; 356 | 357 | let entry_size = size_of::(); 358 | let num_chunks = len as usize / entry_size; 359 | let mut tbl = Vec::::with_capacity(num_chunks); 360 | 361 | let mut buf = vec![0u8; len as usize]; 362 | channel.read_exact(&mut buf)?; 363 | 364 | if verbose { 365 | println!("Available images:"); 366 | } 367 | for i in 0..num_chunks { 368 | let entry = bincode::deserialize::(&buf[i * entry_size..])?; 369 | tbl.push(entry); 370 | if verbose { 371 | println!( 372 | "\t{} (0x{:x} @ 0x{:x}){}", 373 | String::from_utf8(entry.filename.to_vec())?, 374 | entry.len, 375 | entry.base, 376 | match entry.save_pref { 377 | 0 => "", 378 | _ => " *", 379 | } 380 | ); 381 | } 382 | } 383 | 384 | Ok(tbl) 385 | } 386 | 387 | fn sahara_dump_region( 388 | channel: &mut T, 389 | entry: RamdumpTable64, 390 | output: &mut impl Write, 391 | ) -> Result<()> { 392 | let mut pb = ProgressBar::new(entry.len); 393 | pb.show_time_left = true; 394 | pb.message(&format!( 395 | "Dumping {}: ", 396 | String::from_utf8(entry.filename.to_vec())? 397 | )); 398 | pb.set_units(Units::Bytes); 399 | 400 | let mut bytes_read = 0usize; 401 | while bytes_read < entry.len as usize { 402 | let chunk_size = min(4096, entry.len as usize - bytes_read); 403 | let mut buf = vec![0u8; chunk_size]; 404 | let data = ReadMem64Req { 405 | addr: entry.base + bytes_read as u64, 406 | len: chunk_size as u64, 407 | }; 408 | 409 | sahara_send_generic( 410 | channel, 411 | SaharaCmd::SaharaMemRead64, 412 | SaharaPacketBody::ReadMem64Req(data), 413 | size_of_val(&data), 414 | )?; 415 | channel.flush()?; 416 | 417 | bytes_read += channel.read(&mut buf)?; 418 | 419 | // Issue a dummy read to consume the ZLP 420 | if channel.fh_config().backend == QdlBackend::Usb && buf.len().is_multiple_of(512) { 421 | let _ = channel.read(&mut []); 422 | } 423 | 424 | pb.set(bytes_read as u64); 425 | let _ = output.write(&buf)?; 426 | } 427 | 428 | Ok(()) 429 | } 430 | 431 | pub fn sahara_dump_regions( 432 | channel: &mut T, 433 | dump_tbl: Vec, 434 | regions_to_dump: Vec, 435 | ) -> Result<()> { 436 | // Make all of them lowercase for better UX 437 | let regions_to_dump = regions_to_dump 438 | .iter() 439 | .map(|rname| rname.to_ascii_lowercase()) 440 | .collect::>(); 441 | 442 | std::fs::create_dir_all("ramdump/")?; 443 | let filtered_list: Vec = match regions_to_dump.len() { 444 | // Dump everything with save_pref == true if no argument was provided 445 | 0 => dump_tbl 446 | .iter() 447 | .filter(|e| e.save_pref != 0) 448 | .copied() 449 | .collect(), 450 | _ => dump_tbl 451 | .iter() 452 | .filter(|dump_entry| { 453 | regions_to_dump.contains( 454 | &String::from_utf8(dump_entry.filename.to_vec()) 455 | .unwrap_or("".to_owned()) 456 | .to_ascii_lowercase() 457 | .split('.') // Ignore file extensions proposed by ramdump 458 | .next() 459 | .unwrap_or("") 460 | .to_owned(), 461 | ) 462 | }) 463 | .copied() 464 | .collect(), 465 | }; 466 | for entry in filtered_list { 467 | let fname = CStr::from_bytes_until_nul(&entry.filename) 468 | .unwrap() 469 | .to_str()? 470 | .to_owned(); 471 | 472 | let mut f = File::create(std::path::Path::new(&format!("ramdump/{fname}")))?; 473 | sahara_dump_region(channel, entry, &mut f)?; 474 | } 475 | 476 | Ok(()) 477 | } 478 | 479 | pub fn sahara_run( 480 | channel: &mut T, 481 | sahara_mode: SaharaMode, 482 | sahara_command: Option, 483 | images: &mut [Vec], 484 | filenames: Vec, 485 | verbose: bool, 486 | ) -> Result> { 487 | let mut buf = vec![0; 4096]; 488 | 489 | loop { 490 | let bytes_read = channel.read(&mut buf[..])?; 491 | let pkt = sahara_parse_packet(&buf[..bytes_read], verbose)?; 492 | let pktsize = size_of_val(&pkt.cmd) + size_of_val(&pkt.len); 493 | 494 | match pkt.cmd { 495 | SaharaCmd::SaharaHello => { 496 | if let SaharaPacketBody::HelloReq(req) = pkt.body { 497 | assert_eq!(pkt.len as usize, pktsize + mem::size_of::()); 498 | 499 | // MemoryDebug mode can only be entered if the device offers it 500 | let mode = if sahara_mode == SaharaMode::MemoryDebug 501 | && req.mode == SaharaMode::MemoryDebug 502 | { 503 | SaharaMode::MemoryDebug 504 | } else { 505 | sahara_mode 506 | }; 507 | sahara_send_hello_rsp(channel, mode)?; 508 | } 509 | } 510 | SaharaCmd::SaharaReadData => { 511 | if let SaharaPacketBody::ReadReq(rr) = pkt.body { 512 | assert_eq!(pkt.len as usize, pktsize + mem::size_of::()); 513 | sahara_send_img_to_device( 514 | channel, 515 | images, 516 | rr.image as u64, 517 | rr.offset as u64, 518 | rr.len as u64, 519 | )?; 520 | } 521 | } 522 | SaharaCmd::SaharaEndOfImage => { 523 | if let SaharaPacketBody::Eoi(req) = pkt.body { 524 | assert_eq!(pkt.len as usize, pktsize + mem::size_of::()); 525 | 526 | if req.status == 0 { 527 | sahara_send_done(channel)?; 528 | } else { 529 | eprintln!("Received unsuccessful End of Image packet"); 530 | return Ok(vec![]); 531 | } 532 | } 533 | } 534 | SaharaCmd::SaharaDoneResp => { 535 | if let SaharaPacketBody::DoneResp(req) = pkt.body 536 | && (req.status == 1 /* COMPLETE */ /* 8916 bug */ || 537 | images.len() == 1) 538 | { 539 | println!("{}", "Loader sent. Hack away!".green()); 540 | return Ok(vec![]); 541 | } 542 | } 543 | SaharaCmd::SaharaCommandReady => { 544 | assert_eq!(pkt.len as usize, pktsize); 545 | match sahara_command { 546 | Some(cmd) => sahara_send_cmd_exec(channel, cmd), 547 | None => bail!("Missing sahara command"), 548 | }?; 549 | } 550 | SaharaCmd::SaharaExecuteResp => { 551 | if let SaharaPacketBody::ExecResp(resp) = pkt.body { 552 | let mut resp_buf = vec![0u8; resp.len as usize]; 553 | 554 | // Indicate we're ready to receive the requested amount of data 555 | sahara_send_cmd_data(channel, resp.command)?; 556 | 557 | let resp_len = channel.read(&mut resp_buf)?; 558 | assert_eq!(resp_len, resp.len as usize); 559 | 560 | // Got everything we want, exit command mode 561 | sahara_switch_mode(channel, SaharaMode::WaitingForImage)?; 562 | 563 | return Ok(resp_buf); 564 | } 565 | } 566 | SaharaCmd::SaharaMemDebug64 => { 567 | if let SaharaPacketBody::Debug64Req(req) = pkt.body { 568 | assert_eq!(pkt.len as usize, pktsize + mem::size_of::()); 569 | 570 | // Receive the dump info table 571 | let dump_tbl = sahara_get_ramdump_tbl(channel, req.addr, req.len, verbose)?; 572 | 573 | // Grab some (possibly all) of the available regions 574 | sahara_dump_regions(channel, dump_tbl, filenames)?; 575 | 576 | return Ok(vec![]); 577 | } 578 | } 579 | SaharaCmd::SaharaReadData64 => { 580 | if let SaharaPacketBody::ReadData64Req(rr) = pkt.body { 581 | assert_eq!(pkt.len as usize, pktsize + mem::size_of::()); 582 | sahara_send_img_to_device(channel, images, rr.image, rr.offset, rr.len)?; 583 | } 584 | } 585 | SaharaCmd::SaharaResetResp => { 586 | assert_eq!(pkt.len as usize, pktsize); 587 | } 588 | SaharaCmd::SaharaXML => { 589 | // Todo: make this optionally "fine" 590 | println!("Device booted into the loader already"); 591 | return Ok(vec![]); 592 | } 593 | _ => todo!("Got packet {:?}", pkt), 594 | } 595 | } 596 | } 597 | 598 | fn sahara_parse_packet(buf: &[u8], verbose: bool) -> Result { 599 | let (cmd, rest) = buf 600 | .split_first_chunk::<4>() 601 | .ok_or_else(|| anyhow!("Malformed packet, too short: {buf:?}"))?; 602 | let (len, args) = rest 603 | .split_first_chunk::<4>() 604 | .ok_or_else(|| anyhow!("Malformed packet, too short: {buf:?}"))?; 605 | 606 | let cmd = bincode::deserialize::(cmd) 607 | .unwrap_or_else(|_| panic!("Got unknown command {}", u32::from_le_bytes(*cmd))); 608 | 609 | let ret = SaharaPacket { 610 | cmd, 611 | len: u32::from_le_bytes(*len), 612 | body: match cmd { 613 | SaharaCmd::SaharaHello => { 614 | SaharaPacketBody::HelloReq(bincode::deserialize::(args).unwrap()) 615 | } 616 | SaharaCmd::SaharaHelloResp => { 617 | SaharaPacketBody::HelloResp(bincode::deserialize::(args).unwrap()) 618 | } 619 | SaharaCmd::SaharaReadData => { 620 | SaharaPacketBody::ReadReq(bincode::deserialize::(args).unwrap()) 621 | } 622 | SaharaCmd::SaharaEndOfImage => { 623 | SaharaPacketBody::Eoi(bincode::deserialize::(args).unwrap()) 624 | } 625 | SaharaCmd::SaharaDone => SaharaPacketBody::DoneReq(DoneReq {}), 626 | SaharaCmd::SaharaDoneResp => { 627 | SaharaPacketBody::DoneResp(bincode::deserialize::(args).unwrap()) 628 | } 629 | SaharaCmd::SaharaResetResp => SaharaPacketBody::ResetResp(ResetResp {}), 630 | SaharaCmd::SaharaCommandReady => SaharaPacketBody::CommandReady(CommandReady {}), 631 | SaharaCmd::SaharaExecuteResp => { 632 | SaharaPacketBody::ExecResp(bincode::deserialize::(args).unwrap()) 633 | } 634 | SaharaCmd::SaharaExecuteData => { 635 | SaharaPacketBody::Command(bincode::deserialize::(args).unwrap()) 636 | } 637 | SaharaCmd::SaharaMemDebug64 => { 638 | SaharaPacketBody::Debug64Req(bincode::deserialize::(args).unwrap()) 639 | } 640 | SaharaCmd::SaharaMemRead64 => { 641 | SaharaPacketBody::ReadMem64Req(bincode::deserialize::(args).unwrap()) 642 | } 643 | SaharaCmd::SaharaReadData64 => SaharaPacketBody::ReadData64Req( 644 | bincode::deserialize::(args).unwrap(), 645 | ), 646 | SaharaCmd::SaharaXML => bail!( 647 | "Got Firehose command while expecting Sahara command: {:?}", 648 | String::from_utf8_lossy(buf) 649 | ), 650 | _ => bail!("Got unimplemented command: {:?}", buf), 651 | }, 652 | }; 653 | 654 | if verbose { 655 | println!("{:?}", ret); 656 | } 657 | 658 | Ok(ret) 659 | } 660 | --------------------------------------------------------------------------------