From: Viacheslav Dubeyko <Slava.Dubeyko@ibm.com>
To: "idryomov@gmail.com" <idryomov@gmail.com>,
Alex Markuze <amarkuze@redhat.com>,
"michael.bommarito@gmail.com" <michael.bommarito@gmail.com>,
"slava@dubeyko.com" <slava@dubeyko.com>
Cc: "ceph-devel@vger.kernel.org" <ceph-devel@vger.kernel.org>,
"linux-kernel@vger.kernel.org" <linux-kernel@vger.kernel.org>
Subject: Re: [PATCH v3 4/4] ceph: cap delegated inode count in ceph_parse_deleg_inos()
Date: Tue, 7 Jul 2026 18:30:50 +0000 [thread overview]
Message-ID: <47199f35e992c25f4c85fe2b6e5b194197856fba.camel@ibm.com> (raw)
In-Reply-To: <6a37b446fa15089850b123d47484c3663036b6cf.1783447231.git.michael.bommarito@gmail.com>
Hi Michael,
Sorry for delay with the review.
On Tue, 2026-07-07 at 14:06 -0400, Michael Bommarito wrote:
> ceph_parse_deleg_inos() decodes interval sets of delegated inode numbers
> from an MDS create-with-delegation reply. For each set it reads a 64-bit
> start and a 64-bit len with ceph_decode_64_safe(), which only validates
> that the eight bytes are present in the message, not the value, and then
> loops over len while inserting entries into s_delegated_inos.
>
> len is fully attacker controlled. A malicious or compromised MDS can send
> one huge interval, many intervals in one reply, duplicate intervals, or
> repeated replies that accumulate delegated inodes on the same session.
> The original code bounded none of these and could spin the insert loop or
> grow the xarray without limit.
>
> Bound both dimensions with a single enforcement point. Track the number
> of delegated inodes held by each MDS session in an atomic counter and
> grow it only in ceph_insert_deleg_ino(), which uses atomic_add_unless()
> to refuse to push the count past CEPH_MAX_DELEG_INOS. Because that helper
> is the only place the counter grows, the per-session population can never
> exceed the cap, so no separate per-session pre-check is needed. The
> counter is decremented when async create consumes a delegated inode or
> when an insert fails, incremented when a delegated inode is restored,
> initialized with the session xarray, and reset when reconnect destroys
> the xarray.
>
> A per-session cap alone still lets one reply spin the insert loop on
> duplicate ranges without growing the counter, so also cap the aggregate
> interval length accepted from a single reply. Together these bound both
> the loop trip count per reply and the xarray population across replies.
>
> The cap is a fixed, client-chosen constant rather than a value derived
> from the MDS. mds_client_prealloc_inos is a userspace MDS configuration
> option; it is never sent to the kernel client on the wire, and a
> server-supplied bound could not be trusted for a defensive limit in any
> case. The constant is set well above that option's documented default of
> 1000 (a generous multiple), so legitimate refill behavior is unaffected
> while the CPU and xarray memory a malformed delegation stream can consume
> stays bounded.
>
> Impact: a malicious or compromised Ceph MDS can no longer make a client
> spin through an unbounded delegated-inode interval or grow one session's
> delegated-inode xarray without limit.
>
> Fixes: d48464878708 ("ceph: decode interval_sets for delegated inos")
> Cc: stable@vger.kernel.org
> Suggested-by: Viacheslav Dubeyko <Slava.Dubeyko@ibm.com>
> Assisted-by: Claude:claude-opus-4-8
> Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
> ---
> fs/ceph/mds_client.c | 59 +++++++++++++++++++++++++++++++++++++++-----
> fs/ceph/mds_client.h | 1 +
> fs/ceph/super.h | 9 +++++++
> 3 files changed, 63 insertions(+), 6 deletions(-)
>
> diff --git a/fs/ceph/mds_client.c b/fs/ceph/mds_client.c
> index 94f76e9fe4ead..7b00b83371f7e 100644
> --- a/fs/ceph/mds_client.c
> +++ b/fs/ceph/mds_client.c
> @@ -618,10 +618,36 @@ static int parse_reply_info_filelock(void **p, void *end,
>
> #define DELEGATED_INO_AVAILABLE xa_mk_value(1)
>
> +static int ceph_insert_deleg_ino(struct ceph_mds_session *s, u64 ino)
> +{
> + struct ceph_client *cl = s->s_mdsc->fsc->client;
> + int err;
> +
> + /*
> + * Cap how many delegated inodes a single session may hold. This is
> + * the only place that grows the count, so atomic_add_unless() bounds
> + * it at exactly CEPH_MAX_DELEG_INOS; s_num_deleg_inos can never exceed
> + * that.
> + */
> + if (!atomic_add_unless(&s->s_num_deleg_inos, 1, CEPH_MAX_DELEG_INOS)) {
> + pr_warn_ratelimited_client(cl,
> + "MDS session already holds %d delegated inodes\n",
> + CEPH_MAX_DELEG_INOS);
> + return -EOVERFLOW;
> + }
> +
> + err = xa_insert(&s->s_delegated_inos, ino, DELEGATED_INO_AVAILABLE,
> + GFP_KERNEL);
> + if (err)
> + atomic_dec(&s->s_num_deleg_inos);
> + return err;
> +}
> +
> static int ceph_parse_deleg_inos(void **p, void *end,
> struct ceph_mds_session *s)
> {
> struct ceph_client *cl = s->s_mdsc->fsc->client;
> + u64 msg_deleg_inos = 0;
> u32 sets;
>
> ceph_decode_32_safe(p, end, sets, bad);
> @@ -639,16 +665,34 @@ static int ceph_parse_deleg_inos(void **p, void *end,
> start, len);
> continue;
> }
> +
> + /*
> + * Bound the number of inodes one reply may delegate.
> + * ceph_insert_deleg_ino() separately caps the per-session
> + * population, so this only has to stop one reply from spinning
> + * the insert loop under an attacker-controlled len.
> + */
> + if (len > (u64)CEPH_MAX_DELEG_INOS ||
> + msg_deleg_inos > (u64)CEPH_MAX_DELEG_INOS - len) {
> + pr_warn_ratelimited_client(cl,
> + "MDS reply delegates too many inodes (have %llu, +%llu, max %d)\n",
> + msg_deleg_inos, len, CEPH_MAX_DELEG_INOS);
> + return -EIO;
> + }
> + msg_deleg_inos += len;
> +
> while (len--) {
> - int err = xa_insert(&s->s_delegated_inos, start++,
> - DELEGATED_INO_AVAILABLE,
> - GFP_KERNEL);
> + int err = ceph_insert_deleg_ino(s, start++);
> +
> if (!err) {
> doutc(cl, "added delegated inode 0x%llx\n", start - 1);
> } else if (err == -EBUSY) {
> pr_warn_client(cl,
> "MDS delegated inode 0x%llx more than once.\n",
> start - 1);
> + } else if (err == -EOVERFLOW) {
> + /* ceph_insert_deleg_ino() already warned. */
> + return -EIO;
> } else {
> return err;
> }
> @@ -666,16 +710,17 @@ u64 ceph_get_deleg_ino(struct ceph_mds_session *s)
>
> xa_for_each(&s->s_delegated_inos, ino, val) {
> val = xa_erase(&s->s_delegated_inos, ino);
> - if (val == DELEGATED_INO_AVAILABLE)
> + if (val == DELEGATED_INO_AVAILABLE) {
> + atomic_dec(&s->s_num_deleg_inos);
> return ino;
> + }
> }
> return 0;
> }
>
> int ceph_restore_deleg_ino(struct ceph_mds_session *s, u64 ino)
> {
> - return xa_insert(&s->s_delegated_inos, ino, DELEGATED_INO_AVAILABLE,
> - GFP_KERNEL);
> + return ceph_insert_deleg_ino(s, ino);
> }
> #else /* BITS_PER_LONG == 64 */
> /*
> @@ -1062,6 +1107,7 @@ static struct ceph_mds_session *register_session(struct ceph_mds_client *mdsc,
> INIT_LIST_HEAD(&s->s_waiting);
> INIT_LIST_HEAD(&s->s_unsafe);
> xa_init(&s->s_delegated_inos);
> + atomic_set(&s->s_num_deleg_inos, 0);
> INIT_LIST_HEAD(&s->s_cap_releases);
> INIT_WORK(&s->s_cap_release_work, ceph_cap_release_work);
>
> @@ -5103,6 +5149,7 @@ static int send_mds_reconnect(struct ceph_mds_client *mdsc,
>
> /* Serialized by s_mutex against concurrent ceph_get_deleg_ino(). */
> xa_destroy(&session->s_delegated_inos);
> + atomic_set(&session->s_num_deleg_inos, 0);
> if (session->s_state == CEPH_MDS_SESSION_CLOSED ||
> session->s_state == CEPH_MDS_SESSION_REJECTED) {
> pr_info_client(cl, "mds%d skipping reconnect, session %s\n",
> diff --git a/fs/ceph/mds_client.h b/fs/ceph/mds_client.h
> index 731d6ad04956d..d1a10c4bd4d2d 100644
> --- a/fs/ceph/mds_client.h
> +++ b/fs/ceph/mds_client.h
> @@ -299,6 +299,7 @@ struct ceph_mds_session {
> struct list_head s_waiting; /* waiting requests */
> struct list_head s_unsafe; /* unsafe requests */
> struct xarray s_delegated_inos;
> + atomic_t s_num_deleg_inos;
> };
>
> /*
> diff --git a/fs/ceph/super.h b/fs/ceph/super.h
> index 3737ea7ed88b3..fb2b0a4b7bb3e 100644
> --- a/fs/ceph/super.h
> +++ b/fs/ceph/super.h
> @@ -642,6 +642,15 @@ static inline int ceph_ino_compare(struct inode *inode, void *data)
> #define CEPH_MDS_INO_LOG_OFFSET (2 * CEPH_MAX_MDS)
> #define CEPH_INO_SYSTEM_BASE ((6*CEPH_MAX_MDS) + (CEPH_MAX_MDS * CEPH_NUM_STRAY))
>
> +/*
> + * Upper bound on the number of delegated inodes a single MDS session may
> + * hold. The MDS normally hands out a small preallocation window (the
> + * userspace mds_client_prealloc_inos option defaults to 1000) and refills
> + * it as the client consumes entries. This leaves generous headroom while
> + * bounding the CPU and memory a malformed delegation interval can consume.
> + */
> +#define CEPH_MAX_DELEG_INOS 8192
> +
> static inline bool ceph_vino_is_reserved(const struct ceph_vino vino)
> {
> if (vino.ino >= CEPH_INO_SYSTEM_BASE ||
Looks good.
Reviewed-by: Viacheslav Dubeyko <Slava.Dubeyko@ibm.com>
Thanks,
Slava.
prev parent reply other threads:[~2026-07-07 18:30 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-07 18:05 [PATCH v3 0/4] ceph: bound untrusted MDS and monitor reply decoders Michael Bommarito
2026-07-07 18:05 ` [PATCH v3 1/4] ceph: bound xattr value length in __build_xattrs() Michael Bommarito
2026-07-07 18:05 ` [PATCH v3 2/4] ceph: bound MDSCapAuth path and fs_name decode in handle_session() Michael Bommarito
2026-07-07 18:05 ` [PATCH v3 3/4] ceph: bound num_export_targets array for mds info v2/v3 Michael Bommarito
2026-07-07 18:06 ` [PATCH v3 4/4] ceph: cap delegated inode count in ceph_parse_deleg_inos() Michael Bommarito
2026-07-07 18:30 ` Viacheslav Dubeyko [this message]
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=47199f35e992c25f4c85fe2b6e5b194197856fba.camel@ibm.com \
--to=slava.dubeyko@ibm.com \
--cc=amarkuze@redhat.com \
--cc=ceph-devel@vger.kernel.org \
--cc=idryomov@gmail.com \
--cc=linux-kernel@vger.kernel.org \
--cc=michael.bommarito@gmail.com \
--cc=slava@dubeyko.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