root/net/sunrpc/auth_gss/gss_rpc_xdr.c
// SPDX-License-Identifier: GPL-2.0+
/*
 * GSS Proxy upcall module
 *
 *  Copyright (C) 2012 Simo Sorce <simo@redhat.com>
 */

#include <linux/sunrpc/svcauth.h>
#include "gss_rpc_xdr.h"

static int gssx_enc_bool(struct xdr_stream *xdr, int v)
{
        __be32 *p;

        p = xdr_reserve_space(xdr, 4);
        if (unlikely(p == NULL))
                return -ENOSPC;
        *p = v ? xdr_one : xdr_zero;
        return 0;
}

static int gssx_dec_bool(struct xdr_stream *xdr, u32 *v)
{
        __be32 *p;

        p = xdr_inline_decode(xdr, 4);
        if (unlikely(p == NULL))
                return -ENOSPC;
        *v = be32_to_cpu(*p);
        return 0;
}

static int gssx_enc_buffer(struct xdr_stream *xdr,
                           const gssx_buffer *buf)
{
        __be32 *p;

        p = xdr_reserve_space(xdr, sizeof(u32) + buf->len);
        if (!p)
                return -ENOSPC;
        xdr_encode_opaque(p, buf->data, buf->len);
        return 0;
}

static int gssx_enc_in_token(struct xdr_stream *xdr,
                             const struct gssp_in_token *in)
{
        __be32 *p;

        p = xdr_reserve_space(xdr, 4);
        if (!p)
                return -ENOSPC;
        *p = cpu_to_be32(in->page_len);

        /* all we need to do is to write pages */
        xdr_write_pages(xdr, in->pages, in->page_base, in->page_len);

        return 0;
}


static int gssx_dec_buffer(struct xdr_stream *xdr,
                           gssx_buffer *buf)
{
        u32 length;
        __be32 *p;

        p = xdr_inline_decode(xdr, 4);
        if (unlikely(p == NULL))
                return -ENOSPC;

        length = be32_to_cpup(p);
        p = xdr_inline_decode(xdr, length);
        if (unlikely(p == NULL))
                return -ENOSPC;

        if (buf->len == 0) {
                /* we intentionally are not interested in this buffer */
                return 0;
        }
        if (length > buf->len)
                return -ENOSPC;

        if (!buf->data) {
                buf->data = kmemdup(p, length, GFP_KERNEL);
                if (!buf->data)
                        return -ENOMEM;
        } else {
                memcpy(buf->data, p, length);
        }
        buf->len = length;
        return 0;
}

static int gssx_enc_option(struct xdr_stream *xdr,
                           struct gssx_option *opt)
{
        int err;

        err = gssx_enc_buffer(xdr, &opt->option);
        if (err)
                return err;
        err = gssx_enc_buffer(xdr, &opt->value);
        return err;
}

static int gssx_dec_option(struct xdr_stream *xdr,
                           struct gssx_option *opt)
{
        int err;

        err = gssx_dec_buffer(xdr, &opt->option);
        if (err)
                return err;
        err = gssx_dec_buffer(xdr, &opt->value);
        return err;
}

static int dummy_enc_opt_array(struct xdr_stream *xdr,
                                const struct gssx_option_array *oa)
{
        __be32 *p;

        if (oa->count != 0)
                return -EINVAL;

        p = xdr_reserve_space(xdr, 4);
        if (!p)
                return -ENOSPC;
        *p = 0;

        return 0;
}

static int dummy_dec_opt_array(struct xdr_stream *xdr,
                                struct gssx_option_array *oa)
{
        struct gssx_option dummy;
        u32 count, i;
        __be32 *p;

        p = xdr_inline_decode(xdr, 4);
        if (unlikely(p == NULL))
                return -ENOSPC;
        count = be32_to_cpup(p++);
        memset(&dummy, 0, sizeof(dummy));
        for (i = 0; i < count; i++) {
                gssx_dec_option(xdr, &dummy);
        }

        oa->count = 0;
        oa->data = NULL;
        return 0;
}

static int get_host_u32(struct xdr_stream *xdr, u32 *res)
{
        __be32 *p;

        p = xdr_inline_decode(xdr, 4);
        if (!p)
                return -EINVAL;
        /* Contents of linux creds are all host-endian: */
        memcpy(res, p, sizeof(u32));
        return 0;
}

static int gssx_dec_linux_creds(struct xdr_stream *xdr,
                                struct svc_cred *creds)
{
        u32 length;
        __be32 *p;
        u32 tmp;
        u32 N;
        int i, err;

        p = xdr_inline_decode(xdr, 4);
        if (unlikely(p == NULL))
                return -ENOSPC;

        length = be32_to_cpup(p);

        if (length > (3 + NGROUPS_MAX) * sizeof(u32))
                return -ENOSPC;

        /* uid */
        err = get_host_u32(xdr, &tmp);
        if (err)
                return err;
        creds->cr_uid = make_kuid(&init_user_ns, tmp);

        /* gid */
        err = get_host_u32(xdr, &tmp);
        if (err)
                return err;
        creds->cr_gid = make_kgid(&init_user_ns, tmp);

        /* number of additional gid's */
        err = get_host_u32(xdr, &tmp);
        if (err)
                return err;
        N = tmp;
        if ((3 + N) * sizeof(u32) != length)
                return -EINVAL;
        creds->cr_group_info = groups_alloc(N);
        if (creds->cr_group_info == NULL)
                return -ENOMEM;

        /* gid's */
        for (i = 0; i < N; i++) {
                kgid_t kgid;
                err = get_host_u32(xdr, &tmp);
                if (err)
                        goto out_free_groups;
                err = -EINVAL;
                kgid = make_kgid(&init_user_ns, tmp);
                if (!gid_valid(kgid))
                        goto out_free_groups;
                creds->cr_group_info->gid[i] = kgid;
        }
        groups_sort(creds->cr_group_info);

        return 0;
out_free_groups:
        groups_free(creds->cr_group_info);
        return err;
}

static int gssx_dec_option_array(struct xdr_stream *xdr,
                                 struct gssx_option_array *oa)
{
        struct svc_cred *creds;
        u32 count, i;
        __be32 *p;
        int err;

        p = xdr_inline_decode(xdr, 4);
        if (unlikely(p == NULL))
                return -ENOSPC;
        count = be32_to_cpup(p++);
        if (!count)
                return 0;

        /* we recognize only 1 currently: CREDS_VALUE */
        oa->count = 1;

        oa->data = kmalloc_obj(struct gssx_option);
        if (!oa->data)
                return -ENOMEM;

        creds = kzalloc_obj(struct svc_cred);
        if (!creds) {
                err = -ENOMEM;
                goto free_oa;
        }

        oa->data[0].option.data = CREDS_VALUE;
        oa->data[0].option.len = sizeof(CREDS_VALUE);
        oa->data[0].value.data = (void *)creds;
        oa->data[0].value.len = 0;

        for (i = 0; i < count; i++) {
                gssx_buffer dummy = { 0, NULL };
                u32 length;

                /* option buffer */
                p = xdr_inline_decode(xdr, 4);
                if (unlikely(p == NULL)) {
                        err = -ENOSPC;
                        goto free_creds;
                }

                length = be32_to_cpup(p);
                p = xdr_inline_decode(xdr, length);
                if (unlikely(p == NULL)) {
                        err = -ENOSPC;
                        goto free_creds;
                }

                if (length == sizeof(CREDS_VALUE) &&
                    memcmp(p, CREDS_VALUE, sizeof(CREDS_VALUE)) == 0) {
                        /* We have creds here. parse them */
                        err = gssx_dec_linux_creds(xdr, creds);
                        if (err)
                                goto free_creds;
                        oa->data[0].value.len = 1; /* presence */
                } else {
                        /* consume uninteresting buffer */
                        err = gssx_dec_buffer(xdr, &dummy);
                        if (err)
                                goto free_creds;
                }
        }
        return 0;

free_creds:
        kfree(creds);
free_oa:
        kfree(oa->data);
        oa->data = NULL;
        return err;
}

static int gssx_dec_status(struct xdr_stream *xdr,
                           struct gssx_status *status)
{
        __be32 *p;
        int err;

        /* status->major_status */
        p = xdr_inline_decode(xdr, 8);
        if (unlikely(p == NULL))
                return -ENOSPC;
        p = xdr_decode_hyper(p, &status->major_status);

        /* status->mech */
        err = gssx_dec_buffer(xdr, &status->mech);
        if (err)
                return err;

        /* status->minor_status */
        p = xdr_inline_decode(xdr, 8);
        if (unlikely(p == NULL)) {
                err = -ENOSPC;
                goto out_free_mech;
        }
        p = xdr_decode_hyper(p, &status->minor_status);

        /* status->major_status_string */
        err = gssx_dec_buffer(xdr, &status->major_status_string);
        if (err)
                goto out_free_mech;

        /* status->minor_status_string */
        err = gssx_dec_buffer(xdr, &status->minor_status_string);
        if (err)
                goto out_free_major_status_string;

        /* status->server_ctx */
        err = gssx_dec_buffer(xdr, &status->server_ctx);
        if (err)
                goto out_free_minor_status_string;

        /* we assume we have no options for now, so simply consume them */
        /* status->options */
        err = dummy_dec_opt_array(xdr, &status->options);
        if (err)
                goto out_free_server_ctx;

        return 0;

out_free_server_ctx:
        kfree(status->server_ctx.data);
        status->server_ctx.data = NULL;
out_free_minor_status_string:
        kfree(status->minor_status_string.data);
        status->minor_status_string.data = NULL;
out_free_major_status_string:
        kfree(status->major_status_string.data);
        status->major_status_string.data = NULL;
out_free_mech:
        kfree(status->mech.data);
        status->mech.data = NULL;
        return err;
}

static int gssx_enc_call_ctx(struct xdr_stream *xdr,
                             const struct gssx_call_ctx *ctx)
{
        struct gssx_option opt;
        __be32 *p;
        int err;

        /* ctx->locale */
        err = gssx_enc_buffer(xdr, &ctx->locale);
        if (err)
                return err;

        /* ctx->server_ctx */
        err = gssx_enc_buffer(xdr, &ctx->server_ctx);
        if (err)
                return err;

        /* we always want to ask for lucid contexts */
        /* ctx->options */
        p = xdr_reserve_space(xdr, 4);
        *p = cpu_to_be32(2);

        /* we want a lucid_v1 context */
        opt.option.data = LUCID_OPTION;
        opt.option.len = sizeof(LUCID_OPTION);
        opt.value.data = LUCID_VALUE;
        opt.value.len = sizeof(LUCID_VALUE);
        err = gssx_enc_option(xdr, &opt);

        /* ..and user creds */
        opt.option.data = CREDS_OPTION;
        opt.option.len = sizeof(CREDS_OPTION);
        opt.value.data = CREDS_VALUE;
        opt.value.len = sizeof(CREDS_VALUE);
        err = gssx_enc_option(xdr, &opt);

        return err;
}

static int gssx_dec_name_attr(struct xdr_stream *xdr,
                             struct gssx_name_attr *attr)
{
        int err;

        /* attr->attr */
        err = gssx_dec_buffer(xdr, &attr->attr);
        if (err)
                return err;

        /* attr->value */
        err = gssx_dec_buffer(xdr, &attr->value);
        if (err)
                return err;

        /* attr->extensions */
        err = dummy_dec_opt_array(xdr, &attr->extensions);

        return err;
}

static int dummy_enc_nameattr_array(struct xdr_stream *xdr,
                                    struct gssx_name_attr_array *naa)
{
        __be32 *p;

        if (naa->count != 0)
                return -EINVAL;

        p = xdr_reserve_space(xdr, 4);
        if (!p)
                return -ENOSPC;
        *p = 0;

        return 0;
}

static int dummy_dec_nameattr_array(struct xdr_stream *xdr,
                                    struct gssx_name_attr_array *naa)
{
        struct gssx_name_attr dummy = { .attr = {.len = 0} };
        u32 count, i;
        __be32 *p;

        p = xdr_inline_decode(xdr, 4);
        if (unlikely(p == NULL))
                return -ENOSPC;
        count = be32_to_cpup(p++);
        for (i = 0; i < count; i++) {
                gssx_dec_name_attr(xdr, &dummy);
        }

        naa->count = 0;
        naa->data = NULL;
        return 0;
}

static struct xdr_netobj zero_netobj = {};

static struct gssx_name_attr_array zero_name_attr_array = {};

static struct gssx_option_array zero_option_array = {};

static int gssx_enc_name(struct xdr_stream *xdr,
                         struct gssx_name *name)
{
        int err;

        /* name->display_name */
        err = gssx_enc_buffer(xdr, &name->display_name);
        if (err)
                return err;

        /* name->name_type */
        err = gssx_enc_buffer(xdr, &zero_netobj);
        if (err)
                return err;

        /* name->exported_name */
        err = gssx_enc_buffer(xdr, &zero_netobj);
        if (err)
                return err;

        /* name->exported_composite_name */
        err = gssx_enc_buffer(xdr, &zero_netobj);
        if (err)
                return err;

        /* leave name_attributes empty for now, will add once we have any
         * to pass up at all */
        /* name->name_attributes */
        err = dummy_enc_nameattr_array(xdr, &zero_name_attr_array);
        if (err)
                return err;

        /* leave options empty for now, will add once we have any options
         * to pass up at all */
        /* name->extensions */
        err = dummy_enc_opt_array(xdr, &zero_option_array);

        return err;
}


static int gssx_dec_name(struct xdr_stream *xdr,
                         struct gssx_name *name)
{
        struct xdr_netobj dummy_netobj = { .len = 0 };
        struct gssx_name_attr_array dummy_name_attr_array = { .count = 0 };
        struct gssx_option_array dummy_option_array = { .count = 0 };
        int err;

        /* name->display_name */
        err = gssx_dec_buffer(xdr, &name->display_name);
        if (err)
                return err;

        /* name->name_type */
        err = gssx_dec_buffer(xdr, &dummy_netobj);
        if (err)
                goto out_free_display_name;

        /* name->exported_name */
        err = gssx_dec_buffer(xdr, &dummy_netobj);
        if (err)
                goto out_free_display_name;

        /* name->exported_composite_name */
        err = gssx_dec_buffer(xdr, &dummy_netobj);
        if (err)
                goto out_free_display_name;

        /* we assume we have no attributes for now, so simply consume them */
        /* name->name_attributes */
        err = dummy_dec_nameattr_array(xdr, &dummy_name_attr_array);
        if (err)
                goto out_free_display_name;

        /* we assume we have no options for now, so simply consume them */
        /* name->extensions */
        err = dummy_dec_opt_array(xdr, &dummy_option_array);
        if (err)
                goto out_free_display_name;

        return 0;

out_free_display_name:
        kfree(name->display_name.data);
        name->display_name.data = NULL;
        return err;
}

static int dummy_enc_credel_array(struct xdr_stream *xdr,
                                  struct gssx_cred_element_array *cea)
{
        __be32 *p;

        if (cea->count != 0)
                return -EINVAL;

        p = xdr_reserve_space(xdr, 4);
        if (!p)
                return -ENOSPC;
        *p = 0;

        return 0;
}

static int gssx_enc_cred(struct xdr_stream *xdr,
                         struct gssx_cred *cred)
{
        int err;

        /* cred->desired_name */
        err = gssx_enc_name(xdr, &cred->desired_name);
        if (err)
                return err;

        /* cred->elements */
        err = dummy_enc_credel_array(xdr, &cred->elements);
        if (err)
                return err;

        /* cred->cred_handle_reference */
        err = gssx_enc_buffer(xdr, &cred->cred_handle_reference);
        if (err)
                return err;

        /* cred->needs_release */
        err = gssx_enc_bool(xdr, cred->needs_release);

        return err;
}

static int gssx_enc_ctx(struct xdr_stream *xdr,
                        struct gssx_ctx *ctx)
{
        __be32 *p;
        int err;

        /* ctx->exported_context_token */
        err = gssx_enc_buffer(xdr, &ctx->exported_context_token);
        if (err)
                return err;

        /* ctx->state */
        err = gssx_enc_buffer(xdr, &ctx->state);
        if (err)
                return err;

        /* ctx->need_release */
        err = gssx_enc_bool(xdr, ctx->need_release);
        if (err)
                return err;

        /* ctx->mech */
        err = gssx_enc_buffer(xdr, &ctx->mech);
        if (err)
                return err;

        /* ctx->src_name */
        err = gssx_enc_name(xdr, &ctx->src_name);
        if (err)
                return err;

        /* ctx->targ_name */
        err = gssx_enc_name(xdr, &ctx->targ_name);
        if (err)
                return err;

        /* ctx->lifetime */
        p = xdr_reserve_space(xdr, 8+8);
        if (!p)
                return -ENOSPC;
        p = xdr_encode_hyper(p, ctx->lifetime);

        /* ctx->ctx_flags */
        p = xdr_encode_hyper(p, ctx->ctx_flags);

        /* ctx->locally_initiated */
        err = gssx_enc_bool(xdr, ctx->locally_initiated);
        if (err)
                return err;

        /* ctx->open */
        err = gssx_enc_bool(xdr, ctx->open);
        if (err)
                return err;

        /* leave options empty for now, will add once we have any options
         * to pass up at all */
        /* ctx->options */
        err = dummy_enc_opt_array(xdr, &ctx->options);

        return err;
}

static int gssx_dec_ctx(struct xdr_stream *xdr,
                        struct gssx_ctx *ctx)
{
        __be32 *p;
        int err;

        /* ctx->exported_context_token */
        err = gssx_dec_buffer(xdr, &ctx->exported_context_token);
        if (err)
                return err;

        /* ctx->state */
        err = gssx_dec_buffer(xdr, &ctx->state);
        if (err)
                goto out_free_exported_context_token;

        /* ctx->need_release */
        err = gssx_dec_bool(xdr, &ctx->need_release);
        if (err)
                goto out_free_state;

        /* ctx->mech */
        err = gssx_dec_buffer(xdr, &ctx->mech);
        if (err)
                goto out_free_state;

        /* ctx->src_name */
        err = gssx_dec_name(xdr, &ctx->src_name);
        if (err)
                goto out_free_mech;

        /* ctx->targ_name */
        err = gssx_dec_name(xdr, &ctx->targ_name);
        if (err)
                goto out_free_src_name;

        /* ctx->lifetime */
        p = xdr_inline_decode(xdr, 8+8);
        if (unlikely(p == NULL)) {
                err = -ENOSPC;
                goto out_free_targ_name;
        }
        p = xdr_decode_hyper(p, &ctx->lifetime);

        /* ctx->ctx_flags */
        p = xdr_decode_hyper(p, &ctx->ctx_flags);

        /* ctx->locally_initiated */
        err = gssx_dec_bool(xdr, &ctx->locally_initiated);
        if (err)
                goto out_free_targ_name;

        /* ctx->open */
        err = gssx_dec_bool(xdr, &ctx->open);
        if (err)
                goto out_free_targ_name;

        /* we assume we have no options for now, so simply consume them */
        /* ctx->options */
        err = dummy_dec_opt_array(xdr, &ctx->options);
        if (err)
                goto out_free_targ_name;

        return 0;

out_free_targ_name:
        kfree(ctx->targ_name.display_name.data);
        ctx->targ_name.display_name.data = NULL;
out_free_src_name:
        kfree(ctx->src_name.display_name.data);
        ctx->src_name.display_name.data = NULL;
out_free_mech:
        kfree(ctx->mech.data);
        ctx->mech.data = NULL;
out_free_state:
        kfree(ctx->state.data);
        ctx->state.data = NULL;
out_free_exported_context_token:
        kfree(ctx->exported_context_token.data);
        ctx->exported_context_token.data = NULL;
        return err;
}

static int gssx_enc_cb(struct xdr_stream *xdr, struct gssx_cb *cb)
{
        __be32 *p;
        int err;

        /* cb->initiator_addrtype */
        p = xdr_reserve_space(xdr, 8);
        if (!p)
                return -ENOSPC;
        p = xdr_encode_hyper(p, cb->initiator_addrtype);

        /* cb->initiator_address */
        err = gssx_enc_buffer(xdr, &cb->initiator_address);
        if (err)
                return err;

        /* cb->acceptor_addrtype */
        p = xdr_reserve_space(xdr, 8);
        if (!p)
                return -ENOSPC;
        p = xdr_encode_hyper(p, cb->acceptor_addrtype);

        /* cb->acceptor_address */
        err = gssx_enc_buffer(xdr, &cb->acceptor_address);
        if (err)
                return err;

        /* cb->application_data */
        err = gssx_enc_buffer(xdr, &cb->application_data);

        return err;
}

void gssx_enc_accept_sec_context(struct rpc_rqst *req,
                                 struct xdr_stream *xdr,
                                 const void *data)
{
        const struct gssx_arg_accept_sec_context *arg = data;
        int err;

        err = gssx_enc_call_ctx(xdr, &arg->call_ctx);
        if (err)
                goto done;

        /* arg->context_handle */
        if (arg->context_handle)
                err = gssx_enc_ctx(xdr, arg->context_handle);
        else
                err = gssx_enc_bool(xdr, 0);
        if (err)
                goto done;

        /* arg->cred_handle */
        if (arg->cred_handle)
                err = gssx_enc_cred(xdr, arg->cred_handle);
        else
                err = gssx_enc_bool(xdr, 0);
        if (err)
                goto done;

        /* arg->input_token */
        err = gssx_enc_in_token(xdr, &arg->input_token);
        if (err)
                goto done;

        /* arg->input_cb */
        if (arg->input_cb)
                err = gssx_enc_cb(xdr, arg->input_cb);
        else
                err = gssx_enc_bool(xdr, 0);
        if (err)
                goto done;

        err = gssx_enc_bool(xdr, arg->ret_deleg_cred);
        if (err)
                goto done;

        /* leave options empty for now, will add once we have any options
         * to pass up at all */
        /* arg->options */
        err = dummy_enc_opt_array(xdr, &arg->options);

        xdr_inline_pages(&req->rq_rcv_buf,
                PAGE_SIZE/2 /* pretty arbitrary */,
                arg->pages, 0 /* page base */, arg->npages * PAGE_SIZE);
done:
        if (err)
                dprintk("RPC:       gssx_enc_accept_sec_context: %d\n", err);
}

int gssx_dec_accept_sec_context(struct rpc_rqst *rqstp,
                                struct xdr_stream *xdr,
                                void *data)
{
        struct gssx_res_accept_sec_context *res = data;
        u32 value_follows;
        int err;
        struct folio *scratch;

        scratch = folio_alloc(GFP_KERNEL, 0);
        if (!scratch)
                return -ENOMEM;
        xdr_set_scratch_folio(xdr, scratch);

        /* res->status */
        err = gssx_dec_status(xdr, &res->status);
        if (err)
                goto out_free;

        /* res->context_handle */
        err = gssx_dec_bool(xdr, &value_follows);
        if (err)
                goto out_free;
        if (value_follows) {
                err = gssx_dec_ctx(xdr, res->context_handle);
                if (err)
                        goto out_free;
        } else {
                res->context_handle = NULL;
        }

        /* res->output_token */
        err = gssx_dec_bool(xdr, &value_follows);
        if (err)
                goto out_free;
        if (value_follows) {
                err = gssx_dec_buffer(xdr, res->output_token);
                if (err)
                        goto out_free;
        } else {
                res->output_token = NULL;
        }

        /* res->delegated_cred_handle */
        err = gssx_dec_bool(xdr, &value_follows);
        if (err)
                goto out_free;
        if (value_follows) {
                /* we do not support upcall servers sending this data. */
                err = -EINVAL;
                goto out_free;
        }

        /* res->options */
        err = gssx_dec_option_array(xdr, &res->options);

out_free:
        folio_put(scratch);
        return err;
}