mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7c872be3e | |||
| cf1ab6ecca | |||
| 7364e8d725 | |||
| 935a86e72c | |||
| a9a6107043 | |||
| 3c1d5a8bb3 | |||
| 26ec8098e5 |
@@ -7,6 +7,11 @@
|
|||||||
to update the actual links at the bottom of the file.
|
to update the actual links at the bottom of the file.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### Version 2.3.3
|
||||||
|
|
||||||
|
* Add support for XZ-compressed ramdisks ([Issue #203], [PR #207])
|
||||||
|
* Merge property and kernel command line AVB descriptors when replacing partitions ([Issue #203], [PR #208])
|
||||||
|
|
||||||
### Version 2.3.2
|
### Version 2.3.2
|
||||||
|
|
||||||
* Improve error messages when using `--replace` with an image that has the wrong AVB descriptor type ([Issue #201], [PR #202])
|
* Improve error messages when using `--replace` with an image that has the wrong AVB descriptor type ([Issue #201], [PR #202])
|
||||||
@@ -159,3 +164,5 @@ Behind-the-scenes changes:
|
|||||||
[PR #202]: https://github.com/chenxiaolong/avbroot/pull/202
|
[PR #202]: https://github.com/chenxiaolong/avbroot/pull/202
|
||||||
[PR #205]: https://github.com/chenxiaolong/avbroot/pull/205
|
[PR #205]: https://github.com/chenxiaolong/avbroot/pull/205
|
||||||
[PR #206]: https://github.com/chenxiaolong/avbroot/pull/206
|
[PR #206]: https://github.com/chenxiaolong/avbroot/pull/206
|
||||||
|
[PR #207]: https://github.com/chenxiaolong/avbroot/pull/207
|
||||||
|
[PR #208]: https://github.com/chenxiaolong/avbroot/pull/208
|
||||||
|
|||||||
Generated
+4
-4
@@ -121,7 +121,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "avbroot"
|
name = "avbroot"
|
||||||
version = "2.3.2"
|
version = "2.3.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_matches",
|
"assert_matches",
|
||||||
@@ -565,7 +565,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "e2e"
|
name = "e2e"
|
||||||
version = "2.3.2"
|
version = "2.3.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"attohttpc",
|
"attohttpc",
|
||||||
@@ -679,7 +679,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fuzz"
|
name = "fuzz"
|
||||||
version = "2.3.2"
|
version = "2.3.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"avbroot",
|
"avbroot",
|
||||||
"honggfuzz",
|
"honggfuzz",
|
||||||
@@ -2104,7 +2104,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "2.3.2"
|
version = "2.3.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "2.3.2"
|
version = "2.3.3"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/chenxiaolong/avbroot"
|
repository = "https://github.com/chenxiaolong/avbroot"
|
||||||
|
|||||||
+124
-42
@@ -400,6 +400,128 @@ fn get_vbmeta_patch_order(
|
|||||||
Ok(order)
|
Ok(order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Copy the hash or hashtree descriptor from the child image header into the
|
||||||
|
/// parent image header if the child is unsigned or update the parent's chain
|
||||||
|
/// descriptor if the child is signed. The existing descriptor in the parent
|
||||||
|
/// must have the same type as the child.
|
||||||
|
fn update_security_descriptors(
|
||||||
|
parent_header: &mut Header,
|
||||||
|
child_header: &Header,
|
||||||
|
parent_name: &str,
|
||||||
|
child_name: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
// This can't fail since the descriptor must have existed for the dependency
|
||||||
|
// to exist.
|
||||||
|
let parent_descriptor = parent_header
|
||||||
|
.descriptors
|
||||||
|
.iter_mut()
|
||||||
|
.find(|d| d.partition_name() == Some(child_name))
|
||||||
|
.unwrap();
|
||||||
|
let parent_type = parent_descriptor.type_name();
|
||||||
|
|
||||||
|
if child_header.public_key.is_empty() {
|
||||||
|
// vbmeta is unsigned. Copy the child's existing descriptor.
|
||||||
|
let Some(child_descriptor) = child_header
|
||||||
|
.descriptors
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.partition_name() == Some(child_name))
|
||||||
|
else {
|
||||||
|
bail!("{child_name} has no descriptor for itself");
|
||||||
|
};
|
||||||
|
let child_type = child_descriptor.type_name();
|
||||||
|
|
||||||
|
match (parent_descriptor, child_descriptor) {
|
||||||
|
(Descriptor::Hash(pd), Descriptor::Hash(cd)) => {
|
||||||
|
*pd = cd.clone();
|
||||||
|
}
|
||||||
|
(Descriptor::HashTree(pd), Descriptor::HashTree(cd)) => {
|
||||||
|
*pd = cd.clone();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
bail!("{child_name} descriptor ({child_type}) does not match entry in {parent_name} ({parent_type})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// vbmeta is signed; Use a chain descriptor.
|
||||||
|
match parent_descriptor {
|
||||||
|
Descriptor::ChainPartition(pd) => {
|
||||||
|
pd.public_key = child_header.public_key.clone();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
bail!("{child_name} descriptor ({parent_type}) in {parent_name} must be a chain descriptor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the text before the first equal sign in the kernel command line if it is
|
||||||
|
/// not empty.
|
||||||
|
fn cmdline_prefix(cmdline: &str) -> Option<&str> {
|
||||||
|
let Some((prefix, _)) = cmdline.split_once('=') else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if prefix.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge property descriptors and kernel command line descriptors from the
|
||||||
|
/// child into the parent. The property descriptors are matched based on the
|
||||||
|
/// entire property key. The kernel command line descriptors are matched based
|
||||||
|
/// on the non-empty text left of the first equal sign (if it exists).
|
||||||
|
///
|
||||||
|
/// This is a no-op if the child is signed because it is expected to be chain
|
||||||
|
/// loaded by the parent.
|
||||||
|
fn update_metadata_descriptors(parent_header: &mut Header, child_header: &Header) {
|
||||||
|
if !child_header.public_key.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for child_descriptor in &child_header.descriptors {
|
||||||
|
match child_descriptor {
|
||||||
|
Descriptor::Property(cd) => {
|
||||||
|
let parent_property = parent_header.descriptors.iter_mut().find_map(|d| match d {
|
||||||
|
Descriptor::Property(p) if p.key == cd.key => Some(p),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(pd) = parent_property {
|
||||||
|
pd.value = cd.value.clone();
|
||||||
|
} else {
|
||||||
|
parent_header
|
||||||
|
.descriptors
|
||||||
|
.push(Descriptor::Property(cd.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Descriptor::KernelCmdline(cd) => {
|
||||||
|
let Some(prefix) = cmdline_prefix(&cd.cmdline) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let parent_property = parent_header.descriptors.iter_mut().find_map(|d| match d {
|
||||||
|
Descriptor::KernelCmdline(p) if cmdline_prefix(&p.cmdline) == Some(prefix) => {
|
||||||
|
Some(p)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(pd) = parent_property {
|
||||||
|
pd.cmdline = cd.cmdline.clone();
|
||||||
|
} else {
|
||||||
|
parent_header
|
||||||
|
.descriptors
|
||||||
|
.push(Descriptor::KernelCmdline(cd.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Update vbmeta headers.
|
/// Update vbmeta headers.
|
||||||
///
|
///
|
||||||
/// * If [`Header::flags`] is non-zero, then an error is returned because the
|
/// * If [`Header::flags`] is non-zero, then an error is returned because the
|
||||||
@@ -438,52 +560,12 @@ fn update_vbmeta_headers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for dep in deps.iter() {
|
for dep in deps.iter() {
|
||||||
// This can't fail since the descriptor must have existed for the
|
|
||||||
// dependency to exist.
|
|
||||||
let parent_descriptor = parent_header
|
|
||||||
.descriptors
|
|
||||||
.iter_mut()
|
|
||||||
.find(|d| d.partition_name() == Some(dep))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let reader = images.get_mut(dep).unwrap();
|
let reader = images.get_mut(dep).unwrap();
|
||||||
let (header, _, _) = avb::load_image(reader)
|
let (header, _, _) = avb::load_image(reader)
|
||||||
.with_context(|| format!("Failed to load vbmeta footer from image: {dep}"))?;
|
.with_context(|| format!("Failed to load vbmeta footer from image: {dep}"))?;
|
||||||
let pd_type = parent_descriptor.type_name();
|
|
||||||
|
|
||||||
if header.public_key.is_empty() {
|
update_security_descriptors(parent_header, &header, name, dep)?;
|
||||||
// vbmeta is unsigned. Use the existing descriptor.
|
update_metadata_descriptors(parent_header, &header);
|
||||||
let Some(descriptor) = header
|
|
||||||
.descriptors
|
|
||||||
.iter()
|
|
||||||
.find(|d| d.partition_name() == Some(dep))
|
|
||||||
else {
|
|
||||||
bail!("{name} has no descriptor for itself");
|
|
||||||
};
|
|
||||||
let d_type = descriptor.type_name();
|
|
||||||
|
|
||||||
match (parent_descriptor, descriptor) {
|
|
||||||
(Descriptor::Hash(pd), Descriptor::Hash(d)) => {
|
|
||||||
*pd = d.clone();
|
|
||||||
}
|
|
||||||
(Descriptor::HashTree(pd), Descriptor::HashTree(d)) => {
|
|
||||||
*pd = d.clone();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
bail!("{dep} descriptor ({d_type}) does not match entry in {name} ({pd_type})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// vbmeta is signed; Use a chain descriptor.
|
|
||||||
match parent_descriptor {
|
|
||||||
Descriptor::ChainPartition(d) => {
|
|
||||||
d.public_key = header.public_key;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
bail!("{dep} descriptor ({pd_type}) in {name} must be a chain descriptor");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only sign and rewrite the image if we need to. Some vbmeta images may
|
// Only sign and rewrite the image if we need to. Some vbmeta images may
|
||||||
|
|||||||
@@ -10,14 +10,22 @@ use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
|||||||
use lz4_flex::frame::FrameDecoder;
|
use lz4_flex::frame::FrameDecoder;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use xz2::{
|
||||||
|
read::XzDecoder,
|
||||||
|
stream::{Check, Stream},
|
||||||
|
write::XzEncoder,
|
||||||
|
};
|
||||||
|
|
||||||
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
|
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
|
||||||
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
|
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
|
||||||
|
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Unknown compression format")]
|
#[error("Unknown compression format")]
|
||||||
UnknownFormat,
|
UnknownFormat,
|
||||||
|
#[error("XZ stream error")]
|
||||||
|
XzStream(#[from] xz2::stream::Error),
|
||||||
#[error("I/O error")]
|
#[error("I/O error")]
|
||||||
Io(#[from] io::Error),
|
Io(#[from] io::Error),
|
||||||
}
|
}
|
||||||
@@ -102,25 +110,29 @@ pub enum CompressedFormat {
|
|||||||
None,
|
None,
|
||||||
Gzip,
|
Gzip,
|
||||||
Lz4Legacy,
|
Lz4Legacy,
|
||||||
|
Xz,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum CompressedReader<R: Read> {
|
pub enum CompressedReader<R: Read> {
|
||||||
None(R),
|
None(R),
|
||||||
Gzip(GzDecoder<R>),
|
Gzip(GzDecoder<R>),
|
||||||
Lz4(FrameDecoder<R>),
|
Lz4(FrameDecoder<R>),
|
||||||
|
Xz(XzDecoder<R>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Read + Seek> CompressedReader<R> {
|
impl<R: Read + Seek> CompressedReader<R> {
|
||||||
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
|
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
|
||||||
let mut magic = [0u8; 4];
|
let mut magic = [0u8; 6];
|
||||||
reader.read_exact(&mut magic)?;
|
reader.read_exact(&mut magic)?;
|
||||||
|
|
||||||
reader.rewind()?;
|
reader.rewind()?;
|
||||||
|
|
||||||
if &magic[0..2] == GZIP_MAGIC {
|
if &magic[0..2] == GZIP_MAGIC {
|
||||||
Ok(Self::Gzip(GzDecoder::new(reader)))
|
Ok(Self::Gzip(GzDecoder::new(reader)))
|
||||||
} else if &magic == LZ4_LEGACY_MAGIC {
|
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
|
||||||
Ok(Self::Lz4(FrameDecoder::new(reader)))
|
Ok(Self::Lz4(FrameDecoder::new(reader)))
|
||||||
|
} else if &magic == XZ_MAGIC {
|
||||||
|
Ok(Self::Xz(XzDecoder::new(reader)))
|
||||||
} else if raw_if_unknown {
|
} else if raw_if_unknown {
|
||||||
Ok(Self::None(reader))
|
Ok(Self::None(reader))
|
||||||
} else {
|
} else {
|
||||||
@@ -133,6 +145,7 @@ impl<R: Read + Seek> CompressedReader<R> {
|
|||||||
Self::None(_) => CompressedFormat::None,
|
Self::None(_) => CompressedFormat::None,
|
||||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||||
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
|
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
|
||||||
|
Self::Xz(_) => CompressedFormat::Xz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +154,7 @@ impl<R: Read + Seek> CompressedReader<R> {
|
|||||||
Self::None(r) => r,
|
Self::None(r) => r,
|
||||||
Self::Gzip(r) => r.into_inner(),
|
Self::Gzip(r) => r.into_inner(),
|
||||||
Self::Lz4(r) => r.into_inner(),
|
Self::Lz4(r) => r.into_inner(),
|
||||||
|
Self::Xz(r) => r.into_inner(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,6 +165,7 @@ impl<R: Read> Read for CompressedReader<R> {
|
|||||||
Self::None(r) => r.read(buf),
|
Self::None(r) => r.read(buf),
|
||||||
Self::Gzip(r) => r.read(buf),
|
Self::Gzip(r) => r.read(buf),
|
||||||
Self::Lz4(r) => r.read(buf),
|
Self::Lz4(r) => r.read(buf),
|
||||||
|
Self::Xz(r) => r.read(buf),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,6 +174,7 @@ pub enum CompressedWriter<W: Write> {
|
|||||||
None(W),
|
None(W),
|
||||||
Gzip(GzEncoder<W>),
|
Gzip(GzEncoder<W>),
|
||||||
Lz4Legacy(Lz4LegacyEncoder<W>),
|
Lz4Legacy(Lz4LegacyEncoder<W>),
|
||||||
|
Xz(XzEncoder<W>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: Write> CompressedWriter<W> {
|
impl<W: Write> CompressedWriter<W> {
|
||||||
@@ -169,6 +185,11 @@ impl<W: Write> CompressedWriter<W> {
|
|||||||
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
|
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
|
||||||
}
|
}
|
||||||
CompressedFormat::Lz4Legacy => Ok(Self::Lz4Legacy(Lz4LegacyEncoder::new(writer)?)),
|
CompressedFormat::Lz4Legacy => Ok(Self::Lz4Legacy(Lz4LegacyEncoder::new(writer)?)),
|
||||||
|
CompressedFormat::Xz => {
|
||||||
|
// Some kernels are compiled without support for the default CRC64.
|
||||||
|
let stream = Stream::new_easy_encoder(6, Check::Crc32)?;
|
||||||
|
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +198,7 @@ impl<W: Write> CompressedWriter<W> {
|
|||||||
Self::None(_) => CompressedFormat::None,
|
Self::None(_) => CompressedFormat::None,
|
||||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||||
Self::Lz4Legacy(_) => CompressedFormat::Lz4Legacy,
|
Self::Lz4Legacy(_) => CompressedFormat::Lz4Legacy,
|
||||||
|
Self::Xz(_) => CompressedFormat::Xz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +207,7 @@ impl<W: Write> CompressedWriter<W> {
|
|||||||
Self::None(w) => Ok(w),
|
Self::None(w) => Ok(w),
|
||||||
Self::Gzip(w) => w.finish(),
|
Self::Gzip(w) => w.finish(),
|
||||||
Self::Lz4Legacy(w) => w.finish(),
|
Self::Lz4Legacy(w) => w.finish(),
|
||||||
|
Self::Xz(w) => w.finish(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,6 +218,7 @@ impl<W: Write> Write for CompressedWriter<W> {
|
|||||||
Self::None(w) => w.write(buf),
|
Self::None(w) => w.write(buf),
|
||||||
Self::Gzip(w) => w.write(buf),
|
Self::Gzip(w) => w.write(buf),
|
||||||
Self::Lz4Legacy(w) => w.write(buf),
|
Self::Lz4Legacy(w) => w.write(buf),
|
||||||
|
Self::Xz(w) => w.write(buf),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +227,7 @@ impl<W: Write> Write for CompressedWriter<W> {
|
|||||||
Self::None(w) => w.flush(),
|
Self::None(w) => w.flush(),
|
||||||
Self::Gzip(w) => w.flush(),
|
Self::Gzip(w) => w.flush(),
|
||||||
Self::Lz4Legacy(w) => w.flush(),
|
Self::Lz4Legacy(w) => w.flush(),
|
||||||
|
Self::Xz(w) => w.flush(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
id=com.chiller3.avbroot.clearotacerts
|
id=com.chiller3.avbroot.clearotacerts
|
||||||
name=clearotacerts
|
name=clearotacerts
|
||||||
version=v2.3.2
|
version=v2.3.3
|
||||||
versionCode=131842
|
versionCode=131843
|
||||||
author=chenxiaolong
|
author=chenxiaolong
|
||||||
description=Block A/B OTAs by clearing verification certificates
|
description=Block A/B OTAs by clearing verification certificates
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
id=com.chiller3.avbroot.oemunlockonboot
|
id=com.chiller3.avbroot.oemunlockonboot
|
||||||
name=oemunlockonboot
|
name=oemunlockonboot
|
||||||
version=v2.3.2
|
version=v2.3.3
|
||||||
versionCode=131842
|
versionCode=131843
|
||||||
author=chenxiaolong
|
author=chenxiaolong
|
||||||
description=Enable OEM unlocking on every boot
|
description=Enable OEM unlocking on every boot
|
||||||
|
|||||||
Reference in New Issue
Block a user