Linux NFS development
 help / color / mirror / Atom feed
From: Chuck Lever <cel@kernel.org>
To: NeilBrown <neil@brown.name>, Jeff Layton <jlayton@kernel.org>,
	Olga Kornievskaia <okorniev@redhat.com>,
	Dai Ngo <dai.ngo@oracle.com>, Tom Talpey <tom@talpey.com>
Cc: <linux-nfs@vger.kernel.org>
Subject: [PATCH v3 05/10] xdrgen: Add a "pragma pages" directive
Date: Tue,  8 Sep 2026 09:42:29 -0400	[thread overview]
Message-ID: <20260908134234.512312-6-cel@kernel.org> (raw)
In-Reply-To: <20260908134234.512312-1-cel@kernel.org>

The generated encoder for a variable-length opaque or string
member copies its content into the Reply buffer via
xdr_stream_encode_opaque(). A bulk payload such as an NFS READ
result never resides at the member's data pointer: the server
deposits it directly in the pages of the Reply buffer. Until now
the containing procedure encoder had to be hand-rolled.

Add a per-member directive that makes the emitted encoder call
svcxdr_encode_opaque_payload() for the marked member. The README
documents its syntax and the members it may name. Beyond the
struct and union cases described there, the directive also applies
to a member of a self-referential struct, which xdrgen generates
as an XDR pointer type.

A directive that binds to nothing would degrade silently to the
copying encoder, so the front end rejects one that names an
undeclared type or member, or a member that cannot carry a
payload. It also rejects a second marked member of the same type,
and a type whose members reach more than one marked member:
svcxdr_encode_opaque_payload() consumes the whole page vector and
moves the stream into the tail, so one encoder carries at most one
payload. An array of a marked type counts as reaching it more than
once, since the array encoder emits the element once per entry.

Generated code is unchanged until a specification applies the
directive.

Signed-off-by: Chuck Lever <cel@kernel.org>
Acked-by: Jeff Layton <jlayton@kernel.org>
---
 tools/net/sunrpc/xdrgen/README                |  42 +++-
 tools/net/sunrpc/xdrgen/generators/pointer.py |  25 ++-
 tools/net/sunrpc/xdrgen/generators/struct.py  |  23 +-
 tools/net/sunrpc/xdrgen/generators/union.py   |  69 ++++--
 .../C/pointer/encoder/pages_opaque.j2         |  10 +
 .../C/struct/encoder/pages_opaque.j2          |  10 +
 .../templates/C/union/encoder/pages_opaque.j2 |  10 +
 tools/net/sunrpc/xdrgen/xdr_ast.py            | 209 ++++++++++++++++++
 8 files changed, 376 insertions(+), 22 deletions(-)
 create mode 100644 tools/net/sunrpc/xdrgen/templates/C/pointer/encoder/pages_opaque.j2
 create mode 100644 tools/net/sunrpc/xdrgen/templates/C/struct/encoder/pages_opaque.j2
 create mode 100644 tools/net/sunrpc/xdrgen/templates/C/union/encoder/pages_opaque.j2

diff --git a/tools/net/sunrpc/xdrgen/README b/tools/net/sunrpc/xdrgen/README
index 2cf05d1e4cd9..5af9cf506637 100644
--- a/tools/net/sunrpc/xdrgen/README
+++ b/tools/net/sunrpc/xdrgen/README
@@ -230,6 +230,43 @@ Adds these prototypes in the generated header:
 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>".
+
+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 server and client manage payload
+pages differently, so this directive applies only to server-side
+code generation, and so far only to the encoding direction.
+Decode-side handling of a page-resident argument (for example, the
+payload of an NFS WRITE) remains hand-coded.
+
 
 Future Work
 -----------
@@ -242,11 +279,12 @@ Expand the README into a user guide similar to rpcgen(1)
 
 Add more pragma directives:
 
-  * @pages -- use xdr_read/write_pages() for the specified opaque
-    field
   * @skip -- do not decode, but rather skip, the specified argument
     field
 
+Extend pragma pages to the decoding direction and to
+client-side code generation
+
 Enable something like a #include to dynamically insert the content
 of other specification files
 
diff --git a/tools/net/sunrpc/xdrgen/generators/pointer.py b/tools/net/sunrpc/xdrgen/generators/pointer.py
index 9a540e125786..448ebd684212 100644
--- a/tools/net/sunrpc/xdrgen/generators/pointer.py
+++ b/tools/net/sunrpc/xdrgen/generators/pointer.py
@@ -13,6 +13,7 @@ from xdr_ast import _XdrFixedLengthOpaque, _XdrVariableLengthOpaque
 from xdr_ast import _XdrFixedLengthArray, _XdrVariableLengthArray
 from xdr_ast import _XdrOptionalData, _XdrPointer, _XdrDeclaration
 from xdr_ast import public_apis, get_header_name
+from xdr_ast import pages_members, pages_member_maxsize
 
 
 def emit_pointer_declaration(environment: Environment, node: _XdrPointer) -> None:
@@ -176,8 +177,24 @@ def emit_pointer_member_encoder(
     environment: Environment,
     field: _XdrDeclaration,
     struct_name: str,
+    peer: str,
 ) -> None:
     """Emit an encoder for one field in a XDR pointer"""
+    if (struct_name, field.name) in pages_members:
+        if peer != "server":
+            raise NotImplementedError(
+                "pragma pages is server-side encode-only; "
+                + peer
+                + " generation is not yet supported"
+            )
+        template = get_jinja2_template(environment, "encoder", "pages_opaque")
+        print(
+            template.render(
+                name=field.name,
+                maxsize=pages_member_maxsize(field),
+            )
+        )
+        return
     if isinstance(field, _XdrBasic):
         template = get_jinja2_template(environment, "encoder", field.template)
         print(
@@ -239,13 +256,15 @@ def emit_pointer_member_encoder(
         )
 
 
-def emit_pointer_encoder(environment: Environment, node: _XdrPointer) -> None:
+def emit_pointer_encoder(
+    environment: Environment, node: _XdrPointer, peer: str
+) -> None:
     """Emit one encoder function for an XDR pointer type"""
     template = get_jinja2_template(environment, "encoder", "open")
     print(template.render(name=node.name))
 
     for field in node.fields[0:-1]:
-        emit_pointer_member_encoder(environment, field, node.name)
+        emit_pointer_member_encoder(environment, field, node.name, peer)
 
     template = get_jinja2_template(environment, "encoder", "close")
     print(template.render())
@@ -285,7 +304,7 @@ class XdrPointerGenerator(SourceGenerator):
 
     def emit_encoder(self, node: _XdrPointer) -> None:
         """Emit one encoder function for an XDR pointer type"""
-        emit_pointer_encoder(self.environment, node)
+        emit_pointer_encoder(self.environment, node, self.peer)
 
     def emit_maxsize(self, node: _XdrPointer) -> None:
         """Emit one maxsize macro for an XDR pointer type"""
diff --git a/tools/net/sunrpc/xdrgen/generators/struct.py b/tools/net/sunrpc/xdrgen/generators/struct.py
index 91da3863c166..76fd3afbeea0 100644
--- a/tools/net/sunrpc/xdrgen/generators/struct.py
+++ b/tools/net/sunrpc/xdrgen/generators/struct.py
@@ -13,6 +13,7 @@ from xdr_ast import _XdrFixedLengthOpaque, _XdrVariableLengthOpaque
 from xdr_ast import _XdrFixedLengthArray, _XdrVariableLengthArray
 from xdr_ast import _XdrOptionalData, _XdrStruct, _XdrDeclaration
 from xdr_ast import public_apis, get_header_name
+from xdr_ast import pages_members, pages_member_maxsize
 
 
 def emit_struct_declaration(environment: Environment, node: _XdrStruct) -> None:
@@ -176,8 +177,24 @@ def emit_struct_member_encoder(
     environment: Environment,
     field: _XdrDeclaration,
     struct_name: str,
+    peer: str,
 ) -> None:
     """Emit an encoder for one field in an XDR struct"""
+    if (struct_name, field.name) in pages_members:
+        if peer != "server":
+            raise NotImplementedError(
+                "pragma pages is server-side encode-only; "
+                + peer
+                + " generation is not yet supported"
+            )
+        template = get_jinja2_template(environment, "encoder", "pages_opaque")
+        print(
+            template.render(
+                name=field.name,
+                maxsize=pages_member_maxsize(field),
+            )
+        )
+        return
     if isinstance(field, _XdrBasic):
         template = get_jinja2_template(environment, "encoder", field.template)
         print(
@@ -239,13 +256,13 @@ def emit_struct_member_encoder(
         )
 
 
-def emit_struct_encoder(environment: Environment, node: _XdrStruct) -> None:
+def emit_struct_encoder(environment: Environment, node: _XdrStruct, peer: str) -> None:
     """Emit one encoder function for an XDR struct type"""
     template = get_jinja2_template(environment, "encoder", "open")
     print(template.render(name=node.name))
 
     for field in node.fields:
-        emit_struct_member_encoder(environment, field, node.name)
+        emit_struct_member_encoder(environment, field, node.name, peer)
 
     template = get_jinja2_template(environment, "encoder", "close")
     print(template.render())
@@ -285,7 +302,7 @@ class XdrStructGenerator(SourceGenerator):
 
     def emit_encoder(self, node: _XdrStruct) -> None:
         """Emit one encoder function for an XDR struct type"""
-        emit_struct_encoder(self.environment, node)
+        emit_struct_encoder(self.environment, node, self.peer)
 
     def emit_maxsize(self, node: _XdrStruct) -> None:
         """Emit one maxsize macro for an XDR struct type"""
diff --git a/tools/net/sunrpc/xdrgen/generators/union.py b/tools/net/sunrpc/xdrgen/generators/union.py
index d15837dae651..47d63236f88f 100644
--- a/tools/net/sunrpc/xdrgen/generators/union.py
+++ b/tools/net/sunrpc/xdrgen/generators/union.py
@@ -9,7 +9,8 @@ from generators import SourceGenerator
 from generators import create_jinja2_environment, get_jinja2_template
 
 from xdr_ast import _XdrBasic, _XdrUnion, _XdrVoid, _XdrString, get_header_name
-from xdr_ast import _XdrDeclaration, _XdrCaseSpec, public_apis, big_endian
+from xdr_ast import _XdrDeclaration, _XdrCaseSpec
+from xdr_ast import public_apis, big_endian, pages_members, pages_member_maxsize
 
 
 def emit_union_declaration(environment: Environment, node: _XdrUnion) -> None:
@@ -214,13 +215,41 @@ def emit_union_switch_spec_encoder(
     print(template.render(name=node.name, type=node.spec.type_name))
 
 
+def emit_union_pages_arm_encoder(
+    environment: Environment, union_name: str, arm: _XdrDeclaration, peer: str
+) -> bool:
+    """Emit a page-resident encoder for a "pragma pages" union arm
+
+    Return True when the arm is pages-marked and was emitted here, so
+    the caller skips the ordinary arm encoder.
+    """
+    if (union_name, arm.name) not in pages_members:
+        return False
+    if peer != "server":
+        raise NotImplementedError(
+            "pragma pages is server-side encode-only; "
+            + peer
+            + " generation is not yet supported"
+        )
+    template = get_jinja2_template(environment, "encoder", "pages_opaque")
+    print(
+        template.render(
+            name=arm.name,
+            maxsize=pages_member_maxsize(arm),
+        )
+    )
+    return True
+
+
 def emit_union_arm_encoder(
-    environment: Environment, node: _XdrCaseSpec
+    environment: Environment, node: _XdrCaseSpec, union_name: str, peer: str
 ) -> None:
     """Emit encoder for an XDR union's arm (data only, no case/break)"""
 
     if isinstance(node.arm, _XdrVoid):
         return
+    if emit_union_pages_arm_encoder(environment, union_name, node.arm, peer):
+        return
     if isinstance(node.arm, _XdrString):
         type_name = "char *"
     else:
@@ -237,7 +266,11 @@ def emit_union_arm_encoder(
 
 
 def emit_union_case_spec_encoder(
-    environment: Environment, node: _XdrCaseSpec, big_endian_discriminant: bool
+    environment: Environment,
+    node: _XdrCaseSpec,
+    big_endian_discriminant: bool,
+    union_name: str,
+    peer: str,
 ) -> None:
     """Emit encoder functions for an XDR union's case arm"""
 
@@ -254,19 +287,22 @@ def emit_union_case_spec_encoder(
     for case in node.values:
         print(template.render(case=case))
 
-    template = get_jinja2_template(environment, "encoder", node.arm.template)
-    print(
-        template.render(
-            name=node.arm.name,
-            type=type_name,
+    if not emit_union_pages_arm_encoder(environment, union_name, node.arm, peer):
+        template = get_jinja2_template(environment, "encoder", node.arm.template)
+        print(
+            template.render(
+                name=node.arm.name,
+                type=type_name,
+            )
         )
-    )
 
     template = get_jinja2_template(environment, "encoder", "break")
     print(template.render())
 
 
-def emit_union_default_spec_encoder(environment: Environment, node: _XdrUnion) -> None:
+def emit_union_default_spec_encoder(
+    environment: Environment, node: _XdrUnion, peer: str
+) -> None:
     """Emit an encoder function for an XDR union's default arm"""
     default_case = node.default
 
@@ -282,6 +318,9 @@ def emit_union_default_spec_encoder(environment: Environment, node: _XdrUnion) -
         print(template.render())
         return
 
+    if emit_union_pages_arm_encoder(environment, node.name, default_case.arm, peer):
+        return
+
     template = get_jinja2_template(environment, "encoder", default_case.arm.template)
     print(
         template.render(
@@ -291,7 +330,7 @@ def emit_union_default_spec_encoder(environment: Environment, node: _XdrUnion) -
     )
 
 
-def emit_union_encoder(environment, node: _XdrUnion) -> None:
+def emit_union_encoder(environment, node: _XdrUnion, peer: str) -> None:
     """Emit one XDR union encoder"""
     template = get_jinja2_template(environment, "encoder", "open")
     print(template.render(name=node.name))
@@ -304,7 +343,7 @@ def emit_union_encoder(environment, node: _XdrUnion) -> None:
         # Find and emit the TRUE case
         for case in node.cases:
             if case.values and case.values[0] == "TRUE":
-                emit_union_arm_encoder(environment, case)
+                emit_union_arm_encoder(environment, case, node.name, peer)
                 break
 
         template = get_jinja2_template(environment, "encoder", "close")
@@ -317,9 +356,11 @@ def emit_union_encoder(environment, node: _XdrUnion) -> None:
                 environment,
                 case,
                 node.discriminant.spec.type_name in big_endian,
+                node.name,
+                peer,
             )
 
-        emit_union_default_spec_encoder(environment, node)
+        emit_union_default_spec_encoder(environment, node, peer)
 
         template = get_jinja2_template(environment, "encoder", "close")
         print(template.render())
@@ -359,7 +400,7 @@ class XdrUnionGenerator(SourceGenerator):
 
     def emit_encoder(self, node: _XdrUnion) -> None:
         """Emit one encoder function for an XDR union"""
-        emit_union_encoder(self.environment, node)
+        emit_union_encoder(self.environment, node, self.peer)
 
     def emit_maxsize(self, node: _XdrUnion) -> None:
         """Emit one maxsize macro for an XDR union"""
diff --git a/tools/net/sunrpc/xdrgen/templates/C/pointer/encoder/pages_opaque.j2 b/tools/net/sunrpc/xdrgen/templates/C/pointer/encoder/pages_opaque.j2
new file mode 100644
index 000000000000..e868220a59f6
--- /dev/null
+++ b/tools/net/sunrpc/xdrgen/templates/C/pointer/encoder/pages_opaque.j2
@@ -0,0 +1,10 @@
+{# SPDX-License-Identifier: GPL-2.0 #}
+{% if annotate %}
+	/* member {{ name }} (page-resident variable-length opaque) */
+{% endif %}
+{% if maxsize != "0" %}
+	if (value->{{ name }}.len > {{ maxsize }})
+		return false;
+{% endif %}
+	if (!svcxdr_encode_opaque_payload(xdr, value->{{ name }}.len))
+		return false;
diff --git a/tools/net/sunrpc/xdrgen/templates/C/struct/encoder/pages_opaque.j2 b/tools/net/sunrpc/xdrgen/templates/C/struct/encoder/pages_opaque.j2
new file mode 100644
index 000000000000..e868220a59f6
--- /dev/null
+++ b/tools/net/sunrpc/xdrgen/templates/C/struct/encoder/pages_opaque.j2
@@ -0,0 +1,10 @@
+{# SPDX-License-Identifier: GPL-2.0 #}
+{% if annotate %}
+	/* member {{ name }} (page-resident variable-length opaque) */
+{% endif %}
+{% if maxsize != "0" %}
+	if (value->{{ name }}.len > {{ maxsize }})
+		return false;
+{% endif %}
+	if (!svcxdr_encode_opaque_payload(xdr, value->{{ name }}.len))
+		return false;
diff --git a/tools/net/sunrpc/xdrgen/templates/C/union/encoder/pages_opaque.j2 b/tools/net/sunrpc/xdrgen/templates/C/union/encoder/pages_opaque.j2
new file mode 100644
index 000000000000..7fe3108a44c0
--- /dev/null
+++ b/tools/net/sunrpc/xdrgen/templates/C/union/encoder/pages_opaque.j2
@@ -0,0 +1,10 @@
+{# SPDX-License-Identifier: GPL-2.0 #}
+{% if annotate %}
+		/* member {{ name }} (page-resident variable-length opaque) */
+{% endif %}
+{% if maxsize != "0" %}
+		if (ptr->u.{{ name }}.len > {{ maxsize }})
+			return false;
+{% endif %}
+		if (!svcxdr_encode_opaque_payload(xdr, ptr->u.{{ name }}.len))
+			return false;
diff --git a/tools/net/sunrpc/xdrgen/xdr_ast.py b/tools/net/sunrpc/xdrgen/xdr_ast.py
index 9dab8bc545b0..c29555b695a9 100644
--- a/tools/net/sunrpc/xdrgen/xdr_ast.py
+++ b/tools/net/sunrpc/xdrgen/xdr_ast.py
@@ -19,6 +19,21 @@ public_apis = []
 structs = set()
 pass_by_reference = set()
 
+# (type_name, member_name) pairs marked "pragma pages": the member's
+# content resides in the pages of the Reply buffer, so the emitted
+# encoder inserts those pages by reference instead of copying.
+pages_members = set()
+
+# The same pairs, keyed to the directive's identifier node so a
+# diagnostic can point at the directive rather than at the type.
+pages_member_meta = {}
+
+# Typedefs of a variable-length opaque or string, mapped to the length
+# bound. A "pragma pages" member declared through one ("path data;"
+# with "typedef string path<NFS_MAXPATHLEN>") reaches the AST as a
+# basic type reference, so the bound must be recovered from here.
+varlen_object_typedefs = {}
+
 constants = {}
 
 
@@ -427,15 +442,34 @@ class _XdrTypedef(_XdrAst):
         return self.declaration.symbolic_width()
 
     def __post_init__(self):
+        if isinstance(self.declaration, (_XdrVariableLengthOpaque, _XdrString)):
+            varlen_object_typedefs[self.declaration.name] = self.declaration.maxsize
         if isinstance(self.declaration, _XdrBasic):
             new_type = self.declaration
             if isinstance(new_type.spec, _XdrDefinedType):
                 if new_type.spec.type_name in pass_by_reference:
                     pass_by_reference.add(new_type.name)
+                if new_type.spec.type_name in varlen_object_typedefs:
+                    varlen_object_typedefs[new_type.name] = varlen_object_typedefs[
+                        new_type.spec.type_name
+                    ]
                 max_widths[new_type.name] = self.max_width()
                 symbolic_widths[new_type.name] = self.symbolic_width()
 
 
+def pages_member_maxsize(field: _XdrDeclaration):
+    """Return the length bound of a "pragma pages" member.
+
+    Return None when FIELD is neither a variable-length opaque nor a
+    string, whether declared inline or through a typedef.
+    """
+    if isinstance(field, (_XdrVariableLengthOpaque, _XdrString)):
+        return field.maxsize
+    if isinstance(field, _XdrBasic) and field.spec.type_name in varlen_object_typedefs:
+        return varlen_object_typedefs[field.spec.type_name]
+    return None
+
+
 @dataclass
 class _XdrCaseSpec(_XdrAst):
     """One case in an XDR union"""
@@ -791,6 +825,15 @@ class ParseToAst(Transformer):
                 header_name = children[1].symbol
             case "public_directive":
                 public_apis.append(children[1].symbol)
+            case "pages_directive":
+                if children[2] is None:
+                    raise XdrSemanticError(
+                        "pragma pages requires a type name and a member name",
+                        children[1],
+                    )
+                marked = (children[1].symbol, children[2].symbol)
+                pages_members.add(marked)
+                pages_member_meta[marked] = children[2]
             case _:
                 raise NotImplementedError("Directive not supported")
         return _Pragma()
@@ -990,12 +1033,178 @@ def check_rpc_number_range(root: "Specification") -> None:
                 )
 
 
+def _pages_candidate_members(value):
+    """Yield (member_name, declaration, container) for a type's members.
+
+    CONTAINER is the AST node holding the declaration; it decides how
+    the member is rendered in the target language.
+    """
+    if isinstance(value, _XdrStruct):
+        for field in value.fields:
+            yield field.name, field, value
+    elif isinstance(value, _XdrPointer):
+        # The trailing field is the self-reference that makes this an
+        # XDR pointer type; the emitter does not encode it.
+        for field in value.fields[0:-1]:
+            yield field.name, field, value
+    elif isinstance(value, _XdrUnion):
+        cases = list(value.cases)
+        if value.default is not None:
+            cases.append(value.default)
+        for case in cases:
+            if not isinstance(case.arm, _XdrVoid):
+                yield case.arm.name, case.arm, value
+
+
+def check_pages_directives(root: "Specification") -> None:
+    """Reject a "pragma pages" directive that cannot be honored.
+
+    Run in the front end so the diagnostic carries the directive's own
+    source position rather than surfacing as a traceback from whichever
+    emitter reaches the member. A directive that binds to nothing would
+    otherwise degrade silently to the copying encoder.
+    """
+    payloads = {}
+    resolved = set()
+    for definition in root.definitions:
+        value = definition.value
+        type_name = getattr(value, "name", None)
+        members = dict(
+            (name, (field, container))
+            for name, field, container in _pages_candidate_members(value)
+        )
+        for marked in sorted(pages_members):
+            if marked[0] != type_name:
+                continue
+            meta = pages_member_meta.get(marked)
+            if marked[1] not in members:
+                raise XdrSemanticError(
+                    f"type '{type_name}' has no member '{marked[1]}'",
+                    meta,
+                )
+            field, container = members[marked[1]]
+            # A union arm declared directly as a string is generated as
+            # a char *, with no length field for the page encoder to
+            # read; inside a struct it becomes a { len, data } object.
+            if isinstance(container, _XdrUnion) and isinstance(field, _XdrString):
+                raise XdrSemanticError(
+                    f"union arm '{type_name}.{marked[1]}' is declared"
+                    " directly as a string and carries no length field;"
+                    " declare it through a typedef instead",
+                    meta,
+                )
+            if pages_member_maxsize(field) is None:
+                raise XdrSemanticError(
+                    f"'{type_name}.{marked[1]}' is neither a"
+                    " variable-length opaque nor a string",
+                    meta,
+                )
+            # svcxdr_encode_opaque_payload() consumes the whole page
+            # vector and moves the stream into the tail; a second
+            # payload would overwrite the first one's framing.
+            first = payloads.get(type_name)
+            if first is not None:
+                raise XdrSemanticError(
+                    f"'{type_name}' already marks member '{first}' as"
+                    " page-resident; an encoder emits at most one"
+                    " page-resident payload",
+                    meta,
+                )
+            payloads[type_name] = marked[1]
+            resolved.add(marked)
+
+    for marked in sorted(pages_members - resolved):
+        raise XdrSemanticError(
+            f"pragma pages names unknown type '{marked[0]}'",
+            pages_member_meta.get(marked),
+        )
+
+    _check_pages_containment(root, payloads)
+
+
+def _member_type_names(value) -> list:
+    """Return the type names a definition's members reference, one
+    entry per member so a type used twice is counted twice. An array
+    member's encoder emits the element type once per element, so its
+    element type is counted twice as well."""
+    if isinstance(value, _XdrStruct):
+        fields = value.fields
+    elif isinstance(value, _XdrPointer):
+        # The trailing self-reference is list framing that the
+        # caller's loop walks, not a member the pointer's own
+        # encoder emits.
+        fields = value.fields[0:-1]
+    elif isinstance(value, _XdrUnion):
+        fields = [case.arm for case in value.cases]
+        if value.default:
+            fields.append(value.default.arm)
+    elif isinstance(value, _XdrTypedef):
+        fields = [value.declaration]
+    else:
+        return []
+    names = []
+    for field in fields:
+        spec = getattr(field, "spec", None)
+        if spec is not None:
+            names.append(spec.type_name)
+            if isinstance(field, (_XdrFixedLengthArray, _XdrVariableLengthArray)):
+                names.append(spec.type_name)
+    return names
+
+
+def _reachable_payloads(type_name, members, payloads, active) -> list:
+    """Return the marked members an encoder for TYPE_NAME emits, one
+    entry per path that reaches one, so a marked type held by two
+    members appears twice."""
+    if type_name in active:
+        return []
+    active.add(type_name)
+    found = []
+    if type_name in payloads:
+        found.append((type_name, payloads[type_name]))
+    for member_type in members.get(type_name, ()):
+        found += _reachable_payloads(member_type, members, payloads, active)
+    active.discard(type_name)
+    return found
+
+
+def _check_pages_containment(root: "Specification", payloads: dict) -> None:
+    """Reject a type whose encoder reaches more than one page-resident
+    payload through the types its members contain.
+
+    The one-payload check in check_pages_directives() sees only the
+    directly marked type. A type that holds two members of a marked
+    type, or members of two marked types, would call
+    svcxdr_encode_opaque_payload() once per member and overwrite the
+    first payload's framing just as a second marked member would.
+    """
+    members = {}
+    for definition in root.definitions:
+        value = definition.value
+        name = getattr(value, "name", None)
+        if name is None and isinstance(value, _XdrTypedef):
+            name = value.declaration.name
+        if name is not None:
+            members[name] = _member_type_names(value)
+
+    for name in members:
+        reached = _reachable_payloads(name, members, payloads, set())
+        if len(reached) > 1:
+            raise XdrSemanticError(
+                f"'{name}' reaches {len(reached)} page-resident payloads"
+                " through its members; an encoder emits at most one"
+                " page-resident payload",
+                pages_member_meta.get(reached[1]),
+            )
+
+
 def transform_parse_tree(parse_tree):
     """Transform productions into an abstract syntax tree"""
     ast = transformer.transform(parse_tree)
     ast.definitions = _merge_consecutive_passthru(ast.definitions)
     check_duplicate_definitions(ast)
     check_rpc_number_range(ast)
+    check_pages_directives(ast)
     return ast
 
 
-- 
2.55.0


  parent reply	other threads:[~2026-09-08 13:42 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-08 13:42 [PATCH v3 00/10] New pragmas for the xdrgen tool Chuck Lever
2026-09-08 13:42 ` [PATCH v3 01/10] SUNRPC: Carry a generated-codec context pointer in struct xdr_stream Chuck Lever
2026-09-09 23:31   ` NeilBrown
2026-09-08 13:42 ` [PATCH v3 02/10] SUNRPC: Bind the svc_rqst to its XDR streams Chuck Lever
2026-09-08 13:42 ` [PATCH v3 03/10] SUNRPC: Add svcxdr_encode_opaque_payload() Chuck Lever
2026-09-08 13:42 ` [PATCH v3 04/10] xdrgen: Pass the containing struct name to member codec emitters Chuck Lever
2026-09-08 13:42 ` Chuck Lever [this message]
2026-09-08 13:42 ` [PATCH v3 06/10] SUNRPC: Add svcxdr_decode_opaque_payload() Chuck Lever
2026-09-08 13:42 ` [PATCH v3 07/10] xdrgen: Extend the pages directive to page-resident arguments Chuck Lever
2026-09-08 13:42 ` [PATCH v3 08/10] xdrgen: Add hook-driven aggregate codec for variable-length arrays Chuck Lever
2026-09-08 13:42 ` [PATCH v3 09/10] xdrgen: Extend the aggregate codec to optional-data list members Chuck Lever
2026-09-08 13:42 ` [PATCH v3 10/10] xdrgen: Stream optional-data aggregate lists during encode Chuck Lever

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260908134234.512312-6-cel@kernel.org \
    --to=cel@kernel.org \
    --cc=dai.ngo@oracle.com \
    --cc=jlayton@kernel.org \
    --cc=linux-nfs@vger.kernel.org \
    --cc=neil@brown.name \
    --cc=okorniev@redhat.com \
    --cc=tom@talpey.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox