summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHauke Mehrtens2026-07-19 17:58:45 +0000
committerHauke Mehrtens2026-07-21 19:52:08 +0000
commitb421c63071dc56dd9a2aab2b26dee23bc7eb677f (patch)
tree92b964b0beddf72070be3708db1a0d78824f2528
parent6cfbe534c25de14e934bb66b669206869053b64b (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>
-rw-r--r--package/system/rpcd/Makefile2
-rw-r--r--package/system/rpcd/patches/0001-exec-defer-async-reply-teardown-to-avoid-use-after-f.patch238
-rw-r--r--package/system/rpcd/patches/0002-session-clamp-uloop-timeout-to-avoid-int-overflow.patch88
-rw-r--r--package/system/rpcd/patches/0003-exec-prevent-double-close-of-exec-pipe-descriptors.patch39
-rw-r--r--package/system/rpcd/patches/0004-uci-prevent-integer-overflow-of-client-supplied-appl.patch42
-rw-r--r--package/system/rpcd/patches/0005-treat-exec-failures-in-forked-children-with-_exit-in.patch113
-rw-r--r--package/system/rpcd/patches/0006-file-avoid-zero-length-b64_decode-on-empty-write-dat.patch36
-rw-r--r--package/system/rpcd/patches/0007-ucode-bound-recursion-when-converting-blob-arguments.patch103
-rw-r--r--package/system/rpcd/patches/0008-rc-fix-memory-leak-of-list-request-context.patch33
-rw-r--r--package/system/rpcd/patches/0009-rc-copy-list-name-filter-to-avoid-use-after-free.patch57
-rw-r--r--package/system/rpcd/patches/0010-rc-use-a-per-request-blob_buf-for-the-list-reply.patch97
-rw-r--r--package/system/rpcd/patches/0011-file-re-authorize-ACL-against-resolved-path-to-close.patch275
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();