Skip to content

api: add ABI compatibility check and payload validation#578

Open
rjarry wants to merge 7 commits intoDPDK:mainfrom
rjarry:api-version-hash
Open

api: add ABI compatibility check and payload validation#578
rjarry wants to merge 7 commits intoDPDK:mainfrom
rjarry:api-version-hash

Conversation

@rjarry
Copy link
Copy Markdown
Collaborator

@rjarry rjarry commented Mar 28, 2026

Annotate every API request and event with GR_REQ()/GR_EVENT() macros that associate request codes with their payload types. These macros generate compile-time size constants and can be overridden by client libraries to extract metadata differently.

The server uses these constants to reject requests with truncated payloads (EMSGSIZE) before dispatching to handlers. On the client side, gr_api_client_impl.h overrides the macros to build a runtime exchange registry via constructors, enabling response size validation and a generic code-to-name lookup that replaces hand-maintained switch statements.

At build time, abidiff compares the current API headers against the previous git revision. If breaking struct layout changes are detected and GR_API_VERSION has not been bumped, the build fails. The GR_HELLO handshake now negotiates on this integer instead of comparing version strings, so that compatible builds from different commits can interoperate.

@rjarry rjarry requested a review from david-marchand March 28, 2026 18:01
@coderabbitai

This comment was marked as resolved.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@devtools/gen_version_h.sh`:
- Around line 22-35: The hashing step in gen_version_h.sh omits project
compile-time defines so api_hash (computed in the api_hash variable using $cc -E
-P $flags) can differ from full builds; update the script to include the same
defines used by the build system by appending the project's C/CXX compilation
defines (e.g., meson-provided defines or CFLAGS/CXXFLAGS/CPPFLAGS like
-D_GNU_SOURCE, -DALLOW_EXPERIMENTAL_API) into the flags variable before
preprocessing so the preprocessed output matches full-build macros when
computing api_hash.

In `@main/api.c`:
- Around line 204-214: The handler hello currently dereferences request as a
full struct gr_hello_req and reads api_hash without validating the payload size;
add a guard at the top of hello to ensure request is non-NULL and that the
provided payload length (from the api_ctx passed into hello, e.g.
ctx->payload_len) is at least sizeof(struct gr_hello_req); if not, return
api_out(EBADMSG, 0, NULL) to reject short/malformed GR_HELLO payloads before
accessing req->api_hash. Ensure the check uses the same symbols (hello, struct
gr_hello_req, api_ctx, api_out, EBADMSG, GR_HELLO) so the protection applies to
truncated buffers read by read_cb.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ec49cdea-654e-413a-8e5f-7c18ecc3fd30

📥 Commits

Reviewing files that changed from the base of the PR and between a5520bf and 246c81b.

📒 Files selected for processing (7)
  • api/gr_api.h
  • api/gr_api_client_impl.h
  • api/meson.build
  • devtools/gen_version_h.sh
  • frr/meson.build
  • main/api.c
  • meson.build
💤 Files with no reviewable changes (1)
  • api/meson.build

@rjarry rjarry force-pushed the api-version-hash branch from 246c81b to 82f62fc Compare March 28, 2026 18:59
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@devtools/gen_version_h.sh`:
- Line 35: The api_hash assignment pipeline using "$cc -E -P $flags -x c
/dev/null | sed ... | sha256sum | cut ..." can silently succeed even if the
compiler fails; modify the script to enable strict failure detection (e.g. set
-o pipefail or explicitly run the preprocessor into a temp file), check the exit
status of the preprocessor invocation ($cc -E -P ...) before feeding its output
to sed/sha256sum, and if the preprocessor fails emit an error and exit non-zero
so that api_hash (the variable assigned from that pipeline) is never populated
with an invalid empty hash and the header generation aborts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 346fcaed-f497-4a9f-b472-b41888a6ce3e

📥 Commits

Reviewing files that changed from the base of the PR and between 246c81b and 82f62fc.

📒 Files selected for processing (7)
  • api/gr_api.h
  • api/gr_api_client_impl.h
  • api/meson.build
  • devtools/gen_version_h.sh
  • frr/meson.build
  • main/api.c
  • meson.build
💤 Files with no reviewable changes (1)
  • api/meson.build
✅ Files skipped from review due to trivial changes (3)
  • api/gr_api_client_impl.h
  • frr/meson.build
  • api/gr_api.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • main/api.c

@rjarry rjarry force-pushed the api-version-hash branch from c689e02 to 3f9b4ee Compare March 28, 2026 20:47
@david-marchand
Copy link
Copy Markdown
Member

I did not review yet, just some high level question/suggestion.

Would it help to keep a single version number, but map it to this digest the series introduces?

The idea is to have the digest as a comment in the header defining the version number, and have the build framework throw an error if the newly computed digest does not match.

This should allow adding new structures/features but still be compatible for existings features.

It could also help the other way around, like if grout changes its internal behavior for a strictly identical digest and we bump the version number so that external consumer must be updated.

@rjarry
Copy link
Copy Markdown
Collaborator Author

rjarry commented Mar 30, 2026

Hmm, let me see if I understood your suggestion. You're proposing that we add a manual:

// gr_api.h
#define GR_API_VERSION 1

And we bump it every time we think we make a breaking change in the API.

Then, during build time, we compute the hash based on API header contents.

But I don't understand how you can "compare" a hash with a number. There is a chicken and egg problem. That would mean, we need to store the hash in git to compare it.

And also, that would mean you cannot decide to bump the number because the internal handling of the API warrants a change. If the hash didn't change, the build system will error out because the version number changed without any hash change.

Did I misunderstand something?

@rjarry rjarry force-pushed the api-version-hash branch 3 times, most recently from c7b8784 to 0c35816 Compare March 30, 2026 11:11
@david-marchand
Copy link
Copy Markdown
Member

The digest would be stored along the version.
Like:

#define GR_API_VERSION 1 // digest: XXXXXXXXXXXXXXXXXXXXXXXXXX

@rjarry
Copy link
Copy Markdown
Collaborator Author

rjarry commented Mar 30, 2026

Ah, so at runtime you would only check GR_API_VERSION, and during build you would check the digest?

@david-marchand
Copy link
Copy Markdown
Member

Yes, the build time check is to avoid API breaking change going unnoticed.
We still need to check manually when build fails, and a version bump must then be debatted.

Another option could be to ping libabigail people, if they have better ideas.

@rjarry
Copy link
Copy Markdown
Collaborator Author

rjarry commented Mar 30, 2026

I will see what I can do. That seems more flexible than just the compile time generated hash.

@rjarry rjarry force-pushed the api-version-hash branch 2 times, most recently from db55ad9 to dd566c9 Compare March 30, 2026 12:12
@rjarry rjarry changed the title api: replace GROUT_VERSION check with API struct hash api: replace GROUT_VERSION check with API version number Mar 30, 2026
@rjarry rjarry force-pushed the api-version-hash branch 5 times, most recently from 4f5d579 to 4dedfd0 Compare March 31, 2026 10:41
@rjarry rjarry changed the title api: replace GROUT_VERSION check with API version number api: add ABI compatibility check and version negotiation Mar 31, 2026
@rjarry rjarry force-pushed the api-version-hash branch 3 times, most recently from cb31cdd to 5beccfe Compare March 31, 2026 11:59
out.payload = NULL;
goto send;
}
if (ctx->header.payload_len < handler->req_size) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we validate only against payload smaller, or should we rather check for the exact length ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to check for exact length, but some requests have variable length (GR_IFACE_ADD, GR_NH_ADD). So we can only enforce a minimum size.

@rjarry rjarry force-pushed the api-version-hash branch from 5beccfe to b31bb16 Compare March 31, 2026 19:46
@rjarry
Copy link
Copy Markdown
Collaborator Author

rjarry commented Mar 31, 2026

For reference, here is the output of the new check:

$ PREV_REVISION=HEAD^^^ make 
ninja: Entering directory `build'
[1/1] Generating api_check with a custom command
Checking for API changes between v0.14.3-297-g5e76f273 and v0.14.3-300-g7ac5ff29-dirty

1 changed type unreachable from any public interface:

  [C] 'struct gr_hello_req' changed:
    type size changed from 128 to 132 (in bytes)
    1 data member insertion:
      'uint32_t api_version', at offset 0 (in bytes) at gr_api.h:147:1
    1 data member change:
      'char version[128]' offset changed from 0 to 4 (in bytes) (by +4 bytes)

breaking API changes
GR_API_VERSION changed from  to 1

@rjarry rjarry force-pushed the api-version-hash branch 11 times, most recently from 258e417 to be52fb8 Compare April 7, 2026 11:22

// Interface events.
typedef enum {
GR_EVENT_IFACE_UNKNOWN = EVENT_TYPE(GR_INFRA_MODULE, 0x0000),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One reference was left behind:

rjarry/api-version-hash:main/api.c:// GR_EVENT_IFACE_UNKNOWN | 0x0000 |

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unused anywhere. I didn't think necessary to keep this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, then please remove the last occurence.


enum gr_main_requests : uint32_t {
GR_HELLO = 0xcafe1981u,
GR_LOG_PACKETS_SET,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why break ABI?

IIUC, GR_LOG_PACKETS_SET was 0xCAFE0010 so far.

Copy link
Copy Markdown
Collaborator Author

@rjarry rjarry Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though that we could simplify the definitions of these request/event codes with enums and at this point (commit 4bb6da7) there is virtually no abi compatibility possible since clients built on a different commit hash from the server get rejected anyway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes true.
Now, la récréation est finie.
No more gratuitous breakage.

api/gr_api.h Outdated
struct gr_empty { };

enum gr_main_requests : uint32_t {
GR_HELLO = 0xcafe1981u,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have kept the REQUEST_TYPE macro.

enum gr_main_requests : uint32_t {
	GR_HELLO = REQUEST_TYPE(GR_MAIN_MODULE, 0x1981),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since IDs are now part of enums, this would be used only once per module. Do you think it still has any value?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind.

GR_IFACE_LIST,
GR_IFACE_SET,
GR_IFACE_STATS_GET,
GR_AFFINITY_RXQ_LIST,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, some ABI breakage, if I count correctly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, see the reasoning in my previous comment.

Copy link
Copy Markdown
Member

@david-marchand david-marchand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please cleanup main/api.c.

I see:
// GR_EVENT_IFACE_UNKNOWN | 0x0000 |

But also:
main/api.c:// GR_EVENT_IFACE_ADD | 0x0001 |
modules/infra/api/gr_infra.h: GR_EVENT_IFACE_ADD = 0xacdc1001u,

main/api.c:// GR_EVENT_NEXTHOP_UPDATE -> | 0x0102 | -> vec of evutil_socket_t
modules/infra/api/gr_nexthop.h: GR_EVENT_NEXTHOP_NEW = 0xd0171001,
modules/infra/api/gr_nexthop.h: GR_EVENT_NEXTHOP_DELETE,
modules/infra/api/gr_nexthop.h: GR_EVENT_NEXTHOP_UPDATE,

The series lgtm otherwise.

rjarry added 6 commits April 7, 2026 21:25
Replace REQUEST_TYPE/EVENT_TYPE #define codes with typed enums and
annotate each request/response pair with GR_REQ(), GR_REQ_STREAM()
or GR_EVENT() macros.

These macros serve two purposes: they document the association
between a request code and its payload types, and they generate
compile-time size constants (e.g. GR_HELLO_REQ_SIZE) via anonymous
enums. The macros can be overridden before including the headers so
that other tools (ABI checkers, client libraries) can extract the
same metadata differently.

The old REQUEST_TYPE/EVENT_TYPE/STREAM_RESP macros and commented-out
empty struct placeholders are removed.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Reviewed-by: Christophe Fontaine <cfontain@redhat.com>
Reviewed-by: David Marchand <david.marchand@redhat.com>
Store the minimum expected request size (from GR_REQ) in each
api_handler entry. The dispatch loop now rejects requests whose payload
is smaller than the declared struct size with EMSGSIZE, preventing
handlers from dereferencing truncated payloads.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Reviewed-by: Christophe Fontaine <cfontain@redhat.com>
Reviewed-by: David Marchand <david.marchand@redhat.com>
Wrap event_serializer() in a macro that expands the payload size from
the GR_EVENT-generated constant, removing the need to pass sizeof() or
0 manually at each call site.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Reviewed-by: Christophe Fontaine <cfontain@redhat.com>
Reviewed-by: David Marchand <david.marchand@redhat.com>
Add a build-time generated grout.h that includes all public gr_*.h API
headers. Use it in gr_api_client_impl.h to replace the individual gr_*.h
includes. This prepares for the next commit which overrides
GR_REQ/GR_REQ_STREAM/GR_EVENT macros in gr_api_client_impl.h and needs
all API headers included before the macros are expanded.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Reviewed-by: Christophe Fontaine <cfontain@redhat.com>
Reviewed-by: David Marchand <david.marchand@redhat.com>
Override GR_REQ/GR_REQ_STREAM/GR_EVENT in gr_api_client_impl.h to build
a linked list of all known API messages at program startup via
constructors. This gives clients a runtime registry mapping request
codes to names and expected payload sizes.

Since GR_REQ and friends are guarded by #ifndef, the overrides must be
defined before any API header is included. And since the macros expand
at inclusion time, gr_api_client_impl.h must explicitly include every
module API header so that all messages are registered. The previously
added grout.h umbrella header serves that purpose.

The gr_api_message_name() function replaces the hand-maintained switch
statement in the FRR plugin for converting codes to strings.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Reviewed-by: Christophe Fontaine <cfontain@redhat.com>
Reviewed-by: David Marchand <david.marchand@redhat.com>
Pass the request type through gr_api_client_recv() so it can look up the
expected minimum response size from the exchange registry. Responses
smaller than the declared type are rejected with EMSGSIZE. Stream
responses are exempted when the payload is empty (the stream
terminator).

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Reviewed-by: Christophe Fontaine <cfontain@redhat.com>
Reviewed-by: David Marchand <david.marchand@redhat.com>
@rjarry rjarry force-pushed the api-version-hash branch from cacbe15 to 2b31e2d Compare April 7, 2026 19:25
@rjarry
Copy link
Copy Markdown
Collaborator Author

rjarry commented Apr 7, 2026

I have added back a GR_MSG_TYPE() macro which does what REQUEST_TYPE() and EVENT_TYPE() did, without the duplication. Also added a few assertions that we don't have duplicate request/event type ids and that gr_api_client_impl.h is included first.

Introduce a check based on abidiff that compares the current API headers
against the previous git revision. Both sets of headers are compiled into
dummy binaries with debug info, allowing abidiff to detect struct layout
and type changes.

When breaking changes are found and GR_API_VERSION has not been bumped,
the build fails with an error. The hello handshake now negotiates on
api_version (an integer) instead of comparing the full version string,
so that clients and servers built from different commits remain
compatible as long as the API is unchanged.

The check is wired as a meson custom_target that runs on every build
when abidiff and git are available.

Signed-off-by: Robin Jarry <rjarry@redhat.com>
Reviewed-by: Christophe Fontaine <cfontain@redhat.com>
Reviewed-by: David Marchand <david.marchand@redhat.com>
@rjarry rjarry force-pushed the api-version-hash branch from 2b31e2d to 00d4428 Compare April 7, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants