mirror of
https://github.com/u-boot/u-boot.git
synced 2025-04-11 07:24:46 +00:00

- avb_slot_verify_data_free() doesn't check its data parameter - out_data can be null if avb_slot_verify() fails to allocate memory Signed-off-by: Gary Bisson <bisson.gary@gmail.com> Reviewed-by: Mattijs Korpershoek <mkorpershoek@kernel.org> Link: https://lore.kernel.org/r/20250402144219.1875067-1-bisson.gary@gmail.com Signed-off-by: Mattijs Korpershoek <mkorpershoek@kernel.org>
626 lines
16 KiB
C
626 lines
16 KiB
C
// SPDX-License-Identifier: GPL-2.0+
|
|
/*
|
|
* Bootmeth for Android
|
|
*
|
|
* Copyright (C) 2024 BayLibre, SAS
|
|
* Written by Mattijs Korpershoek <mkorpershoek@baylibre.com>
|
|
*/
|
|
#define LOG_CATEGORY UCLASS_BOOTSTD
|
|
|
|
#include <android_ab.h>
|
|
#include <android_image.h>
|
|
#if CONFIG_IS_ENABLED(AVB_VERIFY)
|
|
#include <avb_verify.h>
|
|
#endif
|
|
#include <bcb.h>
|
|
#include <blk.h>
|
|
#include <bootflow.h>
|
|
#include <bootm.h>
|
|
#include <bootmeth.h>
|
|
#include <dm.h>
|
|
#include <image.h>
|
|
#include <malloc.h>
|
|
#include <mapmem.h>
|
|
#include <part.h>
|
|
#include <version.h>
|
|
#include "bootmeth_android.h"
|
|
|
|
#define BCB_FIELD_COMMAND_SZ 32
|
|
#define BCB_PART_NAME "misc"
|
|
#define BOOT_PART_NAME "boot"
|
|
#define VENDOR_BOOT_PART_NAME "vendor_boot"
|
|
#define SLOT_LEN 2
|
|
|
|
/**
|
|
* struct android_priv - Private data
|
|
*
|
|
* This is read from the disk and recorded for use when the full Android
|
|
* kernel must be loaded and booted
|
|
*
|
|
* @boot_mode: Requested boot mode (normal, recovery, bootloader)
|
|
* @slot: Nul-terminated partition slot suffix read from BCB ("a\0" or "b\0")
|
|
* @header_version: Android boot image header version
|
|
*/
|
|
struct android_priv {
|
|
enum android_boot_mode boot_mode;
|
|
char *slot;
|
|
u32 header_version;
|
|
u32 boot_img_size;
|
|
u32 vendor_boot_img_size;
|
|
};
|
|
|
|
static int android_check(struct udevice *dev, struct bootflow_iter *iter)
|
|
{
|
|
/* This only works on mmc devices */
|
|
if (bootflow_iter_check_mmc(iter))
|
|
return log_msg_ret("mmc", -ENOTSUPP);
|
|
|
|
/*
|
|
* This only works on whole devices, as multiple
|
|
* partitions are needed to boot Android
|
|
*/
|
|
if (iter->part != 0)
|
|
return log_msg_ret("mmc part", -ENOTSUPP);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int scan_boot_part(struct udevice *blk, struct android_priv *priv)
|
|
{
|
|
struct blk_desc *desc = dev_get_uclass_plat(blk);
|
|
struct disk_partition partition;
|
|
char partname[PART_NAME_LEN];
|
|
ulong num_blks, bufsz;
|
|
char *buf;
|
|
int ret;
|
|
|
|
if (priv->slot)
|
|
sprintf(partname, BOOT_PART_NAME "_%s", priv->slot);
|
|
else
|
|
sprintf(partname, BOOT_PART_NAME);
|
|
|
|
ret = part_get_info_by_name(desc, partname, &partition);
|
|
if (ret < 0)
|
|
return log_msg_ret("part info", ret);
|
|
|
|
num_blks = DIV_ROUND_UP(sizeof(struct andr_boot_img_hdr_v0), desc->blksz);
|
|
bufsz = num_blks * desc->blksz;
|
|
buf = malloc(bufsz);
|
|
if (!buf)
|
|
return log_msg_ret("buf", -ENOMEM);
|
|
|
|
ret = blk_read(blk, partition.start, num_blks, buf);
|
|
if (ret != num_blks) {
|
|
free(buf);
|
|
return log_msg_ret("part read", -EIO);
|
|
}
|
|
|
|
if (!is_android_boot_image_header(buf)) {
|
|
free(buf);
|
|
return log_msg_ret("header", -ENOENT);
|
|
}
|
|
|
|
if (!android_image_get_bootimg_size(buf, &priv->boot_img_size)) {
|
|
free(buf);
|
|
return log_msg_ret("get bootimg size", -EINVAL);
|
|
}
|
|
|
|
priv->header_version = ((struct andr_boot_img_hdr_v0 *)buf)->header_version;
|
|
|
|
free(buf);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int scan_vendor_boot_part(struct udevice *blk, struct android_priv *priv)
|
|
{
|
|
struct blk_desc *desc = dev_get_uclass_plat(blk);
|
|
struct disk_partition partition;
|
|
char partname[PART_NAME_LEN];
|
|
ulong num_blks, bufsz;
|
|
char *buf;
|
|
int ret;
|
|
|
|
if (priv->slot)
|
|
sprintf(partname, VENDOR_BOOT_PART_NAME "_%s", priv->slot);
|
|
else
|
|
sprintf(partname, VENDOR_BOOT_PART_NAME);
|
|
|
|
ret = part_get_info_by_name(desc, partname, &partition);
|
|
if (ret < 0)
|
|
return log_msg_ret("part info", ret);
|
|
|
|
num_blks = DIV_ROUND_UP(sizeof(struct andr_vnd_boot_img_hdr), desc->blksz);
|
|
bufsz = num_blks * desc->blksz;
|
|
buf = malloc(bufsz);
|
|
if (!buf)
|
|
return log_msg_ret("buf", -ENOMEM);
|
|
|
|
ret = blk_read(blk, partition.start, num_blks, buf);
|
|
if (ret != num_blks) {
|
|
free(buf);
|
|
return log_msg_ret("part read", -EIO);
|
|
}
|
|
|
|
if (!is_android_vendor_boot_image_header(buf)) {
|
|
free(buf);
|
|
return log_msg_ret("header", -ENOENT);
|
|
}
|
|
|
|
if (!android_image_get_vendor_bootimg_size(buf, &priv->vendor_boot_img_size)) {
|
|
free(buf);
|
|
return log_msg_ret("get vendor bootimg size", -EINVAL);
|
|
}
|
|
|
|
free(buf);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int android_read_slot_from_bcb(struct bootflow *bflow, bool decrement)
|
|
{
|
|
struct blk_desc *desc = dev_get_uclass_plat(bflow->blk);
|
|
struct android_priv *priv = bflow->bootmeth_priv;
|
|
struct disk_partition misc;
|
|
char slot_suffix[3];
|
|
int ret;
|
|
|
|
if (!CONFIG_IS_ENABLED(ANDROID_AB)) {
|
|
priv->slot = NULL;
|
|
return 0;
|
|
}
|
|
|
|
ret = part_get_info_by_name(desc, BCB_PART_NAME, &misc);
|
|
if (ret < 0)
|
|
return log_msg_ret("part", ret);
|
|
|
|
ret = ab_select_slot(desc, &misc, decrement);
|
|
if (ret < 0)
|
|
return log_msg_ret("slot", ret);
|
|
|
|
priv->slot = malloc(SLOT_LEN);
|
|
priv->slot[0] = BOOT_SLOT_NAME(ret);
|
|
priv->slot[1] = '\0';
|
|
|
|
sprintf(slot_suffix, "_%s", priv->slot);
|
|
ret = bootflow_cmdline_set_arg(bflow, "androidboot.slot_suffix",
|
|
slot_suffix, false);
|
|
if (ret < 0)
|
|
return log_msg_ret("cmdl", ret);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int configure_serialno(struct bootflow *bflow)
|
|
{
|
|
char *serialno = env_get("serial#");
|
|
|
|
if (!serialno)
|
|
return log_msg_ret("serial", -ENOENT);
|
|
|
|
return bootflow_cmdline_set_arg(bflow, "androidboot.serialno", serialno, false);
|
|
}
|
|
|
|
static int configure_bootloader_version(struct bootflow *bflow)
|
|
{
|
|
return bootflow_cmdline_set_arg(bflow, "androidboot.bootloader",
|
|
PLAIN_VERSION, false);
|
|
}
|
|
|
|
static int android_read_bootflow(struct udevice *dev, struct bootflow *bflow)
|
|
{
|
|
struct blk_desc *desc = dev_get_uclass_plat(bflow->blk);
|
|
struct disk_partition misc;
|
|
struct android_priv *priv;
|
|
char command[BCB_FIELD_COMMAND_SZ];
|
|
int ret;
|
|
|
|
bflow->state = BOOTFLOWST_MEDIA;
|
|
|
|
/*
|
|
* bcb_find_partition_and_load() will print errors to stdout
|
|
* if BCB_PART_NAME is not found. To avoid that, check if the
|
|
* partition exists first.
|
|
*/
|
|
ret = part_get_info_by_name(desc, BCB_PART_NAME, &misc);
|
|
if (ret < 0)
|
|
return log_msg_ret("part", ret);
|
|
|
|
ret = bcb_find_partition_and_load("mmc", desc->devnum, BCB_PART_NAME);
|
|
if (ret < 0)
|
|
return log_msg_ret("bcb load", ret);
|
|
|
|
ret = bcb_get(BCB_FIELD_COMMAND, command, sizeof(command));
|
|
if (ret < 0)
|
|
return log_msg_ret("bcb read", ret);
|
|
|
|
priv = malloc(sizeof(struct android_priv));
|
|
if (!priv)
|
|
return log_msg_ret("buf", -ENOMEM);
|
|
|
|
if (!strcmp("bootonce-bootloader", command)) {
|
|
priv->boot_mode = ANDROID_BOOT_MODE_BOOTLOADER;
|
|
bflow->os_name = strdup("Android (bootloader)");
|
|
} else if (!strcmp("boot-fastboot", command)) {
|
|
priv->boot_mode = ANDROID_BOOT_MODE_RECOVERY;
|
|
bflow->os_name = strdup("Android (fastbootd)");
|
|
} else if (!strcmp("boot-recovery", command)) {
|
|
priv->boot_mode = ANDROID_BOOT_MODE_RECOVERY;
|
|
bflow->os_name = strdup("Android (recovery)");
|
|
} else {
|
|
priv->boot_mode = ANDROID_BOOT_MODE_NORMAL;
|
|
bflow->os_name = strdup("Android");
|
|
}
|
|
if (!bflow->os_name)
|
|
return log_msg_ret("os", -ENOMEM);
|
|
|
|
if (priv->boot_mode == ANDROID_BOOT_MODE_BOOTLOADER) {
|
|
/* Clear BCB */
|
|
memset(command, 0, sizeof(command));
|
|
ret = bcb_set(BCB_FIELD_COMMAND, command);
|
|
if (ret < 0) {
|
|
free(priv);
|
|
return log_msg_ret("bcb set", ret);
|
|
}
|
|
ret = bcb_store();
|
|
if (ret < 0) {
|
|
free(priv);
|
|
return log_msg_ret("bcb store", ret);
|
|
}
|
|
|
|
bflow->bootmeth_priv = priv;
|
|
bflow->state = BOOTFLOWST_READY;
|
|
return 0;
|
|
}
|
|
|
|
bflow->bootmeth_priv = priv;
|
|
|
|
/* For recovery and normal boot, we need to scan the partitions */
|
|
ret = android_read_slot_from_bcb(bflow, false);
|
|
if (ret < 0) {
|
|
log_err("read slot: %d", ret);
|
|
goto free_priv;
|
|
}
|
|
|
|
ret = scan_boot_part(bflow->blk, priv);
|
|
if (ret < 0) {
|
|
log_debug("scan boot failed: err=%d\n", ret);
|
|
goto free_priv;
|
|
}
|
|
|
|
if (priv->header_version >= 3) {
|
|
ret = scan_vendor_boot_part(bflow->blk, priv);
|
|
if (ret < 0) {
|
|
log_debug("scan vendor_boot failed: err=%d\n", ret);
|
|
goto free_priv;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Ignoring return code for the following configurations:
|
|
* these are not mandatory for booting.
|
|
*/
|
|
configure_serialno(bflow);
|
|
configure_bootloader_version(bflow);
|
|
|
|
if (priv->boot_mode == ANDROID_BOOT_MODE_NORMAL && priv->slot) {
|
|
ret = bootflow_cmdline_set_arg(bflow, "androidboot.force_normal_boot",
|
|
"1", false);
|
|
if (ret < 0) {
|
|
log_debug("normal_boot %d", ret);
|
|
goto free_priv;
|
|
}
|
|
}
|
|
|
|
bflow->state = BOOTFLOWST_READY;
|
|
|
|
return 0;
|
|
|
|
free_priv:
|
|
free(priv);
|
|
bflow->bootmeth_priv = NULL;
|
|
return ret;
|
|
}
|
|
|
|
static int android_read_file(struct udevice *dev, struct bootflow *bflow,
|
|
const char *file_path, ulong addr,
|
|
enum bootflow_img_t type, ulong *sizep)
|
|
{
|
|
/*
|
|
* Reading individual files is not supported since we only
|
|
* operate on whole mmc devices (because we require multiple partitions)
|
|
*/
|
|
return log_msg_ret("Unsupported", -ENOSYS);
|
|
}
|
|
|
|
/**
|
|
* read_slotted_partition() - Read a partition by appending a slot suffix
|
|
*
|
|
* Most modern Android devices use Seamless Updates, where each partition
|
|
* is duplicated. For example, the boot partition has boot_a and boot_b.
|
|
* For more information, see:
|
|
* https://source.android.com/docs/core/ota/ab
|
|
* https://source.android.com/docs/core/ota/ab/ab_implement
|
|
*
|
|
* @blk: Block device to read
|
|
* @name: Partition name to read
|
|
* @slot: Nul-terminated slot suffixed to partition name ("a\0" or "b\0")
|
|
* @image_size: Image size in bytes used when reading the partition
|
|
* @addr: Address where the partition content is loaded into
|
|
* Return: 0 if OK, negative errno on failure.
|
|
*/
|
|
static int read_slotted_partition(struct blk_desc *desc, const char *const name,
|
|
const char slot[2], ulong image_size, ulong addr)
|
|
{
|
|
struct disk_partition partition;
|
|
char partname[PART_NAME_LEN];
|
|
size_t partname_len;
|
|
ulong num_blks = DIV_ROUND_UP(image_size, desc->blksz);
|
|
int ret;
|
|
u32 n;
|
|
|
|
/*
|
|
* Ensure name fits in partname.
|
|
* For A/B, it should be <name>_<slot>\0
|
|
* For non A/B, it should be <name>\0
|
|
*/
|
|
if (CONFIG_IS_ENABLED(ANDROID_AB))
|
|
partname_len = PART_NAME_LEN - 2 - 1;
|
|
else
|
|
partname_len = PART_NAME_LEN - 1;
|
|
|
|
if (strlen(name) > partname_len)
|
|
return log_msg_ret("name too long", -EINVAL);
|
|
|
|
if (slot)
|
|
sprintf(partname, "%s_%s", name, slot);
|
|
else
|
|
sprintf(partname, "%s", name);
|
|
|
|
ret = part_get_info_by_name(desc, partname, &partition);
|
|
if (ret < 0)
|
|
return log_msg_ret("part", ret);
|
|
|
|
n = blk_dread(desc, partition.start, num_blks, map_sysmem(addr, 0));
|
|
if (n < num_blks)
|
|
return log_msg_ret("part read", -EIO);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#if CONFIG_IS_ENABLED(AVB_VERIFY)
|
|
static int avb_append_commandline_arg(struct bootflow *bflow, char *arg)
|
|
{
|
|
char *key = strsep(&arg, "=");
|
|
char *value = arg;
|
|
int ret;
|
|
|
|
ret = bootflow_cmdline_set_arg(bflow, key, value, false);
|
|
if (ret < 0)
|
|
return log_msg_ret("avb cmdline", ret);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int avb_append_commandline(struct bootflow *bflow, char *cmdline)
|
|
{
|
|
char *arg = strsep(&cmdline, " ");
|
|
int ret;
|
|
|
|
while (arg) {
|
|
ret = avb_append_commandline_arg(bflow, arg);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
arg = strsep(&cmdline, " ");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int run_avb_verification(struct bootflow *bflow)
|
|
{
|
|
struct blk_desc *desc = dev_get_uclass_plat(bflow->blk);
|
|
struct android_priv *priv = bflow->bootmeth_priv;
|
|
const char * const requested_partitions[] = {"boot", "vendor_boot", NULL};
|
|
struct AvbOps *avb_ops;
|
|
AvbSlotVerifyResult result;
|
|
AvbSlotVerifyData *out_data;
|
|
enum avb_boot_state boot_state;
|
|
char *extra_args;
|
|
char slot_suffix[3] = "";
|
|
bool unlocked = false;
|
|
int ret;
|
|
|
|
avb_ops = avb_ops_alloc(desc->devnum);
|
|
if (!avb_ops)
|
|
return log_msg_ret("avb ops", -ENOMEM);
|
|
|
|
if (priv->slot)
|
|
sprintf(slot_suffix, "_%s", priv->slot);
|
|
|
|
ret = avb_ops->read_is_device_unlocked(avb_ops, &unlocked);
|
|
if (ret != AVB_IO_RESULT_OK)
|
|
return log_msg_ret("avb lock", -EIO);
|
|
|
|
result = avb_slot_verify(avb_ops,
|
|
requested_partitions,
|
|
slot_suffix,
|
|
unlocked,
|
|
AVB_HASHTREE_ERROR_MODE_RESTART_AND_INVALIDATE,
|
|
&out_data);
|
|
|
|
if (!unlocked) {
|
|
/* When device is locked, we only accept AVB_SLOT_VERIFY_RESULT_OK */
|
|
if (result != AVB_SLOT_VERIFY_RESULT_OK) {
|
|
printf("Verification failed, reason: %s\n",
|
|
str_avb_slot_error(result));
|
|
if (out_data)
|
|
avb_slot_verify_data_free(out_data);
|
|
return log_msg_ret("avb verify", -EIO);
|
|
}
|
|
boot_state = AVB_GREEN;
|
|
} else {
|
|
/* When device is unlocked, we also accept verification errors */
|
|
if (result != AVB_SLOT_VERIFY_RESULT_OK &&
|
|
result != AVB_SLOT_VERIFY_RESULT_ERROR_VERIFICATION) {
|
|
printf("Unlocked verification failed, reason: %s\n",
|
|
str_avb_slot_error(result));
|
|
if (out_data)
|
|
avb_slot_verify_data_free(out_data);
|
|
return log_msg_ret("avb verify unlocked", -EIO);
|
|
}
|
|
boot_state = AVB_ORANGE;
|
|
}
|
|
|
|
extra_args = avb_set_state(avb_ops, boot_state);
|
|
if (extra_args) {
|
|
/* extra_args will be modified after this. This is fine */
|
|
ret = avb_append_commandline_arg(bflow, extra_args);
|
|
if (ret < 0)
|
|
goto free_out_data;
|
|
}
|
|
|
|
if (result == AVB_SLOT_VERIFY_RESULT_OK) {
|
|
ret = avb_append_commandline(bflow, out_data->cmdline);
|
|
if (ret < 0)
|
|
goto free_out_data;
|
|
}
|
|
|
|
return 0;
|
|
|
|
free_out_data:
|
|
if (out_data)
|
|
avb_slot_verify_data_free(out_data);
|
|
|
|
return log_msg_ret("avb cmdline", ret);
|
|
}
|
|
#else
|
|
static int run_avb_verification(struct bootflow *bflow)
|
|
{
|
|
int ret;
|
|
|
|
/* When AVB is unsupported, pass ORANGE state */
|
|
ret = bootflow_cmdline_set_arg(bflow,
|
|
"androidboot.verifiedbootstate",
|
|
"orange", false);
|
|
if (ret < 0)
|
|
return log_msg_ret("avb cmdline", ret);
|
|
|
|
return 0;
|
|
}
|
|
#endif /* AVB_VERIFY */
|
|
|
|
static int boot_android_normal(struct bootflow *bflow)
|
|
{
|
|
struct blk_desc *desc = dev_get_uclass_plat(bflow->blk);
|
|
struct android_priv *priv = bflow->bootmeth_priv;
|
|
int ret;
|
|
ulong loadaddr = env_get_hex("loadaddr", 0);
|
|
ulong vloadaddr = env_get_hex("vendor_boot_comp_addr_r", 0);
|
|
|
|
ret = run_avb_verification(bflow);
|
|
if (ret < 0)
|
|
return log_msg_ret("avb", ret);
|
|
|
|
/* Read slot once more to decrement counter from BCB */
|
|
ret = android_read_slot_from_bcb(bflow, true);
|
|
if (ret < 0)
|
|
return log_msg_ret("read slot", ret);
|
|
|
|
ret = read_slotted_partition(desc, "boot", priv->slot, priv->boot_img_size,
|
|
loadaddr);
|
|
if (ret < 0)
|
|
return log_msg_ret("read boot", ret);
|
|
|
|
if (priv->header_version >= 3) {
|
|
ret = read_slotted_partition(desc, "vendor_boot", priv->slot,
|
|
priv->vendor_boot_img_size, vloadaddr);
|
|
if (ret < 0)
|
|
return log_msg_ret("read vendor_boot", ret);
|
|
set_avendor_bootimg_addr(vloadaddr);
|
|
}
|
|
set_abootimg_addr(loadaddr);
|
|
|
|
if (priv->slot)
|
|
free(priv->slot);
|
|
|
|
ret = bootm_boot_start(loadaddr, bflow->cmdline);
|
|
|
|
return log_msg_ret("boot", ret);
|
|
}
|
|
|
|
static int boot_android_recovery(struct bootflow *bflow)
|
|
{
|
|
int ret;
|
|
|
|
ret = boot_android_normal(bflow);
|
|
|
|
return log_msg_ret("boot", ret);
|
|
}
|
|
|
|
static int boot_android_bootloader(struct bootflow *bflow)
|
|
{
|
|
int ret;
|
|
|
|
ret = run_command("fastboot usb 0", 0);
|
|
do_reset(NULL, 0, 0, NULL);
|
|
|
|
return log_msg_ret("boot", ret);
|
|
}
|
|
|
|
static int android_boot(struct udevice *dev, struct bootflow *bflow)
|
|
{
|
|
struct android_priv *priv = bflow->bootmeth_priv;
|
|
int ret;
|
|
|
|
switch (priv->boot_mode) {
|
|
case ANDROID_BOOT_MODE_NORMAL:
|
|
ret = boot_android_normal(bflow);
|
|
break;
|
|
case ANDROID_BOOT_MODE_RECOVERY:
|
|
ret = boot_android_recovery(bflow);
|
|
break;
|
|
case ANDROID_BOOT_MODE_BOOTLOADER:
|
|
ret = boot_android_bootloader(bflow);
|
|
break;
|
|
default:
|
|
printf("ANDROID: Unknown boot mode %d. Running fastboot...\n",
|
|
priv->boot_mode);
|
|
boot_android_bootloader(bflow);
|
|
/* Tell we failed to boot since boot mode is unknown */
|
|
ret = -EFAULT;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int android_bootmeth_bind(struct udevice *dev)
|
|
{
|
|
struct bootmeth_uc_plat *plat = dev_get_uclass_plat(dev);
|
|
|
|
plat->desc = "Android boot";
|
|
plat->flags = BOOTMETHF_ANY_PART;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct bootmeth_ops android_bootmeth_ops = {
|
|
.check = android_check,
|
|
.read_bootflow = android_read_bootflow,
|
|
.read_file = android_read_file,
|
|
.boot = android_boot,
|
|
};
|
|
|
|
static const struct udevice_id android_bootmeth_ids[] = {
|
|
{ .compatible = "u-boot,android" },
|
|
{ }
|
|
};
|
|
|
|
U_BOOT_DRIVER(bootmeth_android) = {
|
|
.name = "bootmeth_android",
|
|
.id = UCLASS_BOOTMETH,
|
|
.of_match = android_bootmeth_ids,
|
|
.ops = &android_bootmeth_ops,
|
|
.bind = android_bootmeth_bind,
|
|
};
|