From: sashiko-bot@kernel.org
To: "Jiri Pirko" <jiri@resnulli.us>
Cc: kvm@vger.kernel.org
Subject: Re: [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library
Date: Fri, 13 Nov 2026 16:28:54 +0000 [thread overview]
Message-ID: <sashiko-outbox-213194@kernel.org> (raw)
In-Reply-To: <20261113155515.2510194-15-jiri@resnulli.us>
Thank you for your contribution! Sashiko AI review found 7 potential
issues to consider:
- [Medium] tools/ctlv: missing ctlv_hdr_is_record() check in record
walkers and getters
- [Low] tools/ctlv: ctlv_put_str() bypasses sticky msg->err on length
overflow
- [Low] tools/ctlv: memory leak in ctlv_msg_new_max() when
ctlv_record_start() fails
- [Low] tools/ctlv: unpopulated ctlv_error in ctlv_spec_check() and
ctlv_query_sized()
- [Low] tools/ctlv: stale bound and truncation of reason in
ctlv_error_parse()
- [Low] tools/ctlv: missing progress check on final fragment in
ctlv_query_guard_next()
- [Low] tools/ctlv: missing -ESTALE retry loop in ctlv_ops_get()
--
commit 128e9e8121ea589172c7fc1358fcc6d31f685bab
Author: Jiri Pirko <jiri@resnulli.us>
tools/ctlv: Add the userspace library
This commit adds the libctlv userspace library for transport, message
building and parsing, device discovery, and schema checking, along with
the userspace code generator in ctlvgen.
> diff --git a/tools/ctlv/libctlv/introspection.c b/tools/ctlv/libctlv/introspection.c
> new file mode 100644
> index 0000000000000..1bd2662e58317
> --- /dev/null
> +++ b/tools/ctlv/libctlv/introspection.c
[ ... ]
> +int ctlv_ops_get(struct ctlv_dev *dev, struct ctlv_ops **opsp,
> + struct ctlv_error *error)
> +{
> + struct ctlv_reply *reply = NULL;
> + int ret;
> +
> + /* Sized, or the dump kept for op lengths would ask for itself. */
> + ret = ctlv_query_sized(dev, CTLV_OP_OP_GET, NULL, 0, &reply, error);
> + if (ret)
> + return ret;
[Severity: Low]
Should ctlv_ops_get() retry up to QUERY_RESTARTS times on -ESTALE like
ctlv_query() does?
Because ctlv_ops_get() calls ctlv_query_sized() directly to avoid
recursing into ctlv_dev_sizes(), a dynamic op state change between
fragments of an op-get dump causes ctlv_ops_get(), ctlv_dev_sizes(), and
ctlv_spec_check() to fail with -ESTALE instead of restarting the dump.
[ ... ]
> diff --git a/tools/ctlv/libctlv/message.c b/tools/ctlv/libctlv/message.c
> new file mode 100644
> index 0000000000000..ab4464fb8af2d
> --- /dev/null
> +++ b/tools/ctlv/libctlv/message.c
[ ... ]
> +int ctlv_msg_new_max(__u32 capacity, __u32 max, struct ctlv_msg **msgp)
> +{
> + struct ctlv_msg *msg;
> +
> + if (!capacity || capacity > max)
> + return -EINVAL;
> + msg = calloc(1, sizeof(*msg));
> + if (!msg)
> + return -ENOMEM;
> + /* Separate from the handle, so growing the buffer does not move it. */
> + msg->buf = malloc(capacity);
> + if (!msg->buf) {
> + free(msg);
> + return -ENOMEM;
> + }
> + msg->capacity = capacity;
> + msg->max = max;
> + *msgp = msg;
> + return ctlv_record_start(msg, CTLV_TYPE_ROOT);
> +}
[Severity: Low]
Does ctlv_msg_new_max() leak msg and msg->buf if ctlv_record_start()
fails?
When capacity is smaller than sizeof(struct ctlv_hdr) (16 bytes),
ctlv_record_start() has to grow msg->buf; if max < 16 or realloc() fails,
ctlv_msg_new_max() returns a negative errno with *msgp pointing to the
allocated struct, which callers such as reply_new() do not free on error.
[ ... ]
> +/* The terminator is part of the payload, and a declared length excludes it. */
> +int ctlv_put_str(struct ctlv_msg *msg, __u64 type, const char *value)
> +{
> + size_t len = strlen(value) + 1;
> +
> + if (len > UINT32_MAX)
> + return -EMSGSIZE;
> + return ctlv_msg_put(msg, type, 0, value, len);
> +}
[Severity: Low]
Does the length overflow path in ctlv_put_str() bypass msg->err?
If len > UINT32_MAX, returning -EMSGSIZE directly neither preserves an
earlier msg->err nor records -EMSGSIZE in msg->err, so a caller checking
ctlv_msg_err() or ctlv_msg_done() at the end will miss the failure and
send an incomplete message.
[ ... ]
> +const struct ctlv_hdr *ctlv_hdr_first(const struct ctlv_hdr *record)
> +{
> + if (record->len <= sizeof(*record))
> + return NULL;
> + /* A record too short to hold the header it announces holds no child. */
> + if (record->len - sizeof(*record) < sizeof(*record))
> + return NULL;
> + return record + 1;
> +}
> +
> +/*
> + * The next child is a step of this one's aligned length. Both the step and the
> + * header it lands on have to stay inside the record.
> + */
> +const struct ctlv_hdr *ctlv_hdr_next(const struct ctlv_hdr *record,
> + const struct ctlv_hdr *child)
> +{
> + const char *end = (const char *)record + record->len;
> + const char *next = (const char *)child;
> + __u32 step;
> +
> + if (aligned(child->len, &step))
> + return NULL;
> + next += step;
> + if (next < (const char *)child || next > end)
> + return NULL;
> + if ((size_t)(end - next) < sizeof(*child))
> + return NULL;
> + return (const struct ctlv_hdr *)next;
> +}
[Severity: Medium]
Should ctlv_hdr_first(), ctlv_hdr_next(), and the generated record
getters verify ctlv_hdr_is_record()?
check_record() only validates nested TLV headers when CTLV_HDR_F_RECORD is
set, and the scalar getters reject records via ctlv_hdr_is_record(hdr),
but ctlv_hdr_first() and ctlv_hdr_next() walk any TLV whose len is at
least 32 bytes. Generated record getters in
tools/ctlv/ctlvgen/lib/gen_user.py:_getter() also return ctlv_hdr_find()
without checking ctlv_hdr_is_record().
If a non-record TLV is passed to ctlv_hdr_first() or returned by a record
getter, its unvalidated payload bytes are interpreted as struct ctlv_hdr,
which can spin forever in ctlv_for_each() when the first 4 payload bytes
are zero (step == 0) or read out of bounds when a child's len is smaller
than sizeof(struct ctlv_hdr).
[ ... ]
> diff --git a/tools/ctlv/libctlv/spec.c b/tools/ctlv/libctlv/spec.c
> new file mode 100644
> index 0000000000000..194966e29e2d8
> --- /dev/null
> +++ b/tools/ctlv/libctlv/spec.c
[ ... ]
> +int ctlv_spec_check(struct ctlv_dev *dev, const struct ctlv_family_spec *spec,
> + char *diff, size_t len, struct ctlv_error *error)
> +{
> + struct ctlv_ops *ops;
> + unsigned int i;
> + int ret;
> +
> + if (diff && len)
> + *diff = '\0';
> + ret = chain_check(dev, spec, diff, len);
> + if (ret)
> + return ret;
> + ret = ctlv_ops_get(dev, &ops, error);
> + if (ret)
> + return ret;
[Severity: Low]
Does ctlv_spec_check() leave *error uninitialized or cleared with
error->err == 0 on failure?
If chain_check() fails, *error is never initialized, and if a later check
fails with -ENOMSG (such as an unknown op, info_check(),
ctlv_schema_equal() in schema_check(), or required_ops_check()), error->err
remains 0 from ctlv_ops_get(). Similarly, in
tools/ctlv/libctlv/transport.c:ctlv_query_sized(), if reply_new() fails it
jumps to out_free_buf instead of out_set_err, leaving error->err as 0.
> diff --git a/tools/ctlv/libctlv/transport.c b/tools/ctlv/libctlv/transport.c
> new file mode 100644
> index 0000000000000..647bab085c9e1
> --- /dev/null
> +++ b/tools/ctlv/libctlv/transport.c
[ ... ]
> + hdr = ctlv_hdr_find(root, CTLV_T_ERROR_INFO_REASON);
> + if (hdr && !ctlv_get_enum(hdr, 1,
> + CTLV_VALIDATION_REASON_STALE_GENERATION,
> + &value, NULL)) {
> + error->reason = value;
> + error->present |= CTLV_ERROR_HAS_REASON;
> + }
[Severity: Low]
Does ctlv_error_parse() ignore the enum bounds and truncate value into
error->reason?
ctlv_get_enum() only checks min and max when the known pointer is
non-NULL, so passing NULL accepts any 64-bit value (including 0 or values
above UINT32_MAX) and truncates it into the 32-bit error->reason field.
In addition, the upper bound passed here is
CTLV_VALIDATION_REASON_STALE_GENERATION (11) rather than
CTLV_VALIDATION_REASON_NONCONTIGUOUS_TYPE (12).
[ ... ]
> +int ctlv_query_guard_next(struct ctlv_query_guard *guard, __u64 cursor,
> + bool progress)
> +{
> + unsigned int slot;
> + int ret;
> +
> + if (!cursor)
> + return 0;
> + if (!progress)
> + return -EPROTO;
[Severity: Low]
Does returning early on !cursor skip the progress check for the final
fragment of a multi-fragment query?
When guard->cursors_count > 0 and the final continuation fragment ends
with cursor == 0 without adding any complete TLV (progress == false),
ctlv_query_guard_next() returns 0 instead of rejecting it with -EPROTO.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113155515.2510194-1-jiri@resnulli.us?part=14
next prev parent reply other threads:[~2026-11-13 16:28 UTC|newest]
Thread overview: 24+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-13 15:54 [PATCH RFC v2 00/15] ctlv: Add a schema-driven TLV uAPI for character devices Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 01/15] ctlv: Add the schema language and generator Jiri Pirko
2026-11-13 16:23 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 02/15] ctlv: Add the UAPI message format and family API Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 03/15] ctlv: Add the root family specification and generated files Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 04/15] Documentation: ctlv: Add userspace ABI Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 05/15] ctlv: Prepare message input handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 06/15] ctlv: Prepare blob descriptor validation Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 07/15] ctlv: Prepare schema validation Jiri Pirko
2026-11-13 16:22 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 08/15] ctlv: Prepare message output handling Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 09/15] ctlv: Prepare VA blob handling Jiri Pirko
2026-11-13 16:13 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 10/15] ctlv: Prepare runtime introspection Jiri Pirko
2026-11-13 16:24 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 11/15] ctlv: Add operation dispatch Jiri Pirko
2026-11-13 16:29 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 12/15] ctlv: Add character device nodes Jiri Pirko
2026-11-13 15:55 ` [PATCH RFC v2 13/15] Documentation: ctlv: Describe how to write a family Jiri Pirko
2026-11-13 16:13 ` sashiko-bot
2026-11-13 15:55 ` [PATCH RFC v2 14/15] tools/ctlv: Add the userspace library Jiri Pirko
2026-11-13 16:28 ` sashiko-bot [this message]
2026-11-13 15:55 ` [PATCH RFC v2 15/15] tools/ctlv: Add ctlvctl Jiri Pirko
2026-11-13 16:19 ` sashiko-bot
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=sashiko-outbox-213194@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=jiri@resnulli.us \
--cc=kvm@vger.kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
/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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.