root/usr/src/lib/libgrubmgmt/common/libgrub_cmd.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */
/*
 * Copyright 2015 Nexenta Systems, Inc.
 */

/*
 * This file contains all the functions that implement the following
 * GRUB commands:
 *      kernel, kernel$, module, module$, findroot, bootfs
 * Return 0 on success, errno on failure.
 */
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <alloca.h>
#include <errno.h>
#include <strings.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/fs/ufs_mount.h>
#include <sys/dktp/fdisk.h>
#if defined(__i386)
#include <sys/x86_archext.h>
#endif /* __i386 */

#include "libgrub_impl.h"

#define RESET_MODULE(barg)      ((barg)->gb_module[0] = 0)

#define BPROP_ZFSBOOTFS "zfs-bootfs"
#define BPROP_BOOTPATH  "bootpath"

#if defined(__i386)
static const char cpuid_dev[] = "/dev/cpu/self/cpuid";

/*
 * Return 1 if the system supports 64-bit mode, 0 if it doesn't,
 * or -1 on failure.
 */
static int
cpuid_64bit_capable(void)
{
        int fd, ret = -1;
        struct {
                uint32_t cp_eax, cp_ebx, cp_ecx, cp_edx;
        } cpuid_regs;

        if ((fd = open(cpuid_dev, O_RDONLY)) == -1)
                return (ret);

        if (pread(fd, &cpuid_regs, sizeof (cpuid_regs), 0x80000001) ==
            sizeof (cpuid_regs))
                ret = ((CPUID_AMD_EDX_LM & cpuid_regs.cp_edx) != 0);

        (void) close(fd);
        return (ret);
}
#endif /* __i386 */


/*
 * Expand $ISAIDR
 */
#if !defined(__i386)
/* ARGSUSED */
#endif /* __i386 */
static size_t
barg_isadir_var(char *var, int sz)
{
#if defined(__i386)
        if (cpuid_64bit_capable() == 1)
                return (strlcpy(var, "amd64", sz));
#endif /* __i386 */

        var[0] = 0;
        return (0);
}

/*
 * Expand $ZFS-BOOTFS
 */
static size_t
barg_bootfs_var(const grub_barg_t *barg, char *var, int sz)
{
        int n;

        assert(barg);
        if (strcmp(barg->gb_root.gr_fstyp, MNTTYPE_ZFS) == 0) {
                n = snprintf(var, sz,
                    BPROP_ZFSBOOTFS "=%s," BPROP_BOOTPATH "=\"%s\"",
                    barg->gb_root.gr_fs[GRBM_ZFS_BOOTFS].gfs_dev,
                    barg->gb_root.gr_physpath);
        } else  {
                var[0] = 0;
                n = 0;
        }
        return (n);
}

/*
 * Expand all the variables without appending them more than once.
 */
static int
expand_var(char *arg, size_t argsz, const char *var, size_t varsz,
    char *val, size_t valsz)
{
        char    *sp = arg;
        size_t  sz = argsz, len;
        char    *buf, *dst, *src;
        int     ret = 0;

        buf = alloca(argsz);
        dst = buf;

        while ((src = strstr(sp, var)) != NULL) {

                len = src - sp;

                if (len + valsz > sz) {
                        ret = E2BIG;
                        break;
                }

                (void) bcopy(sp, dst, len);
                (void) bcopy(val, dst + len, valsz);
                dst += len + valsz;
                sz -= len + valsz;
                sp = src + varsz;
        }

        if (strlcpy(dst, sp, sz) >= sz)
                ret = E2BIG;

        if (ret == 0)
                bcopy(buf, arg, argsz);
        return (ret);
}

/*
 * Searches first occurence of boot-property 'bprop' in str.
 * str supposed to be in format:
 * " [-B prop=[value][,prop=[value]]...]
 */
static const char *
find_bootprop(const char *str, const char *bprop)
{
        const char *s;
        size_t bplen, len;

        assert(str);
        assert(bprop);

        bplen = strlen(bprop);
        s = str;

        while ((str = strstr(s, " -B")) != NULL ||
            (str = strstr(s, "\t-B")) != NULL) {
                s = str + 3;
                len = strspn(s, " \t");

                /* empty -B option, skip it */
                if (len != 0 && s[len] == '-')
                        continue;

                s += len;
                do {
                        len = strcspn(s, "= \t");
                        if (s[len] !=  '=')
                                break;

                        /* boot property we are looking for? */
                        if (len == bplen && strncmp(s, bprop, bplen) == 0)
                                return (s);

                        s += len;

                        /* skip boot property value */
                        while ((s = strpbrk(s + 1, "\"\', \t")) != NULL) {

                                /* skip quoted */
                                if (s[0] == '\"' || s[0] == '\'') {
                                        if ((s = strchr(s + 1, s[0])) == NULL) {
                                                /* unbalanced quotes */
                                                return (s);
                                        }
                                }
                                else
                                        break;
                        }

                        /* no more boot properties */
                        if (s == NULL)
                                return (s);

                        /* no more boot properties in that -B block */
                        if (s[0] != ',')
                                break;

                        s += strspn(s, ",");
                } while (s[0] != ' ' && s[0] != '\t');
        }
        return (NULL);
}

/*
 * Add bootpath property to str if
 *      1. zfs-bootfs property is set explicitly
 * and
 *      2. bootpath property is not set
 */
static int
update_bootpath(char *str, size_t strsz, const char *bootpath)
{
        size_t n;
        char *buf;
        const char *bfs;

        /* zfs-bootfs is not specified, or bootpath is allready set */
        if ((bfs = find_bootprop(str, BPROP_ZFSBOOTFS)) == NULL ||
            find_bootprop(str, BPROP_BOOTPATH) != NULL)
                return (0);

        n = bfs - str;
        buf = alloca(strsz);

        bcopy(str, buf, n);
        if (snprintf(buf + n, strsz - n, BPROP_BOOTPATH "=\"%s\",%s",
            bootpath, bfs) >= strsz - n)
                return (E2BIG);

        bcopy(buf, str, strsz);
        return (0);
}

static int
match_bootfs(zfs_handle_t *zfh, void *data)
{
        int             ret;
        const char      *zfn;
        grub_barg_t     *barg = (grub_barg_t *)data;

        ret = (zfs_get_type(zfh) == ZFS_TYPE_FILESYSTEM &&
            (zfn = zfs_get_name(zfh)) != NULL &&
            strcmp(barg->gb_root.gr_fs[GRBM_ZFS_BOOTFS].gfs_dev, zfn) == 0);

        if (ret != 0)
                barg->gb_walkret = 0;
        else
                (void) zfs_iter_filesystems(zfh, match_bootfs, barg);

        zfs_close(zfh);
        return (barg->gb_walkret == 0);
}

static void
reset_root(grub_barg_t *barg)
{
        (void) memset(&barg->gb_root, 0, sizeof (barg->gb_root));
        barg->gb_bootsign[0] = 0;
        barg->gb_kernel[0] = 0;
        RESET_MODULE(barg);
}

/* ARGSUSED */
int
skip_line(const grub_line_t *lp, grub_barg_t *barg)
{
        return (0);
}

/* ARGSUSED */
int
error_line(const grub_line_t *lp, grub_barg_t *barg)
{
        if (lp->gl_cmdtp == GRBM_ROOT_CMD)
                return (EG_ROOTNOTSUPP);
        return (EG_INVALIDLINE);
}

int
kernel(const grub_line_t *lp, grub_barg_t *barg)
{
        RESET_MODULE(barg);
        if (strlcpy(barg->gb_kernel, lp->gl_arg, sizeof (barg->gb_kernel)) >=
            sizeof (barg->gb_kernel))
                return (E2BIG);

        return (0);
}

int
module(const grub_line_t *lp, grub_barg_t *barg)
{
        if (strlcpy(barg->gb_module, lp->gl_arg, sizeof (barg->gb_module)) >=
            sizeof (barg->gb_module))
                return (E2BIG);

        return (0);
}

int
dollar_kernel(const grub_line_t *lp, grub_barg_t *barg)
{
        int     ret;
        size_t  bfslen, isalen;
        char    isadir[32];
        char    bootfs[BOOTARGS_MAX];

        RESET_MODULE(barg);
        if (strlcpy(barg->gb_kernel, lp->gl_arg, sizeof (barg->gb_kernel)) >=
            sizeof (barg->gb_kernel))
                return (E2BIG);

        bfslen = barg_bootfs_var(barg, bootfs, sizeof (bootfs));
        isalen = barg_isadir_var(isadir, sizeof (isadir));

        if (bfslen >= sizeof (bootfs) || isalen >= sizeof (isadir))
                return (EINVAL);

        if ((ret = expand_var(barg->gb_kernel, sizeof (barg->gb_kernel),
            ZFS_BOOT_VAR, strlen(ZFS_BOOT_VAR), bootfs, bfslen)) != 0)
                return (ret);

        if ((ret = expand_var(barg->gb_kernel, sizeof (barg->gb_kernel),
            ISADIR_VAR, strlen(ISADIR_VAR), isadir, isalen)) != 0)
                return (ret);

        if (strcmp(barg->gb_root.gr_fstyp, MNTTYPE_ZFS) == 0)
                ret = update_bootpath(barg->gb_kernel, sizeof (barg->gb_kernel),
                    barg->gb_root.gr_physpath);

        return (ret);
}

int
dollar_module(const grub_line_t *lp, grub_barg_t *barg)
{
        int     ret;
        size_t  isalen;
        char    isadir[32];

        if (strlcpy(barg->gb_module, lp->gl_arg, sizeof (barg->gb_module)) >=
            sizeof (barg->gb_module))
                return (E2BIG);

        if ((isalen = barg_isadir_var(isadir, sizeof (isadir))) >= sizeof
            (isadir))
                return (EINVAL);

        ret = expand_var(barg->gb_module, sizeof (barg->gb_module),
            ISADIR_VAR, strlen(ISADIR_VAR), isadir, isalen);

        return (ret);
}


int
findroot(const grub_line_t *lp, grub_barg_t *barg)
{
        size_t sz, bsz;
        const char *sign;

        reset_root(barg);

        sign = lp->gl_arg;
        barg->gb_prtnum = (uint_t)PRTNUM_INVALID;
        barg->gb_slcnum = (uint_t)SLCNUM_WHOLE_DISK;

        if (sign[0] == '(') {
                const char *pos;

                ++sign;
                if ((pos = strchr(sign, ',')) == NULL || (sz = pos - sign) == 0)
                        return (EG_FINDROOTFMT);

                ++pos;
                if (!IS_PRTNUM_VALID(barg->gb_prtnum = pos[0] - '0'))
                        return (EG_FINDROOTFMT);

                ++pos;
                /*
                 * check the slice only when its presented
                 */
                if (pos[0] != ')') {
                        if (pos[0] != ',' ||
                            !IS_SLCNUM_VALID(barg->gb_slcnum = pos[1]) ||
                            pos[2] != ')')
                                return (EG_FINDROOTFMT);
                }
        } else {
                sz = strlen(sign);
        }

        bsz = strlen(BOOTSIGN_DIR "/");
        if (bsz + sz + 1 > sizeof (barg->gb_bootsign))
                return (E2BIG);

        bcopy(BOOTSIGN_DIR "/", barg->gb_bootsign, bsz);
        bcopy(sign, barg->gb_bootsign + bsz, sz);
        barg->gb_bootsign [bsz + sz] = 0;

        return (grub_find_bootsign(barg));
}

int
bootfs(const grub_line_t *lp, grub_barg_t *barg)
{
        zfs_handle_t    *zfh;
        grub_menu_t     *mp = barg->gb_entry->ge_menu;
        char            *gfs_devp;
        size_t          gfs_dev_len;

        /* Check if root is zfs */
        if (strcmp(barg->gb_root.gr_fstyp, MNTTYPE_ZFS) != 0)
                return (EG_NOTZFS);

        gfs_devp = barg->gb_root.gr_fs[GRBM_ZFS_BOOTFS].gfs_dev;
        gfs_dev_len = sizeof (barg->gb_root.gr_fs[GRBM_ZFS_BOOTFS].gfs_dev);

        /*
         * If the bootfs value is the same as the bootfs for the pool,
         * do nothing.
         */
        if (strcmp(lp->gl_arg, gfs_devp) == 0)
                return (0);

        if (strlcpy(gfs_devp, lp->gl_arg, gfs_dev_len) >= gfs_dev_len)
                return (E2BIG);

        /* check if specified bootfs belongs to the root pool */
        if ((zfh = zfs_open(mp->gm_fs.gf_lzfh,
            barg->gb_root.gr_fs[GRBM_ZFS_TOPFS].gfs_dev,
            ZFS_TYPE_FILESYSTEM)) == NULL)
                return (EG_OPENZFS);

        barg->gb_walkret = EG_UNKBOOTFS;
        (void) zfs_iter_filesystems(zfh, match_bootfs, barg);
        zfs_close(zfh);

        if (barg->gb_walkret == 0)
                (void) grub_fsd_get_mountp(barg->gb_root.gr_fs +
                    GRBM_ZFS_BOOTFS, MNTTYPE_ZFS);

        return (barg->gb_walkret);
}