+uint64_t
+xpc_get_jetsam_entitlement(const char *key)
+{
+ uint64_t entitlement = 0;
+
+ audit_token_t *token = runtime_get_caller_token();
+ xpc_object_t value = xpc_copy_entitlement_for_token(key, token);
+ if (value) {
+ if (xpc_get_type(value) == XPC_TYPE_UINT64) {
+ entitlement = xpc_uint64_get_value(value);
+ }
+
+ xpc_release(value);
+ }
+
+ return entitlement;
+}
+
+int
+xpc_process_set_jetsam_band(job_t j, xpc_object_t request, xpc_object_t *reply)
+{
+ if (!j) {
+ return EINVAL;
+ }
+
+ const char *label = xpc_dictionary_get_string(request, XPC_PROCESS_ROUTINE_KEY_LABEL);
+ if (!label) {
+ return EXINVAL;
+ }
+
+ xpc_jetsam_band_t entitled_band = -1;
+ xpc_jetsam_band_t requested_band = (xpc_jetsam_band_t)xpc_dictionary_get_uint64(request, XPC_PROCESS_ROUTINE_KEY_PRIORITY_BAND);
+ if (!requested_band) {
+ return EXINVAL;
+ }
+
+ if (!(requested_band >= XPC_JETSAM_BAND_SUSPENDED && requested_band < XPC_JETSAM_BAND_LAST)) {
+ return EXINVAL;
+ }
+
+ uint64_t rcdata = xpc_dictionary_get_uint64(request, XPC_PROCESS_ROUTINE_KEY_RCDATA);
+
+ job_t tj = job_find(root_jobmgr, label);
+ if (!tj) {
+ return EXSRCH;
+ }
+
+ boolean_t allow = false;
+ if (j->embedded_god) {
+ allow = true;
+ } else {
+ entitled_band = xpc_get_jetsam_entitlement("com.apple.private.jetsam.modify-priority");
+ if (entitled_band >= requested_band) {
+ allow = true;
+ }
+ }
+
+ if (!allow) {
+ if (launchd_no_jetsam_perm_check) {
+ job_log(j, LOG_NOTICE, "Jetsam priority checks disabled; allowing job to set priority: %d", requested_band);
+ } else {
+ job_log(j, LOG_ERR, "Job cannot decrease Jetsam priority band (requested/maximum): %d/%d", requested_band, entitled_band);
+ return EPERM;
+ }
+ }
+
+ job_log(j, LOG_INFO, "Setting Jetsam band: %d.", requested_band);
+ job_update_jetsam_properties(tj, requested_band, rcdata);
+
+ xpc_object_t reply2 = xpc_dictionary_create_reply(request);
+ *reply = reply2;
+
+ return 0;
+}
+
+int
+xpc_process_set_jetsam_memory_limit(job_t j, xpc_object_t request, xpc_object_t *reply)
+{
+ if (!j) {
+ return EINVAL;
+ }
+
+ const char *label = xpc_dictionary_get_string(request, XPC_PROCESS_ROUTINE_KEY_LABEL);
+ if (!label) {
+ return EXINVAL;
+ }
+
+ int32_t entitlement_limit = 0;
+ int32_t requested_limit = (int32_t)xpc_dictionary_get_uint64(request, XPC_PROCESS_ROUTINE_KEY_MEMORY_LIMIT);
+
+ job_t tj = job_find(root_jobmgr, label);
+ if (!tj) {
+ return EXSRCH;
+ }
+
+ boolean_t allow = false;
+ if (j->embedded_god) {
+ allow = true;
+ } else {
+ entitlement_limit = (int32_t)xpc_get_jetsam_entitlement("com.apple.private.jetsam.memory_limit");
+ if (entitlement_limit >= requested_limit) {
+ allow = true;
+ }
+ }
+
+ if (!allow) {
+ if (launchd_no_jetsam_perm_check) {
+ job_log(j, LOG_NOTICE, "Jetsam priority checks disabled; allowing job to set memory limit: %d", requested_limit);
+ } else {
+ job_log(j, LOG_ERR, "Job cannot set Jetsam memory limit (requested/maximum): %d/%d", requested_limit, entitlement_limit);
+ return EPERM;
+ }
+ }
+
+ job_log(j, LOG_INFO, "Setting Jetsam memory limit: %d.", requested_limit);
+ job_update_jetsam_memory_limit(tj, requested_limit);
+
+ xpc_object_t reply2 = xpc_dictionary_create_reply(request);
+ *reply = reply2;
+
+ return 0;
+}
+
+static jobmgr_t
+_xpc_process_find_target_manager(job_t j, xpc_service_type_t type, pid_t pid)
+{
+ jobmgr_t target = NULL;
+ if (type == XPC_SERVICE_TYPE_BUNDLED) {
+ job_log(j, LOG_DEBUG, "Bundled service. Searching for XPC domains for PID: %d", pid);
+
+ jobmgr_t jmi = NULL;
+ SLIST_FOREACH(jmi, &root_jobmgr->submgrs, sle) {
+ if (jmi->req_pid && jmi->req_pid == pid) {
+ jobmgr_log(jmi, LOG_DEBUG, "Found job manager for PID.");
+ target = jmi;
+ break;
+ }
+ }
+ } else if (type == XPC_SERVICE_TYPE_LAUNCHD || type == XPC_SERVICE_TYPE_APP) {
+ target = j->mgr;
+ }
+
+ return target;
+}
+
+static int
+xpc_process_attach(job_t j, xpc_object_t request, xpc_object_t *reply)
+{
+ if (!j) {
+ return EINVAL;
+ }
+
+ audit_token_t *token = runtime_get_caller_token();
+ xpc_object_t entitlement = xpc_copy_entitlement_for_token(XPC_SERVICE_ENTITLEMENT_ATTACH, token);
+ if (!entitlement) {
+ job_log(j, LOG_ERR, "Job does not have entitlement: %s", XPC_SERVICE_ENTITLEMENT_ATTACH);
+ return EPERM;
+ }
+
+ if (entitlement != XPC_BOOL_TRUE) {
+ char *desc = xpc_copy_description(entitlement);
+ job_log(j, LOG_ERR, "Job has bad value for entitlement: %s:\n%s", XPC_SERVICE_ENTITLEMENT_ATTACH, desc);
+ free(desc);
+
+ xpc_release(entitlement);
+ return EPERM;
+ }
+
+ const char *name = xpc_dictionary_get_string(request, XPC_PROCESS_ROUTINE_KEY_NAME);
+ if (!name) {
+ return EXINVAL;
+ }
+
+ xpc_service_type_t type = xpc_dictionary_get_int64(request, XPC_PROCESS_ROUTINE_KEY_TYPE);
+ if (!type) {
+ return EXINVAL;
+ }
+
+ mach_port_t port = xpc_dictionary_copy_mach_send(request, XPC_PROCESS_ROUTINE_KEY_NEW_INSTANCE_PORT);
+ if (!MACH_PORT_VALID(port)) {
+ return EXINVAL;
+ }
+
+ pid_t pid = xpc_dictionary_get_int64(request, XPC_PROCESS_ROUTINE_KEY_HANDLE);
+
+ job_log(j, LOG_DEBUG, "Attaching to service: %s", name);
+
+ xpc_object_t reply2 = xpc_dictionary_create_reply(request);
+ jobmgr_t target = _xpc_process_find_target_manager(j, type, pid);
+ if (target) {
+ jobmgr_log(target, LOG_DEBUG, "Found target job manager for service: %s", name);
+ (void)jobmgr_assumes(target, waiting4attach_new(target, name, port, 0, type));
+
+ /* HACK: This is awful. For legacy reasons, launchd job labels are all
+ * stored in a global namespace, which is stored in the root job
+ * manager. But XPC domains have a per-domain namespace. So if we're
+ * looking for a legacy launchd job, we have to redirect any attachment
+ * attempts to the root job manager to find existing instances.
+ *
+ * But because we store attachments on a per-job manager basis, we have
+ * to create the new attachment in the actual target job manager, hence
+ * why we change the target only after we've created the attachment.
+ */
+ if (strcmp(target->name, VPROCMGR_SESSION_AQUA) == 0) {
+ target = root_jobmgr;
+ }
+
+ job_t existing = job_find(target, name);
+ if (existing && existing->p) {
+ job_log(existing, LOG_DEBUG, "Found existing instance of service.");
+ xpc_dictionary_set_int64(reply2, XPC_PROCESS_ROUTINE_KEY_PID, existing->p);
+ } else {
+ xpc_dictionary_set_uint64(reply2, XPC_PROCESS_ROUTINE_KEY_ERROR, ESRCH);
+ }
+ } else if (type == XPC_SERVICE_TYPE_BUNDLED) {
+ (void)job_assumes(j, waiting4attach_new(target, name, port, pid, type));
+ xpc_dictionary_set_uint64(reply2, XPC_PROCESS_ROUTINE_KEY_ERROR, ESRCH);
+ } else {
+ xpc_dictionary_set_uint64(reply2, XPC_PROCESS_ROUTINE_KEY_ERROR, EXSRCH);
+ }
+
+ *reply = reply2;
+ return 0;
+}
+
+static int
+xpc_process_detach(job_t j, xpc_object_t request, xpc_object_t *reply __unused)
+{
+ if (!j) {
+ return EINVAL;
+ }
+
+ const char *name = xpc_dictionary_get_string(request, XPC_PROCESS_ROUTINE_KEY_NAME);
+ if (!name) {
+ return EXINVAL;
+ }
+
+ xpc_service_type_t type = xpc_dictionary_get_int64(request, XPC_PROCESS_ROUTINE_KEY_TYPE);
+ if (!type) {
+ return EXINVAL;
+ }
+
+ job_log(j, LOG_DEBUG, "Deatching from service: %s", name);
+
+ pid_t pid = xpc_dictionary_get_int64(request, XPC_PROCESS_ROUTINE_KEY_PID);
+ jobmgr_t target = _xpc_process_find_target_manager(j, type, pid);
+ if (target) {
+ jobmgr_log(target, LOG_DEBUG, "Found target job manager for service: %s", name);
+
+ struct waiting4attach *w4ai = NULL;
+ struct waiting4attach *w4ait = NULL;
+ LIST_FOREACH_SAFE(w4ai, &target->attaches, le, w4ait) {
+ if (strcmp(name, w4ai->name) == 0) {
+ jobmgr_log(target, LOG_DEBUG, "Found attachment. Deleting.");
+ waiting4attach_delete(target, w4ai);
+ break;
+ }
+ }
+ }
+
+ return 0;
+}
+
+static int
+xpc_process_get_properties(job_t j, xpc_object_t request, xpc_object_t *reply)
+{
+ if (j->anonymous) {
+ /* Total hack. libxpc will send requests to the pipe created out of the
+ * process' bootstrap port, so when job_mig_intran() tries to resolve
+ * the process into a job, it'll wind up creating an anonymous job if
+ * the requestor was an XPC service, whose job manager is an XPC domain.
+ */
+ pid_t pid = j->p;
+ jobmgr_t jmi = NULL;
+ SLIST_FOREACH(jmi, &root_jobmgr->submgrs, sle) {
+ if ((j = jobmgr_find_by_pid(jmi, pid, false))) {
+ break;
+ }
+ }
+ }
+
+ if (!j || j->anonymous) {
+ return EXINVAL;
+ }
+
+ struct waiting4attach *w4a = waiting4attach_find(j->mgr, j);
+ if (!w4a) {
+ return EXINVAL;
+ }
+
+ xpc_object_t reply2 = xpc_dictionary_create_reply(request);
+ xpc_dictionary_set_uint64(reply2, XPC_PROCESS_ROUTINE_KEY_TYPE, w4a->type);
+ xpc_dictionary_set_mach_send(reply2, XPC_PROCESS_ROUTINE_KEY_NEW_INSTANCE_PORT, w4a->port);
+ if (j->prog) {
+ xpc_dictionary_set_string(reply2, XPC_PROCESS_ROUTINE_KEY_PATH, j->prog);
+ } else {
+ xpc_dictionary_set_string(reply2, XPC_PROCESS_ROUTINE_KEY_PATH, j->argv[0]);
+ }
+
+ if (j->argv) {
+ xpc_object_t xargv = xpc_array_create(NULL, 0);
+
+ size_t i = 0;
+ for (i = 0; i < j->argc; i++) {
+ if (j->argv[i]) {
+ xpc_array_set_string(xargv, XPC_ARRAY_APPEND, j->argv[i]);
+ }
+ }
+
+ xpc_dictionary_set_value(reply2, XPC_PROCESS_ROUTINE_KEY_ARGV, xargv);
+ xpc_release(xargv);
+ }
+
+ *reply = reply2;
+ return 0;
+}
+
+static int
+xpc_process_service_kill(job_t j, xpc_object_t request, xpc_object_t *reply)
+{
+#if XPC_LPI_VERSION >= 20130426
+ if (!j) {
+ return ESRCH;
+ }
+
+ jobmgr_t jm = _xpc_process_find_target_manager(j, XPC_SERVICE_TYPE_BUNDLED, j->p);
+ if (!jm) {
+ return ENOENT;
+ }
+
+ const char *name = xpc_dictionary_get_string(request, XPC_PROCESS_ROUTINE_KEY_NAME);
+ if (!name) {
+ return EINVAL;
+ }
+
+ int64_t whichsig = xpc_dictionary_get_int64(request, XPC_PROCESS_ROUTINE_KEY_SIGNAL);
+ if (!whichsig) {
+ return EINVAL;
+ }
+
+ job_t j2kill = job_find(jm, name);
+ if (!j2kill) {
+ return ESRCH;
+ }
+
+ if (j2kill->alias) {
+ // Only allow for private instances to be killed.
+ return EPERM;
+ }
+
+ struct proc_bsdshortinfo proc;
+ if (proc_pidinfo(j2kill->p, PROC_PIDT_SHORTBSDINFO, 1, &proc, PROC_PIDT_SHORTBSDINFO_SIZE) == 0) {
+ if (errno != ESRCH) {
+ (void)jobmgr_assumes_zero(root_jobmgr, errno);
+ }
+
+ return errno;
+ }
+
+ struct ldcred *ldc = runtime_get_caller_creds();
+ if (proc.pbsi_uid != ldc->euid) {
+ // Do not allow non-root to kill RoleAccount services running as a
+ // different user.
+ return EPERM;
+ }
+
+ if (!j2kill->p) {
+ return EALREADY;
+ }
+
+ xpc_object_t reply2 = xpc_dictionary_create_reply(request);
+ if (!reply2) {
+ return EINVAL;
+ }
+
+ int error = 0;
+ int ret = kill(j2kill->p, whichsig);
+ if (ret) {
+ error = errno;
+ }
+
+ xpc_dictionary_set_int64(reply2, XPC_PROCESS_ROUTINE_KEY_ERROR, error);
+ *reply = reply2;
+ return 0;
+#else
+ return ENOTSUP;
+#endif
+}
+
+bool
+xpc_process_demux(mach_port_t p, xpc_object_t request, xpc_object_t *reply)
+{
+ uint64_t op = xpc_dictionary_get_uint64(request, XPC_PROCESS_ROUTINE_KEY_OP);
+ if (!op) {
+ return false;
+ }
+
+ audit_token_t token;
+ xpc_dictionary_get_audit_token(request, &token);
+ runtime_record_caller_creds(&token);
+
+ job_t j = job_mig_intran(p);
+ job_log(j, LOG_DEBUG, "Incoming XPC process request: %llu", op);
+
+ int error = -1;
+ switch (op) {
+ case XPC_PROCESS_JETSAM_SET_BAND:
+ error = xpc_process_set_jetsam_band(j, request, reply);
+ break;
+ case XPC_PROCESS_JETSAM_SET_MEMORY_LIMIT:
+ error = xpc_process_set_jetsam_memory_limit(j, request, reply);
+ break;
+ case XPC_PROCESS_SERVICE_ATTACH:
+ error = xpc_process_attach(j, request, reply);
+ break;
+ case XPC_PROCESS_SERVICE_DETACH:
+ error = xpc_process_detach(j, request, reply);
+ break;
+ case XPC_PROCESS_SERVICE_GET_PROPERTIES:
+ error = xpc_process_get_properties(j, request, reply);
+ break;
+ case XPC_PROCESS_SERVICE_KILL:
+ error = xpc_process_service_kill(j, request, reply);
+ break;
+ default:
+ job_log(j, LOG_ERR, "Bogus process opcode.");
+ error = EDOM;
+ }
+
+ if (error) {
+ xpc_object_t reply2 = xpc_dictionary_create_reply(request);
+ if (reply2) {
+ xpc_dictionary_set_uint64(reply2, XPC_PROCESS_ROUTINE_KEY_ERROR, error);
+ }
+
+ *reply = reply2;
+ }
+
+ return true;
+}
+