Skip to content

Commit 9fd0a24

Browse files
committed
lws_spawn: add cgroup support
1 parent ede430b commit 9fd0a24

File tree

5 files changed

+288
-4
lines changed

5 files changed

+288
-4
lines changed

include/libwebsockets/lws-misc.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,8 @@ typedef void (*lsp_cb_t)(void *opaque, lws_usec_t *accounting, siginfo_t *si,
10531053
* \p timeout: optional us-resolution timeout, or zero
10541054
* \p reap_cb: callback when child process has been reaped and the lsp destroyed
10551055
* \p tsi: tsi to bind stdwsi to... from opt_parent if given
1056+
* \p cgroup_name_suffix: for Linux, encapsulate spawn into this new cgroup
1057+
* \p p_cgroup_ret: NULL, or pointer to int to show if cgroups applied OK (0 = OK)
10561058
*/
10571059
struct lws_spawn_piped_info {
10581060
struct lws_dll2_owner *owner;
@@ -1078,6 +1080,9 @@ struct lws_spawn_piped_info {
10781080
const struct lws_role_ops *ops; /* NULL is raw file */
10791081

10801082
uint8_t disable_ctrlc;
1083+
1084+
const char *cgroup_name_suffix;
1085+
int *p_cgroup_ret;
10811086
};
10821087

10831088
/**
@@ -1154,6 +1159,30 @@ lws_spawn_get_stdfd(struct lws *wsi);
11541159
*/
11551160
LWS_VISIBLE LWS_EXTERN int
11561161
lws_spawn_get_fd_stdxxx(struct lws_spawn_piped *lsp, int std_idx);
1162+
1163+
/**
1164+
* lws_spawn_prepare_self_cgroup() - Create lws parent cgroup
1165+
*
1166+
* \p user: NULL, or the user name that will use the toplevel cgroup
1167+
* \p group: NULL, or the group name that will use the toplevel cgroup
1168+
*
1169+
* This helper should be called once at startup by a process that has root
1170+
* privileges. It will create and configure the master cgroup directory
1171+
* `/sys/fs/cgroup/<toplevel_name>`.
1172+
*
1173+
* After this has been called successfully, the process can drop privileges
1174+
* to a non-root user, and subsequent calls to lws_spawn_piped() with a
1175+
* cgroup_name_suffix will succeed as long as that user has write permission
1176+
* in the master cgroup directory (which can be arranged via chown).
1177+
*
1178+
* Returns 0 on success. On non-Linux platforms, it's a no-op that returns 1.
1179+
*/
1180+
LWS_VISIBLE LWS_EXTERN int
1181+
lws_spawn_prepare_self_cgroup(const char *user, const char *group);
1182+
1183+
LWS_VISIBLE LWS_EXTERN int
1184+
lws_spawn_get_self_cgroup(char *cgroup, size_t max);
1185+
11571186
#endif
11581187

11591188
struct lws_fsmount {

lib/core-net/private-lib-core-net.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,10 @@ struct lws_spawn_piped {
926926
lws_sorted_usec_list_t sul;
927927
lws_sorted_usec_list_t sul_reap;
928928

929+
#if defined(__linux__)
930+
char cgroup_path[256];
931+
#endif
932+
929933
struct lws_context *context;
930934
struct lws *stdwsi[3];
931935
lws_filefd_type pipe_fds[3][2];

lib/core/context.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1757,7 +1757,7 @@ lws_system_cpd_set(struct lws_context *cx, lws_cpd_result_t result)
17571757
return;
17581758

17591759
#if !defined(LWS_WITH_NO_LOGS)
1760-
lwsl_cx_notice(cx, "setting CPD result %s", cname[result]);
1760+
lwsl_cx_info(cx, "setting CPD result %s", cname[result]);
17611761
#endif
17621762

17631763
cx->captive_portal_detect = (uint8_t)result;

lib/plat/unix/unix-spawn.c

Lines changed: 248 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828

2929
#include "private-lib-core.h"
3030
#include <unistd.h>
31+
#include <sys/types.h>
32+
#include <pwd.h>
33+
#include <grp.h>
34+
35+
#if defined(__linux__)
36+
#include <sys/stat.h>
37+
#endif
3138

3239
#if defined(__OpenBSD__) || defined(__NetBSD__)
3340
#include <sys/resource.h>
@@ -51,7 +58,7 @@ lws_spawn_sul_reap(struct lws_sorted_usec_list *sul)
5158
struct lws_spawn_piped *lsp = lws_container_of(sul,
5259
struct lws_spawn_piped, sul_reap);
5360

54-
lwsl_notice("%s: reaping spawn after last stdpipe, tries left %d\n",
61+
lwsl_info("%s: reaping spawn after last stdpipe, tries left %d\n",
5562
__func__, lsp->reap_retry_budget);
5663
if (!lws_spawn_reap(lsp) && !lsp->pipes_alive) {
5764
if (--lsp->reap_retry_budget) {
@@ -212,6 +219,21 @@ lws_spawn_reap(struct lws_spawn_piped *lsp)
212219

213220
lws_sul_cancel(&lsp->sul);
214221

222+
#if defined(__linux__)
223+
if (lsp->cgroup_path[0]) {
224+
/*
225+
* The child has been reaped, we can remove the cgroup dir.
226+
* This will only work if the cgroup is empty, which it should
227+
* be now.
228+
*/
229+
if (rmdir(lsp->cgroup_path))
230+
lwsl_warn("%s: unable to rmdir cgroup %s, errno %d\n",
231+
__func__, lsp->cgroup_path, errno);
232+
else
233+
lwsl_info("%s: reaped cgroup %s\n", __func__, lsp->cgroup_path);
234+
}
235+
#endif
236+
215237
/*
216238
* All the stdwsi went down, nothing more is coming... it's over
217239
* Collect the final information and then reap the dead process
@@ -240,7 +262,7 @@ lws_spawn_reap(struct lws_spawn_piped *lsp)
240262
n = waitid(P_PID, (id_t)lsp->child_pid, &temp.si, WEXITED | WNOHANG);
241263
#endif
242264
temp.si.si_status &= 0xff; /* we use b8 + for flags */
243-
lwsl_info("%s: waitd says %d, process exit %d\n",
265+
lwsl_warn("%s: waitd says %d, process exit %d\n",
244266
__func__, n, temp.si.si_status);
245267

246268
lsp->child_pid = -1;
@@ -319,6 +341,56 @@ lws_spawn_piped_kill_child_process(struct lws_spawn_piped *lsp)
319341
return 0;
320342
}
321343

344+
int
345+
lws_spawn_get_self_cgroup(char *cgroup, size_t max)
346+
{
347+
int fd = open("/proc/self/cgroup", O_RDONLY);
348+
ssize_t r;
349+
char *p, s[256];
350+
351+
if (fd < 0) {
352+
lwsl_err("%s: unable to open /proc/self/cgroup\n", __func__);
353+
return 1;
354+
}
355+
356+
r = read(fd, s, sizeof(s) - 1);
357+
close(fd);
358+
if (r < 0) {
359+
lwsl_err("%s: unable to read from /proc/self/cgroup\n", __func__);
360+
361+
return 1;
362+
}
363+
364+
s[r] = '\0';
365+
p = strchr(s, ':');
366+
367+
if (!p) {
368+
lwsl_err("%s: unable to find first : '%s'\n", __func__, s);
369+
return 1;
370+
}
371+
372+
p = strchr(p + 1, ':');
373+
if (!p || (r - (p - s) < 3)) {
374+
lwsl_err("%s: unable to find second : '%s'\n", __func__, s);
375+
376+
return 1;
377+
}
378+
p++;
379+
380+
if (p[strlen(p) - 1] == '\n')
381+
p[strlen(p) - 1] = '\0';
382+
383+
if ((size_t)(r - (p - s)) + 1 > max - 1) {
384+
lwsl_err("%s: cgroup name too large : '%s'\n", __func__, s);
385+
386+
return 1;
387+
}
388+
389+
memcpy(cgroup, p, (size_t)(r - (p - s) + 1));
390+
391+
return 0;
392+
}
393+
322394
/*
323395
* Deals with spawning a subprocess and executing it securely with stdin/out/err
324396
* diverted into pipes
@@ -331,7 +403,7 @@ lws_spawn_piped(const struct lws_spawn_piped_info *i)
331403
struct lws_context *context = i->vh->context;
332404
struct lws_spawn_piped *lsp;
333405
const char *wd;
334-
int n, m;
406+
int n, m, do_cgroup = 0;
335407

336408
if (i->protocol_name)
337409
pcol = lws_vhost_name_to_protocol(i->vh, i->protocol_name);
@@ -350,6 +422,13 @@ lws_spawn_piped(const struct lws_spawn_piped_info *i)
350422
lsp->info = *i;
351423
lsp->reap_retry_budget = 20;
352424

425+
#if defined(__linux__)
426+
lsp->cgroup_path[0] = '\0';
427+
#endif
428+
429+
if (i->p_cgroup_ret)
430+
*i->p_cgroup_ret = 1; /* Default to cgroup failed */
431+
353432
/*
354433
* Prepare the stdin / out / err pipes
355434
*/
@@ -445,6 +524,55 @@ lws_spawn_piped(const struct lws_spawn_piped_info *i)
445524
lsp->stdwsi[LWS_STDIN]->desc.sockfd,
446525
lsp->stdwsi[LWS_STDOUT]->desc.sockfd,
447526
lsp->stdwsi[LWS_STDERR]->desc.sockfd);
527+
528+
#if defined(__linux__)
529+
if (i->cgroup_name_suffix && i->cgroup_name_suffix[0]) {
530+
char self_cg[256];
531+
532+
if (lws_spawn_get_self_cgroup(self_cg, sizeof(self_cg) - 1))
533+
lwsl_err("%s: failed to get self cgroup\n", __func__);
534+
else {
535+
lws_snprintf(lsp->cgroup_path, sizeof(lsp->cgroup_path),
536+
"/sys/fs/cgroup%s/%s", self_cg, i->cgroup_name_suffix);
537+
538+
if (mkdir(lsp->cgroup_path, 0755)) {
539+
lwsl_warn("%s: failed to generate cgroup dir %s: errno %d\n",
540+
__func__, lsp->cgroup_path, errno);
541+
lsp->cgroup_path[0] = '\0';
542+
} else {
543+
char pth[300];
544+
int cfd;
545+
546+
lws_snprintf(pth, sizeof(pth), "%s/cgroup.type", lsp->cgroup_path);
547+
cfd = lws_open(pth, LWS_O_WRONLY);
548+
if (cfd >= 0) {
549+
if (write(cfd, "threaded", 8) != 8)
550+
lwsl_warn("%s: failed to write threaded\n", __func__);
551+
552+
close(cfd);
553+
}
554+
555+
556+
lwsl_info("%s: created cgroup %s\n", __func__, lsp->cgroup_path);
557+
lws_snprintf(pth, sizeof(pth), "%s/pids.max", lsp->cgroup_path);
558+
cfd = lws_open(pth, LWS_O_WRONLY);
559+
if (cfd >= 0) {
560+
if (write(cfd, "max", 3) != 3)
561+
lwsl_warn("%s: failed to write max\n", __func__);
562+
563+
close(cfd);
564+
}
565+
566+
do_cgroup = 1;
567+
}
568+
}
569+
}
570+
571+
if (i->p_cgroup_ret)
572+
/* Report cgroup success to caller */
573+
*i->p_cgroup_ret = !do_cgroup;
574+
575+
#endif
448576

449577
/* we are ready with the redirection pipes... do the (v)fork */
450578
#if defined(__sun) || !defined(LWS_HAVE_VFORK) || !defined(LWS_HAVE_EXECVPE)
@@ -510,6 +638,32 @@ lws_spawn_piped(const struct lws_spawn_piped_info *i)
510638
* process is OK. Stuff that happens after the execvpe() is OK.
511639
*/
512640

641+
#if defined(__linux__)
642+
if (lsp->cgroup_path[0]) {
643+
char path[300], pid_str[20];
644+
int fd, len;
645+
646+
/*
647+
* We are the new child process. We must move ourselves into
648+
* the cgroup created for us by the parent.
649+
*/
650+
lws_snprintf(path, sizeof(path) - 1, "%s/cgroup.procs", lsp->cgroup_path);
651+
fd = open(path, O_WRONLY);
652+
if (fd >= 0) {
653+
len = lws_snprintf(pid_str, sizeof(pid_str) - 1, "%d", (int)getpid());
654+
if (write(fd, pid_str, (size_t)len) != (ssize_t)len) {
655+
/*
656+
* using lwsl_err here is unsafe in vfork()
657+
* child, just exit with a special code
658+
*/
659+
_exit(121);
660+
}
661+
close(fd);
662+
} else
663+
_exit(122);
664+
}
665+
#endif
666+
513667
if (i->chroot_path && chroot(i->chroot_path)) {
514668
lwsl_err("%s: child chroot %s failed, errno %d\n",
515669
__func__, i->chroot_path, errno);
@@ -603,6 +757,21 @@ lws_spawn_stdwsi_closed(struct lws_spawn_piped *lsp, struct lws *wsi)
603757
{
604758
int n;
605759

760+
/*
761+
* This is part of the normal cleanup path, check if the lsp has already
762+
* been destroyed by a timeout or other error path. If the stdwsi that
763+
* is closing has already been nulled out, we have already been through
764+
* destroy.
765+
*/
766+
for (n = 0; n < 3; n++)
767+
if (lsp->stdwsi[n] == wsi)
768+
goto found;
769+
770+
/* Not found, so must have been destroyed already */
771+
return;
772+
773+
found:
774+
606775
assert(lsp);
607776
lsp->pipes_alive--;
608777
lwsl_debug("%s: pipes alive %d\n", __func__, lsp->pipes_alive);
@@ -621,6 +790,82 @@ lws_spawn_get_stdfd(struct lws *wsi)
621790
return wsi->lsp_channel;
622791
}
623792

793+
int
794+
lws_spawn_prepare_self_cgroup(const char *user, const char *group)
795+
{
796+
#if defined(__linux__)
797+
uid_t uid = (uid_t)-1;
798+
gid_t gid = (gid_t)-1;
799+
char path[256], self_cgroup[256];
800+
int fd;
801+
802+
if (lws_spawn_get_self_cgroup(self_cgroup, sizeof(self_cgroup) - 1)) {
803+
lwsl_err("%s: unable to get self cgroup\n", __func__);
804+
805+
return 1;
806+
}
807+
808+
lws_snprintf(path, sizeof(path), "/sys/fs/cgroup%s/cgroup.subtree_control",
809+
self_cgroup);
810+
811+
fd = lws_open(path, LWS_O_WRONLY);
812+
if (fd < 0) {
813+
/* May fail if user doesn't own the file, that's okay */
814+
lwsl_info("%s: cannot open subtree_control: %s\n",
815+
__func__, strerror(errno));
816+
return 0; /* Still a success if dir exists */
817+
}
818+
819+
if (write(fd, "+cpu +memory +pids +io", 22) != 22)
820+
/* ignore, may be there already or fail due to perms */
821+
lwsl_debug("%s: setting admin cgroup options failed\n", __func__);
822+
close(fd);
823+
824+
lws_snprintf(path, sizeof(path), "/sys/fs/cgroup%s", self_cgroup);
825+
826+
if (user) {
827+
struct passwd *pwd;
828+
829+
pwd = getpwnam(user);
830+
if (pwd)
831+
uid = pwd->pw_uid;
832+
else
833+
lwsl_warn("%s: user '%s' not found\n", __func__, user);
834+
}
835+
if (group) {
836+
struct group *grp;
837+
838+
grp = getgrnam(group);
839+
if (grp)
840+
gid = grp->gr_gid;
841+
else
842+
lwsl_warn("%s: group '%s' not found\n", __func__, group);
843+
}
844+
845+
lwsl_notice("%s: switching %s to %d:%d\n",
846+
__func__, path, uid, gid);
847+
848+
if (chown(path, uid, gid) < 0)
849+
lwsl_warn("%s: failed to chown %s: %s\n",
850+
__func__, path, strerror(errno));
851+
/* 2. ALSO change ownership of the critical control files inside it */
852+
lws_snprintf(path, sizeof(path), "/sys/fs/cgroup%s/cgroup.procs", self_cgroup);
853+
if (chown(path, uid, gid) < 0)
854+
lwsl_warn("%s: failed to chown %s: %s\n",
855+
__func__, path, strerror(errno));
856+
857+
lws_snprintf(path, sizeof(path), "/sys/fs/cgroup%s/cgroup.subtree_control", self_cgroup);
858+
if (chown(path, uid, gid) < 0)
859+
lwsl_warn("%s: failed to chown %s: %s\n",
860+
__func__, path, strerror(errno));
861+
862+
lwsl_notice("%s: lws cgroup parent configured\n", __func__);
863+
864+
return 0;
865+
#endif
866+
return 1; /* Not supported on this platform */
867+
}
868+
624869
int
625870
lws_spawn_get_fd_stdxxx(struct lws_spawn_piped *lsp, int std_idx)
626871
{

0 commit comments

Comments
 (0)