Commit 20d72b00ca81 ("netfs: Fix the request's work item to not require a ref") modified netfs_alloc_request() to initialize the reference counter to 2 instead of 1. The rationale was that the requet's "work" would release the second reference after completion (via netfs_{read,write}_collection_worker()). That works most of the time if all goes well. However, it leaks this additional reference if the request is released before the I/O operation has been submitted: the error code path only decrements the reference counter once and the work item will never be queued because there will never be a completion. This has caused outages of our whole server cluster today because tasks were blocked in netfs_wait_for_outstanding_io(), leading to deadlocks in Ceph (another bug that I will address soon in another patch). This was caused by a netfs_pgpriv2_begin_copy_to_cache() call which failed in fscache_begin_write_operation(). The leaked netfs_io_request was never completed, leaving `netfs_inode.io_count` with a positive value forever. All of this is super-fragile code. Finding out which code paths will lead to an eventual completion and which do not is hard to see: - Some functions like netfs_create_write_req() allocate a request, but will never submit any I/O. - netfs_unbuffered_read_iter_locked() calls netfs_unbuffered_read() and then netfs_put_request(); however, netfs_unbuffered_read() can also fail early before submitting the I/O request, therefore another netfs_put_request() call must be added there. A rule of thumb is that functions that return a `netfs_io_request` do not submit I/O, and all of their callers must be checked. For my taste, the whole netfs code needs an overhaul to make reference counting easier to understand and less fragile & obscure. But to fix this bug here and now and produce a patch that is adequate for a stable backport, I tried a minimal approach that quickly frees the request object upon early failure. I decided against adding a second netfs_put_request() each time because that would cause code duplication which obscures the code further. Instead, I added the function netfs_put_failed_request() which frees such a failed request synchronously under the assumption that the reference count is exactly 2 (as initially set by netfs_alloc_request() and never touched), verified by a WARN_ON_ONCE(). It then deinitializes the request object (without going through the "cleanup_work" indirection) and frees the allocation (with RCU protection to protect against concurrent access by netfs_requests_seq_start()). All code paths that fail early have been changed to call netfs_put_failed_request() instead of netfs_put_request(). Additionally, I have added a netfs_put_request() call to netfs_unbuffered_read() as explained above because the netfs_put_failed_request() approach does not work there.
Unified · Split
| @@ -369,7 +369,7 @@ void netfs_readahead(struct readahead_control *ractl) · 368 unmodified lines | ||
| 369 | 369 | return netfs_put_request(rreq, netfs_rreq_trace_put_return); |
| 370 | 370 | |
| 371 | 371 | cleanup_free: |
| 372 | − return netfs_put_request(rreq, netfs_rreq_trace_put_failed); | |
| 372 | + return netfs_put_failed_request(rreq); | |
| 373 | 373 | } |
| 374 | 374 | EXPORT_SYMBOL(netfs_readahead); |
| 375 | 375 | |
| @@ -472,7 +472,7 @@ static int netfs_read_gaps(struct file *file, struct folio *folio) · 96 unmodified lines | ||
| 472 | 472 | return ret < 0 ? ret : 0; |
| 473 | 473 | |
| 474 | 474 | discard: |
| 475 | − netfs_put_request(rreq, netfs_rreq_trace_put_discard); | |
| 475 | + netfs_put_failed_request(rreq); | |
| 476 | 476 | alloc_error: |
| 477 | 477 | folio_unlock(folio); |
| 478 | 478 | return ret; |
| @@ -532,7 +532,7 @@ int netfs_read_folio(struct file *file, struct folio *folio) · 53 unmodified lines | ||
| 532 | 532 | return ret < 0 ? ret : 0; |
| 533 | 533 | |
| 534 | 534 | discard: |
| 535 | − netfs_put_request(rreq, netfs_rreq_trace_put_discard); | |
| 535 | + netfs_put_failed_request(rreq); | |
| 536 | 536 | alloc_error: |
| 537 | 537 | folio_unlock(folio); |
| 538 | 538 | return ret; |
| @@ -699,7 +699,7 @@ have_folio_no_wait: · 160 unmodified lines | ||
| 699 | 699 | return 0; |
| 700 | 700 | |
| 701 | 701 | error_put: |
| 702 | − netfs_put_request(rreq, netfs_rreq_trace_put_failed); | |
| 702 | + netfs_put_failed_request(rreq); | |
| 703 | 703 | error: |
| 704 | 704 | if (folio) { |
| 705 | 705 | folio_unlock(folio); |
| @@ -754,7 +754,7 @@ int netfs_prefetch_for_write(struct file *file, struct folio *folio, · 48 unmodified lines | ||
| 754 | 754 | return ret < 0 ? ret : 0; |
| 755 | 755 | |
| 756 | 756 | error_put: |
| 757 | − netfs_put_request(rreq, netfs_rreq_trace_put_discard); | |
| 757 | + netfs_put_failed_request(rreq); | |
| 758 | 758 | error: |
| 759 | 759 | _leave(" = %d", ret); |
| 760 | 760 | return ret; |
| @@ -131,6 +131,7 @@ static ssize_t netfs_unbuffered_read(struct netfs_io_request *rreq, bool sync) · 130 unmodified lines | ||
| 131 | 131 | |
| 132 | 132 | if (rreq->len == 0) { |
| 133 | 133 | pr_err("Zero-sized read [R=%x]\n", rreq->debug_id); |
| 134 | + netfs_put_request(rreq, netfs_rreq_trace_put_discard); | |
| 134 | 135 | return -EIO; |
| 135 | 136 | } |
| 136 | 137 | |
| @@ -205,7 +206,7 @@ ssize_t netfs_unbuffered_read_iter_locked(struct kiocb *iocb, struct iov_iter *i · 68 unmodified lines | ||
| 205 | 206 | if (user_backed_iter(iter)) { |
| 206 | 207 | ret = netfs_extract_user_iter(iter, rreq->len, &rreq->buffer.iter, 0); |
| 207 | 208 | if (ret < 0) |
| 208 | − goto out; | |
| 209 | + goto error_put; | |
| 209 | 210 | rreq->direct_bv = (struct bio_vec *)rreq->buffer.iter.bvec; |
| 210 | 211 | rreq->direct_bv_count = ret; |
| 211 | 212 | rreq->direct_bv_unpin = iov_iter_extract_will_pin(iter); |
| @@ -238,6 +239,10 @@ out: · 26 unmodified lines | ||
| 238 | 239 | if (ret > 0) |
| 239 | 240 | orig_count -= ret; |
| 240 | 241 | return ret; |
| 242 | + | |
| 243 | +error_put: | |
| 244 | + netfs_put_failed_request(rreq); | |
| 245 | + return ret; | |
| 241 | 246 | } |
| 242 | 247 | EXPORT_SYMBOL(netfs_unbuffered_read_iter_locked); |
| 243 | 248 | |
| @@ -57,7 +57,7 @@ ssize_t netfs_unbuffered_write_iter_locked(struct kiocb *iocb, struct iov_iter * · 56 unmodified lines | ||
| 57 | 57 | n = netfs_extract_user_iter(iter, len, &wreq->buffer.iter, 0); |
| 58 | 58 | if (n < 0) { |
| 59 | 59 | ret = n; |
| 60 | − goto out; | |
| 60 | + goto error_put; | |
| 61 | 61 | } |
| 62 | 62 | wreq->direct_bv = (struct bio_vec *)wreq->buffer.iter.bvec; |
| 63 | 63 | wreq->direct_bv_count = n; |
| @@ -101,6 +101,10 @@ ssize_t netfs_unbuffered_write_iter_locked(struct kiocb *iocb, struct iov_iter * · 37 unmodified lines | ||
| 101 | 101 | out: |
| 102 | 102 | netfs_put_request(wreq, netfs_rreq_trace_put_return); |
| 103 | 103 | return ret; |
| 104 | + | |
| 105 | +error_put: | |
| 106 | + netfs_put_failed_request(wreq); | |
| 107 | + return ret; | |
| 104 | 108 | } |
| 105 | 109 | EXPORT_SYMBOL(netfs_unbuffered_write_iter_locked); |
| 106 | 110 | |
| @@ -87,6 +87,7 @@ struct netfs_io_request *netfs_alloc_request(struct address_space *mapping, · 86 unmodified lines | ||
| 87 | 87 | void netfs_get_request(struct netfs_io_request *rreq, enum netfs_rreq_ref_trace what); |
| 88 | 88 | void netfs_clear_subrequests(struct netfs_io_request *rreq); |
| 89 | 89 | void netfs_put_request(struct netfs_io_request *rreq, enum netfs_rreq_ref_trace what); |
| 90 | +void netfs_put_failed_request(struct netfs_io_request *rreq); | |
| 90 | 91 | struct netfs_io_subrequest *netfs_alloc_subrequest(struct netfs_io_request *rreq); |
| 91 | 92 | |
| 92 | 93 | static inline void netfs_see_request(struct netfs_io_request *rreq, |
| @@ -116,10 +116,8 @@ static void netfs_free_request_rcu(struct rcu_head *rcu) · 115 unmodified lines | ||
| 116 | 116 | netfs_stat_d(&netfs_n_rh_rreq); |
| 117 | 117 | } |
| 118 | 118 | |
| 119 | −static void netfs_free_request(struct work_struct *work) | |
| 119 | +static void netfs_deinit_request(struct netfs_io_request *rreq) | |
| 120 | 120 | { |
| 121 | − struct netfs_io_request *rreq = | |
| 122 | − container_of(work, struct netfs_io_request, cleanup_work); | |
| 123 | 121 | struct netfs_inode *ictx = netfs_inode(rreq->inode); |
| 124 | 122 | unsigned int i; |
| 125 | 123 | |
| @@ -149,6 +147,14 @@ static void netfs_free_request(struct work_struct *work) · 23 unmodified lines | ||
| 149 | 147 | |
| 150 | 148 | if (atomic_dec_and_test(&ictx->io_count)) |
| 151 | 149 | wake_up_var(&ictx->io_count); |
| 150 | +} | |
| 151 | + | |
| 152 | +static void netfs_free_request(struct work_struct *work) | |
| 153 | +{ | |
| 154 | + struct netfs_io_request *rreq = | |
| 155 | + container_of(work, struct netfs_io_request, cleanup_work); | |
| 156 | + | |
| 157 | + netfs_deinit_request(rreq); | |
| 152 | 158 | call_rcu(&rreq->rcu, netfs_free_request_rcu); |
| 153 | 159 | } |
| 154 | 160 | |
| @@ -167,6 +173,24 @@ void netfs_put_request(struct netfs_io_request *rreq, enum netfs_rreq_ref_trace · 12 unmodified lines | ||
| 167 | 173 | } |
| 168 | 174 | } |
| 169 | 175 | |
| 176 | +/* | |
| 177 | + * Free a request (synchronously) that was just allocated but has | |
| 178 | + * failed before it could be submitted. | |
| 179 | + */ | |
| 180 | +void netfs_put_failed_request(struct netfs_io_request *rreq) | |
| 181 | +{ | |
| 182 | + int r = refcount_read(&rreq->ref); | |
| 183 | + | |
| 184 | + /* new requests have two references (see | |
| 185 | + * netfs_alloc_request(), and this function is only allowed on | |
| 186 | + * new request objects | |
| 187 | + */ | |
| 188 | + WARN_ON_ONCE(r != 2); | |
| 189 | + | |
| 190 | + trace_netfs_rreq_ref(rreq->debug_id, r, netfs_rreq_trace_put_failed); | |
| 191 | + netfs_free_request(&rreq->cleanup_work); | |
| 192 | +} | |
| 193 | + | |
| 170 | 194 | /* |
| 171 | 195 | * Allocate and partially initialise an I/O request structure. |
| 172 | 196 | */ |
| @@ -118,7 +118,7 @@ static struct netfs_io_request *netfs_pgpriv2_begin_copy_to_cache( · 117 unmodified lines | ||
| 118 | 118 | return creq; |
| 119 | 119 | |
| 120 | 120 | cancel_put: |
| 121 | − netfs_put_request(creq, netfs_rreq_trace_put_return); | |
| 121 | + netfs_put_failed_request(creq); | |
| 122 | 122 | cancel: |
| 123 | 123 | rreq->copy_to_cache = ERR_PTR(-ENOBUFS); |
| 124 | 124 | clear_bit(NETFS_RREQ_FOLIO_COPY_TO_CACHE, &rreq->flags); |
| @@ -189,7 +189,7 @@ ssize_t netfs_read_single(struct inode *inode, struct file *file, struct iov_ite · 188 unmodified lines | ||
| 189 | 189 | return ret; |
| 190 | 190 | |
| 191 | 191 | cleanup_free: |
| 192 | − netfs_put_request(rreq, netfs_rreq_trace_put_failed); | |
| 192 | + netfs_put_failed_request(rreq); | |
| 193 | 193 | return ret; |
| 194 | 194 | } |
| 195 | 195 | EXPORT_SYMBOL(netfs_read_single); |
| @@ -133,8 +133,7 @@ struct netfs_io_request *netfs_create_write_req(struct address_space *mapping, · 132 unmodified lines | ||
| 133 | 133 | |
| 134 | 134 | return wreq; |
| 135 | 135 | nomem: |
| 136 | − wreq->error = -ENOMEM; | |
| 137 | − netfs_put_request(wreq, netfs_rreq_trace_put_failed); | |
| 136 | + netfs_put_failed_request(wreq); | |
| 138 | 137 | return ERR_PTR(-ENOMEM); |
| 139 | 138 | } |
| 140 | 139 | |