xdrgen - Linux Kernel XDR code generator

Introduction
------------

SunRPC programs are typically specified using a language defined by
RFC 4506. In fact, all IETF-published NFS specifications provide a
description of the specified protocol using this language.

Since the 1990's, user space consumers of SunRPC have had access to
a tool that could read such XDR specifications and then generate C
code that implements the RPC portions of that protocol. This tool is
called rpcgen.

This RPC-level code is code that handles input directly from the
network, and thus a high degree of memory safety and sanity checking
is needed to help ensure proper levels of security. Bugs in this
code can have significant impact on security and performance.

However, it is code that is repetitive and tedious to write by hand.

The C code generated by rpcgen makes extensive use of the facilities
of the user space TI-RPC library and libc. Furthermore, the dialect
of the generated code is very traditional K&R C.

The Linux kernel's implementation of SunRPC-based protocols hand-roll
their XDR implementation. There are two main reasons for this:

1. libtirpc (and its predecessors) operate only in user space. The
   kernel's RPC implementation and its API are significantly
   different than libtirpc.

2. rpcgen-generated code is believed to be less efficient than code
   that is hand-written.

These days, gcc and its kin are capable of optimizing code better
than human authors. There are only a few instances where writing
XDR code by hand will make a measurable performance different.

In addition, the current hand-written code in the Linux kernel is
difficult to audit and prove that it implements exactly what is in
the protocol specification.

In order to accrue the benefits of machine-generated XDR code in the
kernel, a tool is needed that will output C code that works against
the kernel's SunRPC implementation rather than libtirpc.

Enter xdrgen.


Dependencies
------------

These dependencies are typically packaged by Linux distributions:

- python3
- python3-lark
- python3-jinja2

These dependencies are available via PyPi:

- pip install 'lark[interegular]'


XDR Specifications
------------------

When adding a new protocol implementation to the kernel, the XDR
specification can be derived by feeding a .txt copy of the RFC to
the script located in tools/net/sunrpc/extract.sh.

   $ extract.sh < rfc0001.txt > new2.x


Operation
---------

Once a .x file is available, use xdrgen to generate source and
header files containing an implementation of XDR encoding and
decoding functions for the specified protocol.

   $ ./xdrgen definitions new2.x > include/linux/sunrpc/xdrgen/new2.h
   $ ./xdrgen declarations new2.x > new2xdr_gen.h

and

   $ ./xdrgen source new2.x > new2xdr_gen.c

The files are ready to use for a server-side protocol implementation,
or may be used as a guide for implementing these routines by hand.

By default, the only comments added to this code are kdoc comments
that appear directly in front of the public per-procedure APIs. For
deeper introspection, specifying the "--annotate" flag will insert
additional comments in the generated code to help readers match the
generated code to specific parts of the XDR specification.

Because the generated code is targeted for the Linux kernel, it
is tagged with a GPLv2-only license.

The xdrgen tool can also provide lexical and syntax checking of
an XDR specification:

   $ ./xdrgen lint xdr/new.x


How It Works
------------

xdrgen does not use machine learning to generate source code. The
translation is entirely deterministic.

RFC 4506 Section 6 contains a BNF grammar of the XDR specification
language. The grammar has been adapted for use by the Python Lark
module.

The xdr.ebnf file in this directory contains the grammar used to
parse XDR specifications. xdrgen configures Lark using the grammar
in xdr.ebnf. Lark parses the target XDR specification using this
grammar, creating a parse tree.

xdrgen then transforms the parse tree into an abstract syntax tree.
This tree is passed to a series of code generators.

The generators are implemented as Python classes residing in the
generators/ directory. Each generator emits code created from Jinja2
templates stored in the templates/ directory.

The source code is generated in the same order in which they appear
in the specification to ensure the generated code compiles. This
conforms with the behavior of rpcgen.

xdrgen assumes that the generated source code is further compiled by
a compiler that can optimize in a number of ways, including:

 - Unused functions are discarded (ie, not added to the executable)

 - Aggressive function inlining removes unnecessary stack frames

 - Single-arm switch statements are replaced by a single conditional
   branch

And so on.


Pragmas
-------

Pragma directives specify exceptions to the normal generation of
encoding and decoding functions. The directives are described
below.

Pragma aggregate
------ ---------

  pragma aggregate <struct> <member> ;

A variable-length array member is normally encoded and decoded by
staging it in a C array and walking that array. When the application
object is not that array -- for instance when the member is a wire
projection of a different in-kernel structure -- building it is
wasted work. This directive marks such a member so that xdrgen emits a
codec that owns only the wire framing (the length prefix, the bound
check, and the per-element codec) and drives the application
through begin/item/end hooks, processing one element at a time with
no staged array. The marked member must be a struct member
declared either as a variable-length array or as an optional-data
list ("type *name"); xdrgen rejects a directive naming anything
else, and generates the codec for the server side only.

The two forms differ only in framing. A variable-length array
carries a u32 element count, so the framing writes that count and
bound-checks it. Its generated decoder hands each element to the
ordinary element decoder from a local variable, so the element
type may not contain a variable-length array or an optional-data
member at any depth; xdrgen rejects a directive whose element type
does. The array must declare a maximum size: the decoder hands the
wire count to the begin hook before it decodes any element, and
only the declared bound limits that count. xdrgen rejects a
directive naming an unbounded array. An optional-data list carries
no count: each element is prefixed by a value-follows TRUE and the
sequence is closed by a FALSE, which the framing writes after the
last element. The element type must therefore be a pointer type,
whose codec owns that framing; xdrgen rejects any other element
type. That form has no count to loop on, so its encoder pulls
elements until the item hook returns false. False ends the list
rather than failing the encode: a producer stops when its source
is exhausted or when a reply budget fills. A producer that stops
for an error records it in its own state and either fails the
encode from the end hook or reports it in the procedure status.
For the optional-data form xdrgen generates a working
encoder only; the decoder it emits fails without consuming the
list, so xdrgen also rejects the directive when the struct is
reachable from an RPC argument.

For example:

  pragma aggregate secattr aclent;
  pragma aggregate secattr dfaclent;

The hooks are supplied by hand. Their names derive from the pragma
header and the containing struct, so that two programs sharing a
type name resolve to distinct symbols. All marked members of one
struct share a hook set, so they share one element type as well;
xdrgen rejects a marked member whose element type differs from
that of a member already marked in the same struct. The cursor's
member_id field selects the member:

  nfs_acl2_secattr_encode_begin()
  nfs_acl2_secattr_encode()
  nfs_acl2_secattr_encode_end()
  nfs_acl2_secattr_decode_begin()
  nfs_acl2_secattr_decode()
  nfs_acl2_secattr_decode_end()

For a variable-length array, a decoder fills the cursor's count from
the wire before calling the begin hook, and an encoder's begin hook
sets that count for the framing to bound-check and emit. An
optional-data list's encoder does not read the count and its
decoder never fills it. Once a begin hook
has succeeded its end hook runs, so that it can release what begin
took; the end hook receives the running success flag.

For each marked member xdrgen emits an enumeration constant -- the
pragma header, struct, and member joined by underscores and
upper-cased -- in the generated header, and the generated framing
initializes the cursor's member_id with it. The hooks compare
member_id against these constants rather than bare integers:

  NFS_ACL2_SECATTR_ACLENT
  NFS_ACL2_SECATTR_DFACLENT

The hooks reach their application state through the xdr_stream's
xdrgen_ctx pointer: svcxdr_init_decode() and svcxdr_init_encode()
bind it to the svc_rqst, and the generated framing copies it into
the cursor's ctx field, from which a hook reaches rq_argp or
rq_resp. xdrgen emits the hook prototypes in the generated header.

Pragma big_endian
------ ----------

  pragma big_endian <enum> ;

For variables that might contain only a small number values, it
is more efficient to avoid the byte-swap when encoding or decoding
on little-endian machines. Such is often the case with error status
codes. For example:

  pragma big_endian nfsstat3;

In this case, when generating an XDR struct or union containing a
field of type "nfsstat3", xdrgen will make the type of that field
"__be32" instead of "enum nfsstat3". XDR unions then switch on the
non-byte-swapped value of that field.

Pragma exclude
------ -------

  pragma exclude <RPC procedure> ;

In some cases, a procedure encoder or decoder function might need
special processing that cannot be automatically generated. The
automatically-generated functions might conflict or interfere with
the hand-rolled function. To avoid editing the generated source code
by hand, a pragma can specify that the procedure's encoder and
decoder functions are not included in the generated header and
source.

For example:

  pragma exclude NFSPROC3_READDIRPLUS;

Excludes the decoder function for the READDIRPLUS argument and the
encoder function for the READDIRPLUS result.

Note that because data item encoder and decoder functions are
defined "static __maybe_unused", subsequent compilation
automatically excludes data item encoder and decoder functions that
are used only by excluded procedure.

Pragma header
------ ------

  pragma header <string> ;

Provide a name to use for the header file. For example:

  pragma header nlm4;

Adds

  #include "nlm4xdr_gen.h"

to the generated source file.

Pragma public
------ ------

  pragma public <XDR data item> ;

Normally XDR encoder and decoder functions are "static". In case an
implementer wants to call these functions from other source code,
s/he can add a public pragma in the input .x file to indicate a set
of functions that should get a prototype in the generated header,
and the function definitions will not be declared static.

For example:

  pragma public nfsstat3;

Adds these prototypes in the generated header:

  bool xdrgen_decode_nfsstat3(struct xdr_stream *xdr, enum nfsstat3 *ptr);
  bool xdrgen_encode_nfsstat3(struct xdr_stream *xdr, enum nfsstat3 value);

And, in the generated source code, both of these functions appear
without the "static __maybe_unused" modifiers.

Pragma pages
------ -----

  pragma pages <type> <member> ;

A variable-length opaque or string member is normally encoded by
copying its content into the Reply buffer. When the member carries
a bulk payload that a server-side read operation has already
deposited in the pages of the Reply buffer, that copy is wasted
work, and it prevents a transport capable of direct data placement
from conveying the payload separately from the rest of the Reply.
This directive marks such a member so that the emitted encoder
inserts the payload pages into the encoding stream by reference and
marks the byte range as a result payload.

The marked member may be a member of a struct or an arm of a union,
and its type may be named directly or reached through a typedef. A
union arm declared directly as a string is the one exception: xdrgen
generates such an arm as a char *, which carries no length field, so
the directive is rejected there and the arm has to be declared
through a typedef. For example:

  pragma pages READ3resok data;
  pragma pages readlinkres data;

The first marks the opaque data member of struct READ3resok; the
second marks the path arm of union readlinkres, where path is
"typedef string path<NFS_MAXPATHLEN>".

Whether the directive drives the encoder or the decoder follows
from how the containing type is used. A server decodes arguments
and encodes results, so the directive on a member of a result type
(such as READ3resok) marks a payload to encode by reference, while
the directive on a member of an argument type (such as the data
member of an NFS WRITE argument) marks a payload to decode in place.

An argument type here is the struct named as an RPC procedure
argument, any aggregate reachable from it, and any typedef alias of
either; a result type is defined the same way. A decoded member
and an encoded one take different C representations, so a marked
type that is reachable from both an argument and a result is
rejected. The directive on a union arm of an argument type is
rejected as well:
the union generator emits only the ordinary copying decoder, so a
page-resident union arm needs generator support that does not yet
exist.

The emitted encoder reads only the marked member's length field to
determine how many octets of page content to insert; the member's
data pointer is not read. The emitted decoder reads the length
prefix and captures the payload octets by reference, leaving them in
the pages of the Receive buffer. A decoded page-resident member is
represented as a struct xdr_buf so that a later stage can hand the
payload to the server's file system without a copy.

The server and client manage payload pages differently, so this
directive applies only to server-side code generation. Client-side
handling of a page-resident member remains future work.


Future Work
-----------

Finish implementing XDR pointer and list types.

Generate client-side procedure functions

Expand the README into a user guide similar to rpcgen(1)

Add more pragma directives:

  * @skip -- do not decode, but rather skip, the specified argument
    field

Extend pragma pages to client-side code generation, and to union
arms of RPC procedure arguments

Enable something like a #include to dynamically insert the content
of other specification files

Build a unit test suite for verifying translation of XDR language
into compilable code

Add a command-line option to insert trace_printk call sites in the
generated source code, for improved (temporary) observability

Generate kernel Rust code as well as C code
