Skip to content

Commit b708aa5

Browse files
charsyamgregkh
authored andcommitted
ksmbd: harden file lifetime during session teardown
commit a42896b upstream. __close_file_table_ids() is the per-session teardown that closes every fp belonging to a session (or to one tree connect on that session) by walking the session's volatile-id idr. The current loop has three related problems on busy or racing workloads: * Sleeping under ft->lock. The session-teardown skip callback, session_fd_check(), already sleeps in ksmbd_vfs_copy_durable_owner() -> kstrdup(GFP_KERNEL) and down_write(&fp->f_ci->m_lock) (a rw_semaphore). Running the callback inside write_lock(&ft->lock) trips CONFIG_DEBUG_ATOMIC_SLEEP / CONFIG_PROVE_LOCKING on a durable-fd workload. * Refcount accounting blind to f_state. The unconditional atomic_dec_and_test(&fp->refcount) does not distinguish FP_INITED (idr-owned reference still intact) from FP_CLOSED (an earlier ksmbd_close_fd() already consumed the idr-owned reference while leaving fp in the idr because a holder kept refcount non-zero). When the latter races with teardown the same path over-decrements into a holder reference and ksmbd_fd_put() later UAFs that holder. * FP_NEW window. Between __open_id() publishing fp into the session idr and ksmbd_update_fstate(..., FP_INITED) committing the transition at the end of smb2_open(), an fp is in FP_NEW and an intervening teardown that takes a transient reference and unpublishes the volatile id leaves the original idr-owned reference orphaned -- the opener is unaware that fp has been unpublished, returns success to the client, and the fp leaks at refcount = 1. Refactor __close_file_table_ids() to take a transient reference on fp and unpublish fp from the session idr *under ft->lock* before calling skip() outside the lock. A transient ref protects lifetime but not concurrent field mutation, so the idr_remove() is what keeps __ksmbd_lookup_fd() through this session's idr from granting a new ksmbd_fp_get() reference to an fp whose fp->conn / fp->tcon / fp->volatile_id / op->conn / lock_list links are about to be rewritten by session_fd_check(). Durable reconnect is unaffected because it reaches fp through the global durable table (ksmbd_lookup_durable_fd -> global_ft). Decide n_to_drop together with any FP_INITED -> FP_CLOSED transition under ft->lock so teardown and ksmbd_close_fd() never both consume the idr-owned reference. See ksmbd_mark_fp_closed() for the per-state accounting. For the FP_NEW path to be safe, the opener has to learn that fp was unpublished: ksmbd_update_fstate() now returns -ENOENT when an FP_NEW -> FP_INITED transition finds f_state already advanced or the volatile id cleared (both committed by teardown under ft->lock); smb2_open() propagates that as STATUS_OBJECT_NAME_INVALID and drops the original reference via ksmbd_fd_put(). The list removal cannot be left for a deferred final putter because fp->volatile_id has already been cleared and __ksmbd_remove_fd() will intentionally skip both idr_remove() and list_del_init(). Move the m_fp_list unlink in __ksmbd_remove_fd() above the volatile-id check so that an FP_NEW fp that happened to be added to m_fp_list (smb2_open() adds fp->node before ksmbd_update_fstate() runs) is still cleaned up on the deferred putter path; list_del_init() on an empty node is a no-op and remains safe for fps that were never added. Add a defensive guard in session_fd_check() that refuses non-FP_INITED fps so that even if a teardown reaches an FP_NEW fp it falls into the close branch (where the n_to_drop = 1 accounting keeps the opener's reference alive) instead of the durable-preserve branch (which mutates fp->conn / fp->tcon). Validation on a debug kernel additionally built with CONFIG_DEBUG_LIST and CONFIG_DEBUG_OBJECTS_WORK used a same-session two-tcon workload (open/write storm on one tcon, 50 tree disconnects on the other) and reported no list-corruption, work_struct ODEBUG, sleep-in-atomic, lockdep or kmemleak reports. Reverting only the __close_file_table_ids() hunk while keeping a forced-is_reconnectable() harness produced the expected sleep-in-atomic at vfs_cache.c:1095, confirming the ft->lock-out-of-sleepable-skip discipline. KASAN-enabled direct SMB2 coverage with durable handles enabled exercised ksmbd_close_tree_conn_fds(), ksmbd_close_session_fds(), the FP_NEW failure path, tree_conn_fd_check(), and a non-zero session_fd_check() durable-preserve return. This produced no KASAN, DEBUG_LIST, ODEBUG, or WARNING reports. Fixes: f441584 ("cifsd: add file operations") Signed-off-by: DaeMyung Kang <charsyam@gmail.com> Acked-by: Namjae Jeon <linkinjeon@kernel.org> Signed-off-by: Steve French <stfrench@microsoft.com> Signed-off-by: Namjae Jeon <linkinjeon@kernel.org> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
1 parent 45afabe commit b708aa5

3 files changed

Lines changed: 164 additions & 25 deletions

File tree

fs/smb/server/smb2pdu.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3773,8 +3773,10 @@ int smb2_open(struct ksmbd_work *work)
37733773

37743774
err_out2:
37753775
if (!rc) {
3776-
ksmbd_update_fstate(&work->sess->file_table, fp, FP_INITED);
3777-
rc = ksmbd_iov_pin_rsp(work, (void *)rsp, iov_len);
3776+
rc = ksmbd_update_fstate(&work->sess->file_table, fp,
3777+
FP_INITED);
3778+
if (!rc)
3779+
rc = ksmbd_iov_pin_rsp(work, (void *)rsp, iov_len);
37783780
}
37793781
if (rc) {
37803782
if (rc == -EINVAL)

fs/smb/server/vfs_cache.c

Lines changed: 158 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -358,13 +358,13 @@ static void ksmbd_remove_durable_fd(struct ksmbd_file *fp)
358358

359359
static void __ksmbd_remove_fd(struct ksmbd_file_table *ft, struct ksmbd_file *fp)
360360
{
361-
if (!has_file_id(fp->volatile_id))
362-
return;
363-
364361
down_write(&fp->f_ci->m_lock);
365362
list_del_init(&fp->node);
366363
up_write(&fp->f_ci->m_lock);
367364

365+
if (!has_file_id(fp->volatile_id))
366+
return;
367+
368368
write_lock(&ft->lock);
369369
idr_remove(ft->idr, fp->volatile_id);
370370
write_unlock(&ft->lock);
@@ -748,56 +748,188 @@ struct ksmbd_file *ksmbd_open_fd(struct ksmbd_work *work, struct file *filp)
748748
return ERR_PTR(ret);
749749
}
750750

751-
void ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp,
752-
unsigned int state)
751+
/**
752+
* ksmbd_update_fstate() - update an fp state under the file-table lock
753+
* @ft: file table that publishes @fp's volatile id
754+
* @fp: file pointer to update
755+
* @state: new state
756+
*
757+
* Return: 0 on success. The FP_NEW -> FP_INITED transition is special:
758+
* -ENOENT if teardown already unpublished @fp by advancing the state or
759+
* clearing the volatile id. Other state updates preserve the historical
760+
* fire-and-forget behavior.
761+
*/
762+
int ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp,
763+
unsigned int state)
753764
{
765+
int ret;
766+
754767
if (!fp)
755-
return;
768+
return -ENOENT;
756769

757770
write_lock(&ft->lock);
758-
fp->f_state = state;
771+
if (state == FP_INITED &&
772+
(fp->f_state != FP_NEW || !has_file_id(fp->volatile_id))) {
773+
ret = -ENOENT;
774+
} else {
775+
fp->f_state = state;
776+
ret = 0;
777+
}
759778
write_unlock(&ft->lock);
779+
780+
return ret;
781+
}
782+
783+
/*
784+
* ksmbd_mark_fp_closed() - mark fp closed under ft->lock and return how many
785+
* refs the teardown path owns.
786+
*
787+
* FP_INITED has a normal idr-owned reference, so teardown owns both that
788+
* reference and the transient lookup reference. FP_NEW is still owned by the
789+
* in-flight opener/reopener, which will drop the original reference after
790+
* ksmbd_update_fstate(..., FP_INITED) observes the cleared volatile id.
791+
* FP_CLOSED on entry means an earlier ksmbd_close_fd() already consumed the
792+
* idr-owned ref.
793+
*/
794+
static int ksmbd_mark_fp_closed(struct ksmbd_file *fp)
795+
{
796+
if (fp->f_state == FP_INITED) {
797+
set_close_state_blocked_works(fp);
798+
fp->f_state = FP_CLOSED;
799+
return 2;
800+
}
801+
802+
return 1;
760803
}
761804

762805
static int
763806
__close_file_table_ids(struct ksmbd_session *sess,
764807
struct ksmbd_tree_connect *tcon,
765808
bool (*skip)(struct ksmbd_tree_connect *tcon,
766809
struct ksmbd_file *fp,
767-
struct ksmbd_user *user))
810+
struct ksmbd_user *user),
811+
bool skip_preserves_fp)
768812
{
769813
struct ksmbd_file_table *ft = &sess->file_table;
770814
struct ksmbd_file *fp;
771815
unsigned int id = 0;
772816
int num = 0;
773817

774818
while (1) {
819+
int n_to_drop;
820+
775821
write_lock(&ft->lock);
776822
fp = idr_get_next(ft->idr, &id);
777823
if (!fp) {
778824
write_unlock(&ft->lock);
779825
break;
780826
}
781-
782-
if (skip(tcon, fp, sess->user) ||
783-
!atomic_dec_and_test(&fp->refcount)) {
827+
if (!atomic_inc_not_zero(&fp->refcount)) {
784828
id++;
785829
write_unlock(&ft->lock);
786830
continue;
787831
}
788832

789-
set_close_state_blocked_works(fp);
790-
idr_remove(ft->idr, fp->volatile_id);
791-
fp->volatile_id = KSMBD_NO_FID;
792-
write_unlock(&ft->lock);
833+
if (skip_preserves_fp) {
834+
/*
835+
* Session teardown: skip() is session_fd_check(),
836+
* which may sleep and mutates fp->conn / fp->tcon /
837+
* fp->volatile_id when it chooses to preserve fp
838+
* for durable reconnect. Unpublish fp from the
839+
* session idr here, under ft->lock, so that
840+
* __ksmbd_lookup_fd() through this session cannot
841+
* grant a new ksmbd_fp_get() reference to an fp
842+
* whose fields are about to be rewritten outside
843+
* the lock. Durable reconnect still reaches fp via
844+
* global_ft.
845+
*/
846+
idr_remove(ft->idr, id);
847+
fp->volatile_id = KSMBD_NO_FID;
848+
write_unlock(&ft->lock);
793849

850+
if (skip(tcon, fp, sess->user)) {
851+
/*
852+
* session_fd_check() has converted fp to
853+
* durable-preserve state and cleared its
854+
* per-conn fields. fp is already unpublished
855+
* above; the original idr-owned ref keeps it
856+
* alive for the durable scavenger. Drop only
857+
* the transient ref. atomic_dec() is safe --
858+
* atomic_inc_not_zero() succeeded on a
859+
* positive value and we added one more, so
860+
* refcount cannot be zero here.
861+
*/
862+
atomic_dec(&fp->refcount);
863+
id++;
864+
continue;
865+
}
866+
867+
/*
868+
* Keep the close-state decision under the same lock
869+
* observed by ksmbd_update_fstate(), which is how an
870+
* in-flight FP_NEW opener learns that teardown has
871+
* cleared its volatile id.
872+
*/
873+
write_lock(&ft->lock);
874+
n_to_drop = ksmbd_mark_fp_closed(fp);
875+
write_unlock(&ft->lock);
876+
} else {
877+
/*
878+
* Tree teardown: skip() is tree_conn_fd_check(), a
879+
* cheap pointer compare that doesn't sleep and has
880+
* no side effects, so keep the skip decision plus
881+
* the unpublish-and-mark-closed sequence atomic
882+
* under ft->lock. fps belonging to other tree
883+
* connects (skip() == true) stay fully published in
884+
* the session idr with no lock window.
885+
*/
886+
if (skip(tcon, fp, sess->user)) {
887+
atomic_dec(&fp->refcount);
888+
write_unlock(&ft->lock);
889+
id++;
890+
continue;
891+
}
892+
idr_remove(ft->idr, id);
893+
fp->volatile_id = KSMBD_NO_FID;
894+
n_to_drop = ksmbd_mark_fp_closed(fp);
895+
write_unlock(&ft->lock);
896+
}
897+
898+
/*
899+
* fp->volatile_id is already cleared to prevent stale idr
900+
* removal from a deferred final close. Remove fp from
901+
* m_fp_list here because __ksmbd_remove_fd() will skip the
902+
* list unlink when volatile_id is KSMBD_NO_FID.
903+
*/
794904
down_write(&fp->f_ci->m_lock);
795905
list_del_init(&fp->node);
796906
up_write(&fp->f_ci->m_lock);
797907

798-
__ksmbd_close_fd(ft, fp);
799-
800-
num++;
908+
/*
909+
* Drop the references this iteration owns:
910+
*
911+
* n_to_drop == 2: we observed FP_INITED and committed
912+
* the FP_CLOSED transition ourselves, so we own the
913+
* transient (+1) and the still-intact idr-owned ref.
914+
*
915+
* n_to_drop == 1: either a prior ksmbd_close_fd()
916+
* already consumed the idr-owned ref, or fp was still
917+
* FP_NEW and the in-flight opener/reopener must keep
918+
* the original reference until ksmbd_update_fstate()
919+
* observes the cleared volatile id.
920+
*
921+
* If we end up as the final putter, finalize fp and
922+
* account the open_files_count decrement via the caller's
923+
* atomic_sub(num, ...). Otherwise the remaining user's
924+
* ksmbd_fd_put() reaches __put_fd_final(), which does its
925+
* own atomic_dec(&open_files_count), so we must not count
926+
* this fp here -- doing so would double-decrement the
927+
* connection-wide counter.
928+
*/
929+
if (atomic_sub_and_test(n_to_drop, &fp->refcount)) {
930+
__ksmbd_close_fd(NULL, fp);
931+
num++;
932+
}
801933
id++;
802934
}
803935

@@ -1071,6 +1203,9 @@ static bool session_fd_check(struct ksmbd_tree_connect *tcon,
10711203
if (!is_reconnectable(fp))
10721204
return false;
10731205

1206+
if (fp->f_state != FP_INITED)
1207+
return false;
1208+
10741209
if (WARN_ON_ONCE(!fp->conn))
10751210
return false;
10761211

@@ -1122,7 +1257,8 @@ void ksmbd_close_tree_conn_fds(struct ksmbd_work *work)
11221257
{
11231258
int num = __close_file_table_ids(work->sess,
11241259
work->tcon,
1125-
tree_conn_fd_check);
1260+
tree_conn_fd_check,
1261+
false);
11261262

11271263
atomic_sub(num, &work->conn->stats.open_files_count);
11281264
}
@@ -1131,7 +1267,8 @@ void ksmbd_close_session_fds(struct ksmbd_work *work)
11311267
{
11321268
int num = __close_file_table_ids(work->sess,
11331269
work->tcon,
1134-
session_fd_check);
1270+
session_fd_check,
1271+
true);
11351272

11361273
atomic_sub(num, &work->conn->stats.open_files_count);
11371274
}
@@ -1271,7 +1408,7 @@ void ksmbd_destroy_file_table(struct ksmbd_session *sess)
12711408
if (!ft->idr)
12721409
return;
12731410

1274-
__close_file_table_ids(sess, NULL, session_fd_check);
1411+
__close_file_table_ids(sess, NULL, session_fd_check, true);
12751412
idr_destroy(ft->idr);
12761413
kfree(ft->idr);
12771414
ft->idr = NULL;

fs/smb/server/vfs_cache.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ int ksmbd_close_inode_fds(struct ksmbd_work *work, struct inode *inode);
172172
int ksmbd_init_global_file_table(void);
173173
void ksmbd_free_global_file_table(void);
174174
void ksmbd_set_fd_limit(unsigned long limit);
175-
void ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp,
176-
unsigned int state);
175+
int ksmbd_update_fstate(struct ksmbd_file_table *ft, struct ksmbd_file *fp,
176+
unsigned int state);
177177
bool ksmbd_vfs_compare_durable_owner(struct ksmbd_file *fp,
178178
struct ksmbd_user *user);
179179

0 commit comments

Comments
 (0)