diff options
| author | Hauke Mehrtens | 2026-07-19 17:58:45 +0000 |
|---|---|---|
| committer | Hauke Mehrtens | 2026-07-21 19:52:08 +0000 |
| commit | b421c63071dc56dd9a2aab2b26dee23bc7eb677f (patch) | |
| tree | 92b964b0beddf72070be3708db1a0d78824f2528 | |
| parent | 6cfbe534c25de14e934bb66b669206869053b64b (diff) | |
rpcd: cherry pick patches from main
This fixes multiple bugs, some might be security relevant.
Link: https://github.com/openwrt/openwrt/pull/24330
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
12 files changed, 1122 insertions, 1 deletions
diff --git a/package/system/rpcd/Makefile b/package/system/rpcd/Makefile index dd2d57f6e7..6cd3b63994 100644 --- a/package/system/rpcd/Makefile +++ b/package/system/rpcd/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=rpcd -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL=$(PROJECT_GIT)/project/rpcd.git diff --git a/package/system/rpcd/patches/0001-exec-defer-async-reply-teardown-to-avoid-use-after-f.patch b/package/system/rpcd/patches/0001-exec-defer-async-reply-teardown-to-avoid-use-after-f.patch new file mode 100644 index 0000000000..843c64c6db --- /dev/null +++ b/package/system/rpcd/patches/0001-exec-defer-async-reply-teardown-to-avoid-use-after-f.patch @@ -0,0 +1,238 @@ +From e19b63ae4a4ffa6ef23dc1bb059d0fdf602fa078 Mon Sep 17 00:00:00 2001 +From: Meng <x.meng@genexis.eu> +Date: Mon, 16 Mar 2026 09:58:36 +0100 +Subject: exec: defer async reply teardown to avoid use-after-free + +Async exec replies currently tear down their context directly from +ustream callbacks. When the callback chain keeps running after the +reply path frees the context, rpcd can hit a use-after-free and crash +with SIGSEGV. + +Defer reply completion through a 0 ms uloop timeout and guard against +duplicate scheduling so cleanup runs after the callbacks unwind in both +exec paths. + +Signed-off-by: Meng <x.meng@genexis.eu> +(cherry picked from commit e655a0d69492a91d9dada74f0832e028e6b5367b) +--- + exec.c | 36 +++++++++++++++++++++++++++++++----- + file.c | 39 ++++++++++++++++++++++++++++++++++----- + include/rpcd/exec.h | 3 +++ + 3 files changed, 68 insertions(+), 10 deletions(-) + +--- a/exec.c ++++ b/exec.c +@@ -122,6 +122,9 @@ rpc_exec_reply(struct rpc_exec_context * + { + uloop_timeout_cancel(&c->timeout); + uloop_process_delete(&c->process); ++ uloop_timeout_cancel(&c->deferred_reply); ++ ++ c->deferred_reply_pending = false; + + if (rv == UBUS_STATUS_OK) + { +@@ -156,13 +159,34 @@ rpc_exec_reply(struct rpc_exec_context * + } + + static void ++rpc_exec_deferred_reply_cb(struct uloop_timeout *t) ++{ ++ struct rpc_exec_context *c = ++ container_of(t, struct rpc_exec_context, deferred_reply); ++ ++ c->deferred_reply_pending = false; ++ rpc_exec_reply(c, c->deferred_status); ++} ++ ++static void ++rpc_exec_schedule_reply(struct rpc_exec_context *c, int rv) ++{ ++ if (c->deferred_reply_pending) ++ return; ++ ++ c->deferred_status = rv; ++ c->deferred_reply_pending = true; ++ uloop_timeout_set(&c->deferred_reply, 0); ++} ++ ++static void + rpc_exec_timeout_cb(struct uloop_timeout *t) + { + struct rpc_exec_context *c = + container_of(t, struct rpc_exec_context, timeout); + + kill(c->process.pid, SIGKILL); +- rpc_exec_reply(c, UBUS_STATUS_TIMEOUT); ++ rpc_exec_schedule_reply(c, UBUS_STATUS_TIMEOUT); + } + + static void +@@ -222,7 +246,7 @@ rpc_exec_opipe_read_cb(struct ustream *s + } + else if (ustream_read_buf_full(s)) + { +- rpc_exec_reply(c, UBUS_STATUS_NOT_SUPPORTED); ++ rpc_exec_schedule_reply(c, UBUS_STATUS_NOT_SUPPORTED); + } + } + +@@ -252,7 +276,7 @@ rpc_exec_epipe_read_cb(struct ustream *s + } + else if (ustream_read_buf_full(s)) + { +- rpc_exec_reply(c, UBUS_STATUS_NOT_SUPPORTED); ++ rpc_exec_schedule_reply(c, UBUS_STATUS_NOT_SUPPORTED); + } + } + +@@ -263,7 +287,7 @@ rpc_exec_opipe_state_cb(struct ustream * + container_of(s, struct rpc_exec_context, opipe.stream); + + if (c->opipe.stream.eof && c->epipe.stream.eof) +- rpc_exec_reply(c, UBUS_STATUS_OK); ++ rpc_exec_schedule_reply(c, UBUS_STATUS_OK); + } + + static void +@@ -273,7 +297,7 @@ rpc_exec_epipe_state_cb(struct ustream * + container_of(s, struct rpc_exec_context, epipe.stream); + + if (c->opipe.stream.eof && c->epipe.stream.eof) +- rpc_exec_reply(c, UBUS_STATUS_OK); ++ rpc_exec_schedule_reply(c, UBUS_STATUS_OK); + } + + int +@@ -349,6 +373,8 @@ rpc_exec(const char **args, rpc_exec_wri + c->process.cb = rpc_exec_process_cb; + uloop_process_add(&c->process); + ++ c->deferred_reply.cb = rpc_exec_deferred_reply_cb; ++ + c->timeout.cb = rpc_exec_timeout_cb; + uloop_timeout_set(&c->timeout, rpc_exec_timeout); + +--- a/file.c ++++ b/file.c +@@ -68,7 +68,10 @@ struct rpc_file_exec_context { + struct uloop_process process; + struct ustream_fd opipe; + struct ustream_fd epipe; ++ struct uloop_timeout deferred_reply; + int stat; ++ int deferred_status; ++ bool deferred_reply_pending; + }; + + +@@ -696,6 +699,9 @@ rpc_file_exec_reply(struct rpc_file_exec + { + uloop_timeout_cancel(&c->timeout); + uloop_process_delete(&c->process); ++ uloop_timeout_cancel(&c->deferred_reply); ++ ++ c->deferred_reply_pending = false; + + if (rv == UBUS_STATUS_OK) + { +@@ -722,13 +728,34 @@ rpc_file_exec_reply(struct rpc_file_exec + } + + static void ++rpc_file_exec_deferred_reply_cb(struct uloop_timeout *t) ++{ ++ struct rpc_file_exec_context *c = ++ container_of(t, struct rpc_file_exec_context, deferred_reply); ++ ++ c->deferred_reply_pending = false; ++ rpc_file_exec_reply(c, c->deferred_status); ++} ++ ++static void ++rpc_file_exec_schedule_reply(struct rpc_file_exec_context *c, int rv) ++{ ++ if (c->deferred_reply_pending) ++ return; ++ ++ c->deferred_status = rv; ++ c->deferred_reply_pending = true; ++ uloop_timeout_set(&c->deferred_reply, 0); ++} ++ ++static void + rpc_file_exec_timeout_cb(struct uloop_timeout *t) + { + struct rpc_file_exec_context *c = + container_of(t, struct rpc_file_exec_context, timeout); + + kill(c->process.pid, SIGKILL); +- rpc_file_exec_reply(c, UBUS_STATUS_TIMEOUT); ++ rpc_file_exec_schedule_reply(c, UBUS_STATUS_TIMEOUT); + } + + static void +@@ -750,7 +777,7 @@ rpc_file_exec_opipe_read_cb(struct ustre + container_of(s, struct rpc_file_exec_context, opipe.stream); + + if (ustream_read_buf_full(s)) +- rpc_file_exec_reply(c, UBUS_STATUS_NOT_SUPPORTED); ++ rpc_file_exec_schedule_reply(c, UBUS_STATUS_NOT_SUPPORTED); + } + + static void +@@ -760,7 +787,7 @@ rpc_file_exec_epipe_read_cb(struct ustre + container_of(s, struct rpc_file_exec_context, epipe.stream); + + if (ustream_read_buf_full(s)) +- rpc_file_exec_reply(c, UBUS_STATUS_NOT_SUPPORTED); ++ rpc_file_exec_schedule_reply(c, UBUS_STATUS_NOT_SUPPORTED); + } + + static void +@@ -770,7 +797,7 @@ rpc_file_exec_opipe_state_cb(struct ustr + container_of(s, struct rpc_file_exec_context, opipe.stream); + + if (c->opipe.stream.eof && c->epipe.stream.eof) +- rpc_file_exec_reply(c, UBUS_STATUS_OK); ++ rpc_file_exec_schedule_reply(c, UBUS_STATUS_OK); + } + + static void +@@ -780,7 +807,7 @@ rpc_file_exec_epipe_state_cb(struct ustr + container_of(s, struct rpc_file_exec_context, epipe.stream); + + if (c->opipe.stream.eof && c->epipe.stream.eof) +- rpc_file_exec_reply(c, UBUS_STATUS_OK); ++ rpc_file_exec_schedule_reply(c, UBUS_STATUS_OK); + } + + static void +@@ -942,6 +969,8 @@ rpc_file_exec_run(const char *cmd, const + c->process.cb = rpc_file_exec_process_cb; + uloop_process_add(&c->process); + ++ c->deferred_reply.cb = rpc_file_exec_deferred_reply_cb; ++ + c->timeout.cb = rpc_file_exec_timeout_cb; + uloop_timeout_set(&c->timeout, *ops->exec_timeout); + +--- a/include/rpcd/exec.h ++++ b/include/rpcd/exec.h +@@ -61,6 +61,7 @@ struct rpc_exec_context { + struct ubus_request_data request; + struct uloop_timeout timeout; + struct uloop_process process; ++ struct uloop_timeout deferred_reply; + struct ustream_fd ipipe; + struct ustream_fd opipe; + struct ustream_fd epipe; +@@ -69,6 +70,8 @@ struct rpc_exec_context { + int errlen; + char *err; + int stat; ++ int deferred_status; ++ bool deferred_reply_pending; + void *priv; + bool blob_array; + void *blob_cookie; diff --git a/package/system/rpcd/patches/0002-session-clamp-uloop-timeout-to-avoid-int-overflow.patch b/package/system/rpcd/patches/0002-session-clamp-uloop-timeout-to-avoid-int-overflow.patch new file mode 100644 index 0000000000..578f8f125e --- /dev/null +++ b/package/system/rpcd/patches/0002-session-clamp-uloop-timeout-to-avoid-int-overflow.patch @@ -0,0 +1,88 @@ +From e09c041c8fb027dde39ef617e24285eb1c654503 Mon Sep 17 00:00:00 2001 +From: Breeze <chanlikessummer@gmail.com> +Date: Thu, 16 Apr 2026 23:56:19 +0800 +Subject: session: clamp uloop timeout to avoid int overflow + +rpc_touch_session() computes `ses->timeout * 1000` as int*int, which +overflows INT_MAX once ses->timeout exceeds 2147483 seconds (~24.85 +days). The wrapped-around negative value passed to uloop_timeout_set() +causes libubox to fire the session timeout callback on the very next +uloop iteration, destroying the just-created session before the caller +can use it. + +In practice this makes `ubus call session login` with e.g. +`timeout:2592000` (30 days) return a valid-looking session id whose +reported `expires` is a large negative number, and any subsequent +session.set / session.get call on that SID returns "Not found". LuCI's +ucode dispatcher passes the value of `luci.sauth.sessiontime` straight +through as the login timeout, so users who follow common advice to bump +sessiontime to 30 days get silently locked out of LuCI (uhttpd logs +`accepted login`, response is 403 with no Set-Cookie) while SSH keeps +working with the same credentials. At least one affected user resorted +to factory-resetting multiple APs before recovering, see +https://forum.openwrt.org/t/241892 . + +The same overflow lurks in rpc_session_from_blob() when thawing a +persisted session, where `blobmsg_get_u64(EXPIRES) * 1000` is passed to +uloop_timeout_set()'s int `msecs` parameter. + +Introduce a small helper that converts a seconds value to milliseconds +with clamping to INT_MAX (and negative-input guard), and use it at both +call sites. The cap still allows uloop timeouts of ~24.85 days, which +is longer than any realistic administrative session. + +Signed-off-by: Breeze <chanlikessummer@gmail.com> +Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> +(cherry picked from commit d005c885dbe439745abe7f64710cb93f5da569ce) +--- + session.c | 28 ++++++++++++++++++++++++++-- + 1 file changed, 26 insertions(+), 2 deletions(-) + +--- a/session.c ++++ b/session.c +@@ -248,11 +248,34 @@ rpc_session_dump(struct rpc_session *ses + ubus_send_reply(ctx, req, buf.head); + } + ++/* ++ * Convert a session timeout in seconds to a millisecond value suitable ++ * for uloop_timeout_set(), clamping to INT_MAX to avoid overflowing the ++ * int argument. Without this, any timeout exceeding ~2147483 seconds ++ * (~24.85 days) would wrap around to a negative value and cause libubox ++ * to fire the timeout callback on the next uloop iteration, destroying ++ * the session immediately after creation. ++ */ ++static int ++rpc_session_timeout_ms(int64_t seconds) ++{ ++ int64_t msecs; ++ ++ if (seconds < 0) ++ seconds = 0; ++ ++ msecs = seconds * 1000; ++ if (msecs > INT_MAX) ++ msecs = INT_MAX; ++ ++ return (int)msecs; ++} ++ + static void + rpc_touch_session(struct rpc_session *ses) + { + if (ses->timeout > 0) +- uloop_timeout_set(&ses->t, ses->timeout * 1000); ++ uloop_timeout_set(&ses->t, rpc_session_timeout_ms(ses->timeout)); + } + + static void +@@ -1315,7 +1338,8 @@ rpc_session_from_blob(struct uci_context + + avl_insert(&sessions, &ses->avl); + +- uloop_timeout_set(&ses->t, blobmsg_get_u64(tb[RPC_DUMP_EXPIRES]) * 1000); ++ uloop_timeout_set(&ses->t, ++ rpc_session_timeout_ms(blobmsg_get_u64(tb[RPC_DUMP_EXPIRES]))); + + return true; + } diff --git a/package/system/rpcd/patches/0003-exec-prevent-double-close-of-exec-pipe-descriptors.patch b/package/system/rpcd/patches/0003-exec-prevent-double-close-of-exec-pipe-descriptors.patch new file mode 100644 index 0000000000..c73d3f29b2 --- /dev/null +++ b/package/system/rpcd/patches/0003-exec-prevent-double-close-of-exec-pipe-descriptors.patch @@ -0,0 +1,39 @@ +From f47d4e3764f16acf05ad802843f80695f52b3652 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sun, 31 May 2026 17:25:08 +0200 +Subject: exec: prevent double close() of exec pipe descriptors + +rpc_exec_process_cb() closes the stdout/stderr read descriptors after +the child has exited, and rpc_exec_reply() closes them once more during +teardown. ustream_free() neither closes nor resets the stored fd, so +both code paths operate on the same descriptor number. In the window +between the two closes the daemon may open another descriptor that +reuses the freed number, which rpc_exec_reply() would then close by +mistake. + +Reset the descriptors to -1 after closing them in the process callback +so the later close() in rpc_exec_reply() becomes a harmless no-op. + +Assisted-by: Claude:claude-opus-4-8 +Link: https://github.com/openwrt/rpcd/pull/34 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit 26dba5206e1721efb217f6e7bafa27cd225bb698) +--- + exec.c | 6 ++++++ + 1 file changed, 6 insertions(+) + +--- a/exec.c ++++ b/exec.c +@@ -203,6 +203,12 @@ rpc_exec_process_cb(struct uloop_process + close(c->opipe.fd.fd); + close(c->epipe.fd.fd); + ++ /* ustream_free() does not reset the fd, and rpc_exec_reply() closes it ++ * again later. Mark the descriptors as consumed so that the second ++ * close() cannot accidentally close an unrelated, meanwhile reused fd. */ ++ c->opipe.fd.fd = -1; ++ c->epipe.fd.fd = -1; ++ + ustream_poll(&c->opipe.stream); + ustream_poll(&c->epipe.stream); + } diff --git a/package/system/rpcd/patches/0004-uci-prevent-integer-overflow-of-client-supplied-appl.patch b/package/system/rpcd/patches/0004-uci-prevent-integer-overflow-of-client-supplied-appl.patch new file mode 100644 index 0000000000..bddc6ee2c6 --- /dev/null +++ b/package/system/rpcd/patches/0004-uci-prevent-integer-overflow-of-client-supplied-appl.patch @@ -0,0 +1,42 @@ +From ee5070bce70a041ccbde011187cd33fb3fd5cf27 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sun, 31 May 2026 17:34:08 +0200 +Subject: uci: prevent integer overflow of client supplied apply timeout + +rpc_uci_apply() reads the "timeout" argument as a uint32 into an int and +scales it with "timeout * 1000" before passing it to +uloop_timeout_set(). A large value overflows the signed multiplication, +and a value with the high bit set wraps to a negative int; both cases +produce a negative millisecond value that schedules the rollback timer +in the past, firing it immediately instead of after the requested +window. + +Clamp the millisecond value to a sane range, consistent with the +session timeout and -t argument overflow fixes. + +Assisted-by: Claude:claude-opus-4-8 +Link: https://github.com/openwrt/rpcd/pull/34 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit af5d6f431186bcd1847e9d3210652b09dbc90ce5) +--- + uci.c | 8 +++++++- + 1 file changed, 7 insertions(+), 1 deletion(-) + +--- a/uci.c ++++ b/uci.c +@@ -1635,8 +1635,14 @@ rpc_uci_apply(struct ubus_context *ctx, + globfree(&gl); + + if (rollback) { ++ /* Clamp to a sane range to avoid signed overflow of the ++ * millisecond value for a large (or, via uint32 wraparound, ++ * negative) client supplied timeout, which would otherwise ++ * fire the rollback timer immediately. */ ++ int msecs = (timeout > 0 && timeout <= INT_MAX / 1000) ++ ? timeout * 1000 : INT_MAX; + apply_timer.cb = rpc_uci_apply_timeout; +- uloop_timeout_set(&apply_timer, timeout * 1000); ++ uloop_timeout_set(&apply_timer, msecs); + apply_ctx = ctx; + } + } diff --git a/package/system/rpcd/patches/0005-treat-exec-failures-in-forked-children-with-_exit-in.patch b/package/system/rpcd/patches/0005-treat-exec-failures-in-forked-children-with-_exit-in.patch new file mode 100644 index 0000000000..e425f8f242 --- /dev/null +++ b/package/system/rpcd/patches/0005-treat-exec-failures-in-forked-children-with-_exit-in.patch @@ -0,0 +1,113 @@ +From 56a8ae88024efae44bfa296f3c8b01d2c350648f Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sun, 31 May 2026 17:51:23 +0200 +Subject: treat exec failures in forked children with _exit() instead of return + +Several handlers fork and, in the child, fall back to "return <ubus +status>" when open()/malloc()/execv() etc. fail. Returning a ubus +status code from the child does not terminate it: control unwinds back +into the ubus dispatch loop and the child keeps running as a duplicate +rpcd, racing the parent on the ubus socket and pipes. + +Replace these child-side returns with _exit(127) so a child that cannot +exec the target simply terminates, which is also what the parent's +WEXITSTATUS based reporting expects. Affected paths: exec.c (rpc_exec), +file.c (rpc_file_exec_run), sys.c (rpc_cgi_password_set) and plugin.c +(rpc_plugin_register_exec). + +Assisted-by: Claude:claude-opus-4-8 +Link: https://github.com/openwrt/rpcd/pull/34 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit 3037a0e36856159075624fb00b5410d5577ed5fe) +--- + exec.c | 2 +- + file.c | 10 +++++----- + plugin.c | 2 +- + sys.c | 4 ++-- + 4 files changed, 9 insertions(+), 9 deletions(-) + +--- a/exec.c ++++ b/exec.c +@@ -360,7 +360,7 @@ rpc_exec(const char **args, rpc_exec_wri + close(epipe[1]); + + if (execv(cmd, (char * const *)args)) +- return rpc_errno_status(); ++ _exit(127); + + default: + memset(c, 0, sizeof(*c)); +--- a/file.c ++++ b/file.c +@@ -896,7 +896,7 @@ rpc_file_exec_run(const char *cmd, const + devnull = open("/dev/null", O_RDWR); + + if (devnull == -1) +- return UBUS_STATUS_UNKNOWN_ERROR; ++ _exit(127); + + dup2(devnull, 0); + dup2(opipe[1], 1); +@@ -912,7 +912,7 @@ rpc_file_exec_run(const char *cmd, const + args = malloc(sizeof(char *) * arglen); + + if (!args) +- return UBUS_STATUS_UNKNOWN_ERROR; ++ _exit(127); + + args[0] = (char *)executable; + args[1] = NULL; +@@ -927,7 +927,7 @@ rpc_file_exec_run(const char *cmd, const + if (arglen == 255) + { + free(args); +- return UBUS_STATUS_INVALID_ARGUMENT; ++ _exit(127); + } + + arglen++; +@@ -936,7 +936,7 @@ rpc_file_exec_run(const char *cmd, const + if (!tmp) + { + free(args); +- return UBUS_STATUS_UNKNOWN_ERROR; ++ _exit(127); + } + + args = tmp; +@@ -957,7 +957,7 @@ rpc_file_exec_run(const char *cmd, const + } + + if (execv(executable, args)) +- return rpc_errno_status(); ++ _exit(127); + + default: + memset(c, 0, sizeof(*c)); +--- a/plugin.c ++++ b/plugin.c +@@ -451,7 +451,7 @@ rpc_plugin_register_exec(struct ubus_con + close(fds[1]); + + if (execl(path, path, "list", NULL)) +- return UBUS_STATUS_UNKNOWN_ERROR; ++ _exit(127); + + default: + plugin = rpc_plugin_parse_exec(name + 1, fds[0]); +--- a/sys.c ++++ b/sys.c +@@ -129,11 +129,11 @@ rpc_cgi_password_set(struct ubus_context + + ret = chdir("/"); + if (ret < 0) +- return rpc_errno_status(); ++ _exit(127); + + if (execl(passwd, passwd, + blobmsg_data(tb[RPC_P_USER]), NULL)) +- return rpc_errno_status(); ++ _exit(127); + + default: + close(fds[0]); diff --git a/package/system/rpcd/patches/0006-file-avoid-zero-length-b64_decode-on-empty-write-dat.patch b/package/system/rpcd/patches/0006-file-avoid-zero-length-b64_decode-on-empty-write-dat.patch new file mode 100644 index 0000000000..be510fee3a --- /dev/null +++ b/package/system/rpcd/patches/0006-file-avoid-zero-length-b64_decode-on-empty-write-dat.patch @@ -0,0 +1,36 @@ +From a47b5b9cbfcdbcc446ef411a0a594b76b2925ce2 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sun, 31 May 2026 18:08:25 +0200 +Subject: file: avoid zero-length b64_decode() on empty write data + +rpc_file_write() computes data_len as blobmsg_data_len() - 1, which is 0 +for an empty "data" string. With base64 enabled it then called +b64_decode(data, data, 0); b64_decode() asserts dest size > 0, so a +request like file.write {"data":"","base64":true} aborts the daemon when +libubox is built with assertions enabled (and is a pointless call +otherwise). + +Skip the decode when there is no data; the file is still created/ +truncated and zero bytes are written, which is the correct result. + +Assisted-by: Claude:claude-opus-4-8 +Link: https://github.com/openwrt/rpcd/pull/34 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit 79c8087c8e8eb8933980fa6fc584703526388446) +--- + file.c | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +--- a/file.c ++++ b/file.c +@@ -405,7 +405,9 @@ rpc_file_write(struct ubus_context *ctx, + if (fd < 0) + return rpc_errno_status(); + +- if (tb[RPC_F_RW_BASE64] && blobmsg_get_bool(tb[RPC_F_RW_BASE64])) ++ /* data_len can be 0 for an empty "data" string; skip the decode in that ++ * case since b64_decode() asserts on a zero destination size. */ ++ if (data_len > 0 && tb[RPC_F_RW_BASE64] && blobmsg_get_bool(tb[RPC_F_RW_BASE64])) + { + data_len = b64_decode(data, data, data_len); + if (data_len < 0) diff --git a/package/system/rpcd/patches/0007-ucode-bound-recursion-when-converting-blob-arguments.patch b/package/system/rpcd/patches/0007-ucode-bound-recursion-when-converting-blob-arguments.patch new file mode 100644 index 0000000000..67fb9f3c33 --- /dev/null +++ b/package/system/rpcd/patches/0007-ucode-bound-recursion-when-converting-blob-arguments.patch @@ -0,0 +1,103 @@ +From af062601c6b2dc1da27a24d08cb8d11814cbc09e Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sun, 31 May 2026 18:14:05 +0200 +Subject: ucode: bound recursion when converting blob arguments to ucode values + +rpc_ucode_blob_array_to_ucv()/rpc_ucode_blob_to_ucv() recurse once per +nesting level of the incoming request message, which is fully attacker +controlled. ubus permits messages up to UBUS_MAX_MSGLEN (1 MiB) and +libubox enforces no nesting limit, so a deeply nested array/table +argument to a ucode plugin method can drive tens of thousands of +recursion levels and overflow the stack, crashing rpcd. + +Thread a depth counter through the conversion and stop descending past +RPC_UCODE_MAX_NESTING (32) levels, which is far beyond any legitimate +ubus message. Over-deep subtrees become null values instead of +crashing. + +Assisted-by: Claude:claude-opus-4-8 +Link: https://github.com/openwrt/rpcd/pull/34 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit cd1d9588da6368af1dbc99596ded90d7e5e027e1) +--- + ucode.c | 26 ++++++++++++++++++-------- + 1 file changed, 18 insertions(+), 8 deletions(-) + +--- a/ucode.c ++++ b/ucode.c +@@ -34,6 +34,11 @@ + + #define RPC_UCSCRIPT_DIRECTORY INSTALL_PREFIX "/share/rpcd/ucode" + ++/* Bound the recursion when converting a (client supplied) blob message into ++ * ucode values, so a deeply nested argument cannot overflow the stack. Real ++ * ubus messages never come anywhere near this depth. */ ++#define RPC_UCODE_MAX_NESTING 32 ++ + static struct blob_buf buf; + static int request_timeout; + +@@ -173,23 +178,28 @@ rpc_ucode_ucv_object_to_blob(uc_value_t + } + + static uc_value_t * +-rpc_ucode_blob_to_ucv(uc_vm_t *vm, struct blob_attr *attr, bool table, const char **name); ++rpc_ucode_blob_to_ucv(uc_vm_t *vm, struct blob_attr *attr, bool table, const char **name, int depth); + + static uc_value_t * +-rpc_ucode_blob_array_to_ucv(uc_vm_t *vm, struct blob_attr *attr, size_t len, bool table) ++rpc_ucode_blob_array_to_ucv(uc_vm_t *vm, struct blob_attr *attr, size_t len, bool table, int depth) + { +- uc_value_t *o = table ? ucv_object_new(vm) : ucv_array_new(vm); ++ uc_value_t *o; + uc_value_t *v; + struct blob_attr *pos; + size_t rem = len; + const char *name; + ++ if (depth > RPC_UCODE_MAX_NESTING) ++ return NULL; ++ ++ o = table ? ucv_object_new(vm) : ucv_array_new(vm); ++ + if (!o) + return NULL; + + __blob_for_each_attr(pos, attr, rem) { + name = NULL; +- v = rpc_ucode_blob_to_ucv(vm, pos, table, &name); ++ v = rpc_ucode_blob_to_ucv(vm, pos, table, &name, depth); + + if (table && name) + ucv_object_add(o, name, v); +@@ -203,7 +213,7 @@ rpc_ucode_blob_array_to_ucv(uc_vm_t *vm, + } + + static uc_value_t * +-rpc_ucode_blob_to_ucv(uc_vm_t *vm, struct blob_attr *attr, bool table, const char **name) ++rpc_ucode_blob_to_ucv(uc_vm_t *vm, struct blob_attr *attr, bool table, const char **name, int depth) + { + void *data; + int len; +@@ -245,10 +255,10 @@ rpc_ucode_blob_to_ucv(uc_vm_t *vm, struc + return ucv_string_new(data); + + case BLOBMSG_TYPE_ARRAY: +- return rpc_ucode_blob_array_to_ucv(vm, data, len, false); ++ return rpc_ucode_blob_array_to_ucv(vm, data, len, false, depth + 1); + + case BLOBMSG_TYPE_TABLE: +- return rpc_ucode_blob_array_to_ucv(vm, data, len, true); ++ return rpc_ucode_blob_array_to_ucv(vm, data, len, true, depth + 1); + + default: + return NULL; +@@ -313,7 +323,7 @@ rpc_ucode_validate_call_args(struct ubus + } + } + +- *res = rpc_ucode_blob_array_to_ucv(&script->vm, blob_data(msg), blob_len(msg), true); ++ *res = rpc_ucode_blob_array_to_ucv(&script->vm, blob_data(msg), blob_len(msg), true, 0); + + return UBUS_STATUS_OK; + diff --git a/package/system/rpcd/patches/0008-rc-fix-memory-leak-of-list-request-context.patch b/package/system/rpcd/patches/0008-rc-fix-memory-leak-of-list-request-context.patch new file mode 100644 index 0000000000..a43b8aabfb --- /dev/null +++ b/package/system/rpcd/patches/0008-rc-fix-memory-leak-of-list-request-context.patch @@ -0,0 +1,33 @@ +From 4591e70507777ac1bdd5cc3229b38db3a57da14e Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sun, 31 May 2026 17:38:38 +0200 +Subject: rc: fix memory leak of list request context + +rc_list() allocates a struct rc_list_context with calloc() and defers +the request; rc_list_readdir() walks /etc/init.d asynchronously and, +once the directory is exhausted (or the requested entry was found), +sends the reply and completes the deferred request -- but never frees +the context. free(c) only existed in the early opendir() failure path, +so every successful "rc list" ubus call leaked the context (which embeds +a PATH_MAX sized path buffer), slowly exhausting memory. + +Free the context in the terminal path after completing the request. + +Assisted-by: Claude:claude-opus-4-8 +Link: https://github.com/openwrt/rpcd/pull/34 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit d06d2a81dc29d746c5f13a9cc07ce779b5ed39be) +--- + rc.c | 1 + + 1 file changed, 1 insertion(+) + +--- a/rc.c ++++ b/rc.c +@@ -193,6 +193,7 @@ static void rc_list_readdir(struct rc_li + closedir(c->dir); + ubus_send_reply(c->ctx, &c->req, c->buf->head); + ubus_complete_deferred_request(c->ctx, &c->req, UBUS_STATUS_OK); ++ free(c); + return; + } + diff --git a/package/system/rpcd/patches/0009-rc-copy-list-name-filter-to-avoid-use-after-free.patch b/package/system/rpcd/patches/0009-rc-copy-list-name-filter-to-avoid-use-after-free.patch new file mode 100644 index 0000000000..9c8f9fecda --- /dev/null +++ b/package/system/rpcd/patches/0009-rc-copy-list-name-filter-to-avoid-use-after-free.patch @@ -0,0 +1,57 @@ +From 361104e320e1a5edd8fe52a28bad7bf193e83cc1 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sun, 31 May 2026 17:41:10 +0200 +Subject: rc: copy list "name" filter to avoid use-after-free + +rc_list() stored the "name" argument as a bare pointer into the parsed +request message and then deferred the request. The init.d directory is +walked asynchronously across multiple uloop iterations (each script's +"running" check is a forked child), during which the single per-context +ubus receive buffer that backs the message is overwritten by any other +incoming ubus request. c->req_name then points at unrelated/freed data +and is compared against directory entries, leading to wrong matches or a +crash. + +Duplicate the name into the context and release it when the request +completes. + +Assisted-by: Claude:claude-opus-4-8 +Link: https://github.com/openwrt/rpcd/pull/34 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit 0de6668115593f2d316070573d12c29409c89078) +--- + rc.c | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +--- a/rc.c ++++ b/rc.c +@@ -50,7 +50,7 @@ struct rc_list_context { + struct blob_buf *buf; + DIR *dir; + bool skip_running_check; +- const char *req_name; ++ char *req_name; + + /* Info about currently processed init.d entry */ + struct { +@@ -193,6 +193,7 @@ static void rc_list_readdir(struct rc_li + closedir(c->dir); + ubus_send_reply(c->ctx, &c->req, c->buf->head); + ubus_complete_deferred_request(c->ctx, &c->req, UBUS_STATUS_OK); ++ free(c->req_name); + free(c); + return; + } +@@ -283,8 +284,11 @@ static int rc_list(struct ubus_context * + } + if (tb[RC_LIST_SKIP_RUNNING_CHECK]) + c->skip_running_check = blobmsg_get_bool(tb[RC_LIST_SKIP_RUNNING_CHECK]); ++ /* Copy the requested name: msg (and the ctx receive buffer it points ++ * into) is only valid during this call, but req_name is dereferenced ++ * later from the deferred, asynchronous directory walk. */ + if (tb[RC_LIST_NAME]) +- c->req_name = blobmsg_get_string(tb[RC_LIST_NAME]); ++ c->req_name = strdup(blobmsg_get_string(tb[RC_LIST_NAME])); + + ubus_defer_request(ctx, req, &c->req); + diff --git a/package/system/rpcd/patches/0010-rc-use-a-per-request-blob_buf-for-the-list-reply.patch b/package/system/rpcd/patches/0010-rc-use-a-per-request-blob_buf-for-the-list-reply.patch new file mode 100644 index 0000000000..53b4cf627f --- /dev/null +++ b/package/system/rpcd/patches/0010-rc-use-a-per-request-blob_buf-for-the-list-reply.patch @@ -0,0 +1,97 @@ +From 3e026930ab4bbf6d741e9586d5ec207a6e6496d4 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sun, 31 May 2026 17:42:56 +0200 +Subject: rc: use a per-request blob_buf for the list reply + +rc_list() built the reply in a function-local "static struct blob_buf", +shared by every invocation, and kept using it across the deferred, +asynchronous directory walk. Two overlapping "rc list" requests (the +walk waits on forked children, so a second request is easily interleaved) +would both blob_buf_init() and append to the same buffer, corrupting +each other's reply or crashing. + +Move the blob_buf into the per-request context so each request owns its +own buffer, and free it when the request completes. + +Assisted-by: Claude:claude-opus-4-8 +Link: https://github.com/openwrt/rpcd/pull/34 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit 75470f4b5124074ff078ed7052bb756e3b694712) +--- + rc.c | 24 ++++++++++++------------ + 1 file changed, 12 insertions(+), 12 deletions(-) + +--- a/rc.c ++++ b/rc.c +@@ -47,7 +47,7 @@ struct rc_list_context { + struct uloop_timeout timeout; + struct ubus_context *ctx; + struct ubus_request_data req; +- struct blob_buf *buf; ++ struct blob_buf buf; + DIR *dir; + bool skip_running_check; + char *req_name; +@@ -88,17 +88,17 @@ static void rc_list_add_table(struct rc_ + { + void *e; + +- e = blobmsg_open_table(c->buf, c->entry.d_name); ++ e = blobmsg_open_table(&c->buf, c->entry.d_name); + + if (c->entry.start >= 0) +- blobmsg_add_u16(c->buf, "start", c->entry.start); ++ blobmsg_add_u16(&c->buf, "start", c->entry.start); + if (c->entry.stop >= 0) +- blobmsg_add_u16(c->buf, "stop", c->entry.stop); +- blobmsg_add_u8(c->buf, "enabled", c->entry.enabled); ++ blobmsg_add_u16(&c->buf, "stop", c->entry.stop); ++ blobmsg_add_u8(&c->buf, "enabled", c->entry.enabled); + if (!c->skip_running_check && c->entry.use_procd) +- blobmsg_add_u8(c->buf, "running", c->entry.running); ++ blobmsg_add_u8(&c->buf, "running", c->entry.running); + +- blobmsg_close_table(c->buf, e); ++ blobmsg_close_table(&c->buf, e); + } + + static void rpc_list_exec_timeout_cb(struct uloop_timeout *t) +@@ -191,8 +191,9 @@ static void rc_list_readdir(struct rc_li + */ + if (!e || (c->req_name && c->entry.d_name)) { + closedir(c->dir); +- ubus_send_reply(c->ctx, &c->req, c->buf->head); ++ ubus_send_reply(c->ctx, &c->req, c->buf.head); + ubus_complete_deferred_request(c->ctx, &c->req, UBUS_STATUS_OK); ++ blob_buf_free(&c->buf); + free(c->req_name); + free(c); + return; +@@ -264,24 +265,23 @@ static int rc_list(struct ubus_context * + struct blob_attr *msg) + { + struct blob_attr *tb[__RC_LIST_MAX]; +- static struct blob_buf buf; + struct rc_list_context *c; + + blobmsg_parse(rc_list_policy, __RC_LIST_MAX, tb, blobmsg_data(msg), blobmsg_data_len(msg)); + +- blob_buf_init(&buf, 0); +- + c = calloc(1, sizeof(*c)); + if (!c) + return UBUS_STATUS_UNKNOWN_ERROR; + + c->ctx = ctx; +- c->buf = &buf; + c->dir = opendir("/etc/init.d"); + if (!c->dir) { + free(c); + return UBUS_STATUS_UNKNOWN_ERROR; + } ++ ++ blob_buf_init(&c->buf, 0); ++ + if (tb[RC_LIST_SKIP_RUNNING_CHECK]) + c->skip_running_check = blobmsg_get_bool(tb[RC_LIST_SKIP_RUNNING_CHECK]); + /* Copy the requested name: msg (and the ctx receive buffer it points diff --git a/package/system/rpcd/patches/0011-file-re-authorize-ACL-against-resolved-path-to-close.patch b/package/system/rpcd/patches/0011-file-re-authorize-ACL-against-resolved-path-to-close.patch new file mode 100644 index 0000000000..d3d1194623 --- /dev/null +++ b/package/system/rpcd/patches/0011-file-re-authorize-ACL-against-resolved-path-to-close.patch @@ -0,0 +1,275 @@ +From 25ca51acb9ce43c438252bd1cb5cf7f12f5e7f0c Mon Sep 17 00:00:00 2001 +From: Jo-Philipp Wich <jo@mein.io> +Date: Sun, 19 Jul 2026 14:57:18 +0200 +Subject: file: re-authorize ACL against resolved path to close symlink bypass + +The file plugin matched ACL grants against the textual, canonicalized +path but then let open()/stat()/opendir() follow symlinks unchecked. +A symlink placed inside an ACL-covered directory therefore let a grant +on the link authorize read/write/list/stat access to whatever file it +pointed to, including files entirely outside the granted scope. + +Re-resolve the path with realpath() and re-run the ACL check against +the resolved target whenever it differs from the canonicalized string, +for every operation that dereferences the final component (read, +write, md5, list, stat). lstat and remove are left untouched since +they either need to see the link itself or already handle it safely +via unlink()'s no-follow semantics. Newly created files (file.write) +and dangling symlinks pointing outside the granted scope are handled +via the containing directory. + +Also mask file.write's mode parameter to 0777 and drop the local +umask(0) override, and authorize recursive file.remove per-entry +instead of only at the top-level path. + +Fixes GHSA-q5gr-86pq-vvwr. + +Signed-off-by: Jo-Philipp Wich <jo@mein.io> +(cherry picked from commit e37ed9d814699098eb7e26c8b33c054840782dfb) +--- + file.c | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- + 1 file changed, 144 insertions(+), 12 deletions(-) + +--- a/file.c ++++ b/file.c +@@ -77,6 +77,7 @@ struct rpc_file_exec_context { + + static struct blob_buf buf; + static char *canonpath; ++static char *resolvedpath; + static char cmdstr[RPC_CMDLINE_MAX_SIZE]; + + enum { +@@ -246,10 +247,122 @@ next: + return canonpath; + } + ++/* ++ * rpc_canonicalize_path() only folds "//", "/./" and "/../" textually; it ++ * never resolves symlink components. Since the ACL check above matches the ++ * textual path, a symlink placed inside an ACL-covered directory would let ++ * a grant on the link authorize whatever file the link points to once the ++ * caller's stat()/open()/opendir() follows it. Re-resolve *path with ++ * realpath() and, if that changes the path, re-run the ACL check against ++ * the resolved target so grants only ever cover the real file. If the ++ * target does not exist yet (e.g. a new file being written), resolve and ++ * re-check the containing directory instead and rebuild *path from that. ++ */ ++static bool ++rpc_check_symlink_access(const struct blob_attr *sid, const char *perm, char **path) ++{ ++ char resolved[PATH_MAX]; ++ char dirbuf[PATH_MAX]; ++ const char *base; ++ struct stat lst; ++ ++ if (realpath(*path, resolved) != NULL) ++ { ++ if (strcmp(resolved, *path) == 0) ++ return true; ++ ++ if (!rpc_file_access(sid, resolved, perm)) ++ { ++ errno = EACCES; ++ return false; ++ } ++ ++ free(resolvedpath); ++ resolvedpath = strdup(resolved); ++ ++ if (resolvedpath == NULL) ++ { ++ errno = ENOMEM; ++ return false; ++ } ++ ++ *path = resolvedpath; ++ return true; ++ } ++ ++ if (errno == ENOENT) ++ { ++ /* realpath() also fails with ENOENT for a dangling symlink whose ++ * final target component is missing. Distinguish that case (an ++ * *existing* symlink we must not silently create-through, e.g. ++ * via open(O_CREAT) on file.write) from a genuinely nonexistent ++ * path by lstat()'ing the requested path itself. */ ++ errno = (lstat(*path, &lst) == 0 && S_ISLNK(lst.st_mode)) ? EACCES : 0; ++ } ++ ++ if (errno != 0) ++ return false; ++ ++ base = strrchr(*path, '/'); ++ ++ if (base == NULL) ++ { ++ errno = ENOENT; ++ return false; ++ } ++ ++ if (base == *path) ++ { ++ dirbuf[0] = '/'; ++ dirbuf[1] = '\0'; ++ } ++ else if ((size_t)(base - *path) >= sizeof(dirbuf)) ++ { ++ errno = ENAMETOOLONG; ++ return false; ++ } ++ else ++ { ++ memcpy(dirbuf, *path, base - *path); ++ dirbuf[base - *path] = '\0'; ++ } ++ ++ base++; ++ ++ if (realpath(dirbuf, resolved) == NULL) ++ return false; ++ ++ if (strcmp(resolved, dirbuf) == 0) ++ { ++ errno = ENOENT; ++ return true; ++ } ++ ++ free(resolvedpath); ++ ++ if (asprintf(&resolvedpath, "%s/%s", resolved, base) < 0) ++ { ++ errno = ENOMEM; ++ return false; ++ } ++ ++ if (!rpc_file_access(sid, resolvedpath, perm)) ++ { ++ errno = EACCES; ++ return false; ++ } ++ ++ *path = resolvedpath; ++ errno = ENOENT; ++ ++ return true; ++} ++ + static struct blob_attr ** + __rpc_check_path(const struct blobmsg_policy *policy, size_t policy_len, + int policy_path_idx, int policy_sid_idx, const char *perm, +- struct blob_attr *msg, char **path, struct stat *s) ++ struct blob_attr *msg, char **path, struct stat *s, ++ bool resolve_symlinks) + { + static struct blob_attr *tb[__RPC_F_RW_MAX]; /* largest _MAX constant */ + +@@ -275,18 +388,32 @@ __rpc_check_path(const struct blobmsg_po + return NULL; + } + ++ if (resolve_symlinks && !rpc_check_symlink_access(tb[policy_sid_idx], perm, path)) ++ return NULL; ++ + if (s != NULL && stat(*path, s) != 0) + return NULL; + + return tb; + } + ++// symlinks are resolved and the ACL re-checked against the resolved target + #define rpc_check_path(msg, policy_selector, perm, path, s) \ + __rpc_check_path(rpc_file_ ## policy_selector ## _policy, \ + ARRAY_SIZE(rpc_file_ ## policy_selector ## _policy), \ + RPC_F_ ## policy_selector ## _PATH, \ + RPC_F_ ## policy_selector ## _SESSION, \ +- perm, msg, path, s) ++ perm, msg, path, s, true) ++ ++// symlinks are left unresolved since the caller either wants to inspect the ++// link itself or already handles it safely via unlink()'s no-follow ++// semantics (remove) ++#define rpc_check_path_no_resolve(msg, policy_selector, perm, path, s) \ ++ __rpc_check_path(rpc_file_ ## policy_selector ## _policy, \ ++ ARRAY_SIZE(rpc_file_ ## policy_selector ## _policy), \ ++ RPC_F_ ## policy_selector ## _PATH, \ ++ RPC_F_ ## policy_selector ## _SESSION, \ ++ perm, msg, path, s, false) + + static int + rpc_file_read(struct ubus_context *ctx, struct ubus_object *obj, +@@ -376,7 +503,7 @@ rpc_file_write(struct ubus_context *ctx, + { + struct blob_attr **tb; + int append = O_TRUNC; +- mode_t prev_mode, mode = 0666; ++ mode_t mode = 0666; + int fd, rv = 0; + char *path = NULL; + void *data = NULL; +@@ -397,11 +524,9 @@ rpc_file_write(struct ubus_context *ctx, + append = O_APPEND; + + if (tb[RPC_F_RW_MODE]) +- mode = blobmsg_get_u32(tb[RPC_F_RW_MODE]); ++ mode = blobmsg_get_u32(tb[RPC_F_RW_MODE]) & 0777; + +- prev_mode = umask(0); + fd = open(path, O_CREAT | O_WRONLY | append, mode); +- umask(prev_mode); + if (fd < 0) + return rpc_errno_status(); + +@@ -562,10 +687,10 @@ rpc_file_stat(struct ubus_context *ctx, + } + + static int +-rpc_file_remove_recursive(const char *path); ++rpc_file_remove_recursive(const struct blob_attr *sid, const char *path); + + static int +-rpc_file_remove_recursive(const char *path) ++rpc_file_remove_recursive(const struct blob_attr *sid, const char *path) + { + DIR *fd; + int err = 0; +@@ -583,10 +708,14 @@ rpc_file_remove_recursive(const char *pa + + if (asprintf(&entrypath, "%s/%s", path, e->d_name) >= 0) + { +- if (!lstat(entrypath, &s)) ++ if (!rpc_file_access(sid, entrypath, "write")) ++ { ++ err = UBUS_STATUS_PERMISSION_DENIED; ++ } ++ else if (!lstat(entrypath, &s)) + { + if (S_ISDIR(s.st_mode)) +- err = rpc_file_remove_recursive(entrypath); ++ err = rpc_file_remove_recursive(sid, entrypath); + else if (unlink(entrypath)) + err = rpc_errno_status(); + } +@@ -612,17 +741,20 @@ rpc_file_remove(struct ubus_context *ctx + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) + { ++ struct blob_attr **tb; + struct stat s; + char *path = NULL; + +- if (!rpc_check_path(msg, R, "write", &path, NULL)) ++ tb = rpc_check_path_no_resolve(msg, R, "write", &path, NULL); ++ ++ if (tb == NULL) + return rpc_errno_status(); + + if (lstat(path, &s)) + return rpc_errno_status(); + + if (S_ISDIR(s.st_mode)) +- return rpc_file_remove_recursive(path); ++ return rpc_file_remove_recursive(tb[RPC_F_R_SESSION], path); + + if (unlink(path)) + return rpc_errno_status(); |