Changeset 23264
- Timestamp:
- 05/25/07 13:44:01 (19 months ago)
- Location:
- trunk/launchd/src
- Files:
-
- 6 modified
-
launchd_core_logic.c (modified) (22 diffs)
-
launchd_mig_types.defs (modified) (1 diff)
-
liblaunch_private.h (modified) (3 diffs)
-
libvproc.c (modified) (5 diffs)
-
libvproc_internal.h (modified) (1 diff)
-
protocol_job.defs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
trunk/launchd/src/launchd_core_logic.c
r23262 r23264 73 73 #include <glob.h> 74 74 #include <spawn.h> 75 #include <sandbox.h> 75 76 76 77 #include "liblaunch_public.h" … … 195 196 static void limititem_delete(job_t j, struct limititem *li); 196 197 static void limititem_setup(launch_data_t obj, const char *key, void *context); 198 static void seatbelt_setup_flags(launch_data_t obj, const char *key, void *context); 197 199 198 200 typedef enum { … … 298 300 struct rusage ru; 299 301 #endif 300 binpref_tj_binpref;302 cpu_type_t *j_binpref; 301 303 size_t j_binpref_cnt; 302 304 mach_port_t j_port; … … 314 316 struct machservice *lastlookup; 315 317 unsigned int lastlookup_gennum; 318 char *seatbelt_profile; 319 uint64_t seatbelt_flags; 320 void *quarantine_data; 321 size_t quarantine_data_sz; 316 322 pid_t p; 317 323 int argc; … … 355 361 static void job_import_dictionary(job_t j, const char *key, launch_data_t value); 356 362 static void job_import_array(job_t j, const char *key, launch_data_t value); 363 static void job_import_opaque(job_t j, const char *key, launch_data_t value); 357 364 static bool job_set_global_on_demand(job_t j, bool val); 358 365 static void job_watch(job_t j); … … 377 384 static job_t job_new_anonymous(jobmgr_t jm, pid_t anonpid); 378 385 static job_t job_new(jobmgr_t jm, const char *label, const char *prog, const char *const *argv); 379 static job_t job_new_spawn(job_t j, const char *label, const char *path, const char *workingdir, const char *const *argv, const char *const *env, mode_t *u_mask, bool w4d);380 386 static job_t job_new_via_mach_init(job_t j, const char *cmd, uid_t uid, bool ond); 381 387 static const char *job_prog(job_t j); … … 774 780 free(j->stderrpath); 775 781 } 782 if (j->seatbelt_profile) { 783 free(j->seatbelt_profile); 784 } 785 if (j->quarantine_data) { 786 free(j->quarantine_data); 787 } 776 788 if (j->start_interval) { 777 789 job_assumes(j, kevent_mod((uintptr_t)&j->start_interval, EVFILT_TIMER, EV_DELETE, 0, 0, NULL) != -1); … … 920 932 921 933 return 0; 922 }923 924 job_t925 job_new_spawn(job_t j, const char *label, const char *path, const char *workingdir, const char *const *argv, const char *const *env, mode_t *u_mask, bool w4d)926 {927 job_t jr;928 size_t i;929 930 if ((jr = job_find(label)) != NULL) {931 errno = EEXIST;932 return NULL;933 }934 935 jr = job_new(j->mgr, label, path, argv);936 937 if (!jr) {938 return NULL;939 }940 941 job_reparent_hack(jr, NULL);942 943 if (getpid() == 1) {944 struct ldcred ldc;945 946 runtime_get_caller_creds(&ldc);947 jr->mach_uid = ldc.uid;948 }949 950 jr->unload_at_exit = true;951 jr->wait4pipe_eof = true;952 jr->stall_before_exec = w4d;953 954 if (workingdir) {955 jr->workingdir = strdup(workingdir);956 }957 958 if (u_mask) {959 jr->mask = *u_mask;960 jr->setmask = true;961 }962 963 if (env) for (i = 0; env[i]; i++) {964 char *eqoff, tmpstr[strlen(env[i]) + 1];965 966 strcpy(tmpstr, env[i]);967 968 eqoff = strchr(tmpstr, '=');969 970 if (!eqoff) {971 job_log(jr, LOG_WARNING, "Environmental variable missing '=' separator: %s", tmpstr);972 continue;973 }974 975 *eqoff = '\0';976 envitem_new(jr, tmpstr, eqoff + 1, false);977 }978 979 return jr;980 934 } 981 935 … … 1358 1312 } else if (strcasecmp(key, LAUNCH_JOBKEY_STANDARDERRORPATH) == 0) { 1359 1313 where2put = &j->stderrpath; 1314 } else if (strcasecmp(key, LAUNCH_JOBKEY_SANDBOXPROFILE) == 0) { 1315 where2put = &j->seatbelt_profile; 1360 1316 } 1361 1317 break; … … 1427 1383 job_log_error(j, LOG_ERR, "adding kevent timer"); 1428 1384 } 1429 } 1385 } else if (strcasecmp(key, LAUNCH_JOBKEY_SANDBOXFLAGS) == 0) { 1386 j->seatbelt_flags = value; 1387 } 1388 1430 1389 break; 1431 1390 default: 1432 1391 job_log(j, LOG_WARNING, "Unknown key for integer: %s", key); 1392 break; 1393 } 1394 } 1395 1396 void 1397 job_import_opaque(job_t j, const char *key, launch_data_t value) 1398 { 1399 switch (key[0]) { 1400 case 'q': 1401 case 'Q': 1402 if (strcasecmp(key, LAUNCH_JOBKEY_QUARANTINEDATA) == 0) { 1403 size_t tmpsz = launch_data_get_opaque_size(value); 1404 1405 if (job_assumes(j, j->quarantine_data = malloc(tmpsz))) { 1406 memcpy(j->quarantine_data, launch_data_get_opaque(value), tmpsz); 1407 j->quarantine_data_sz = tmpsz; 1408 } 1409 } 1410 break; 1411 default: 1433 1412 break; 1434 1413 } … … 1478 1457 } else if (strcasecmp(key, LAUNCH_JOBKEY_SOFTRESOURCELIMITS) == 0) { 1479 1458 launch_data_dict_iterate(value, limititem_setup, j); 1459 } else if (strcasecmp(key, LAUNCH_JOBKEY_SANDBOXFLAGS) == 0) { 1460 launch_data_dict_iterate(value, seatbelt_setup_flags, j); 1480 1461 } 1481 1462 break; … … 1553 1534 if (strcasecmp(key, LAUNCH_JOBKEY_BONJOURFDS) == 0) { 1554 1535 socketgroup_setup(value, LAUNCH_JOBKEY_BONJOURFDS, j); 1536 } else if (strcasecmp(key, LAUNCH_JOBKEY_BINARYORDERPREFERENCE) == 0) { 1537 if (job_assumes(j, j->j_binpref = malloc(value_cnt * sizeof(*j->j_binpref)))) { 1538 j->j_binpref_cnt = value_cnt; 1539 for (i = 0; i < value_cnt; i++) { 1540 j->j_binpref[i] = launch_data_get_integer(launch_data_array_get_index(value, i)); 1541 } 1542 } 1555 1543 } 1556 1544 break; … … 1596 1584 case LAUNCH_DATA_ARRAY: 1597 1585 job_import_array(j, key, obj); 1586 break; 1587 case LAUNCH_DATA_OPAQUE: 1588 job_import_opaque(j, key, obj); 1598 1589 break; 1599 1590 default: … … 2375 2366 } 2376 2367 2368 if (j->quarantine_data) { 2369 qtn_proc_t qp; 2370 2371 if (job_assumes(j, qp = qtn_proc_alloc())) { 2372 if (job_assumes(j, qtn_proc_init_with_data(qp, j->quarantine_data, j->quarantine_data_sz) == 0)) { 2373 job_assumes(j, qtn_proc_apply_to_self(qp) == 0); 2374 } 2375 } 2376 } 2377 2378 if (j->seatbelt_profile) { 2379 char *seatbelt_err_buf = NULL; 2380 2381 if (!job_assumes(j, sandbox_init(j->seatbelt_profile, j->seatbelt_flags, &seatbelt_err_buf) != -1)) { 2382 if (seatbelt_err_buf) { 2383 job_log(j, LOG_ERR, "Sandbox failed to init: %s", seatbelt_err_buf); 2384 } 2385 goto out_bad; 2386 } 2387 } 2388 2377 2389 if (j->prog) { 2378 2390 errno = posix_spawn(&junk_pid, j->inetcompat ? file2exec : j->prog, NULL, &spattr, (char *const*)argv, environ); … … 2383 2395 } 2384 2396 2397 out_bad: 2385 2398 _exit(EXIT_FAILURE); 2386 2399 } … … 3229 3242 3230 3243 free(li); 3244 } 3245 3246 void 3247 seatbelt_setup_flags(launch_data_t obj, const char *key, void *context) 3248 { 3249 job_t j = context; 3250 3251 if (launch_data_get_type(obj) != LAUNCH_DATA_BOOL) { 3252 job_log(j, LOG_WARNING, "Sandbox flag value must be boolean: %s", key); 3253 return; 3254 } 3255 3256 if (launch_data_get_bool(obj) == false) { 3257 return; 3258 } 3259 3260 if (strcasecmp(key, LAUNCH_JOBKEY_SANDBOX_NAMED) == 0) { 3261 j->seatbelt_flags |= SANDBOX_NAMED; 3262 } 3231 3263 } 3232 3264 … … 5321 5353 kr = _vproc_grab_subset(target_subset, &reqport, &rcvright, &out_obj_array, &l2l_ports, &l2l_port_cnt); 5322 5354 5323 job_log(j, LOG_NOTICE, "@@@@@ kr == 0x%x", kr);5324 5325 5355 if (!job_assumes(j, kr == 0)) { 5326 5356 goto out; … … 5344 5374 job_assumes(j, obj_at_idx = launch_data_array_get_index(out_obj_array, l2l_i)); 5345 5375 job_assumes(j, tmp = launch_data_dict_lookup(obj_at_idx, TAKE_SUBSET_PID)); 5346 job_assumes(j, target_pid = launch_data_get_integer(tmp));5376 target_pid = launch_data_get_integer(tmp); 5347 5377 job_assumes(j, tmp = launch_data_dict_lookup(obj_at_idx, TAKE_SUBSET_PERPID)); 5348 job_assumes(j, serv_perpid = launch_data_get_bool(tmp));5378 serv_perpid = launch_data_get_bool(tmp); 5349 5379 job_assumes(j, tmp = launch_data_dict_lookup(obj_at_idx, TAKE_SUBSET_NAME)); 5350 job_assumes(j, serv_name = launch_data_get_string(tmp));5380 serv_name = launch_data_get_string(tmp); 5351 5381 5352 5382 j_for_service = jobmgr_find_by_pid(jmr, target_pid, true); … … 5648 5678 5649 5679 kern_return_t 5650 job_mig_spawn(job_t j, _internal_string_t charbuf, mach_msg_type_number_t charbuf_cnt, 5651 uint32_t argc, uint32_t envc, uint64_t flags, uint16_t mig_umask, 5652 binpref_t bin_pref, uint32_t binpref_cnt, pid_t *child_pid, mach_port_t *obsvr_port) 5653 { 5680 job_mig_spawn(job_t j, vm_offset_t indata, mach_msg_type_number_t indataCnt, pid_t *child_pid, mach_port_t *obsvr_port) 5681 { 5682 launch_data_t input_obj = NULL; 5683 size_t data_offset = 0; 5684 struct ldcred ldc; 5654 5685 job_t jr; 5655 size_t offset = 0;5656 char *tmpp;5657 const char **argv = NULL, **env = NULL;5658 const char *label = NULL;5659 const char *path = NULL;5660 const char *workingdir = NULL;5661 size_t argv_i = 0, env_i = 0;5662 struct ldcred ldc;5663 5686 5664 5687 runtime_get_caller_creds(&ldc); … … 5673 5696 } 5674 5697 5675 argv = alloca((argc + 1) * sizeof(char *)); 5676 memset(argv, 0, (argc + 1) * sizeof(char *)); 5677 5678 if (envc > 0) { 5679 env = alloca((envc + 1) * sizeof(char *)); 5680 memset(env, 0, (envc + 1) * sizeof(char *)); 5681 } 5682 5683 while (offset < charbuf_cnt) { 5684 tmpp = charbuf + offset; 5685 offset += strlen(tmpp) + 1; 5686 if (!label) { 5687 label = tmpp; 5688 } else if (argc > 0) { 5689 argv[argv_i] = tmpp; 5690 argv_i++; 5691 argc--; 5692 } else if (envc > 0) { 5693 env[env_i] = tmpp; 5694 env_i++; 5695 envc--; 5696 } else if (flags & SPAWN_HAS_PATH) { 5697 path = tmpp; 5698 flags &= ~SPAWN_HAS_PATH; 5699 } else if (flags & SPAWN_HAS_WDIR) { 5700 workingdir = tmpp; 5701 flags &= ~SPAWN_HAS_WDIR; 5702 } 5703 } 5704 5705 jr = job_new_spawn(j, label, path, workingdir, argv, env, flags & SPAWN_HAS_UMASK ? &mig_umask : NULL, 5706 flags & SPAWN_WANTS_WAIT4DEBUGGER); 5698 if (indataCnt == 0) { 5699 return 1; 5700 } 5701 5702 if (!job_assumes(j, (input_obj = launch_data_unpack((void *)indata, indataCnt, NULL, 0, &data_offset, NULL)) != NULL)) { 5703 return 1; 5704 } 5705 5706 jr = job_import2(input_obj); 5707 5707 5708 5708 if (jr == NULL) switch (errno) { … … 5713 5713 } 5714 5714 5715 memcpy(jr->j_binpref, bin_pref, sizeof(jr->j_binpref)); 5716 jr->j_binpref_cnt = binpref_cnt; 5715 job_reparent_hack(jr, NULL); 5716 5717 if (getpid() == 1) { 5718 jr->mach_uid = ldc.uid; 5719 } 5720 5721 jr->unload_at_exit = true; 5722 jr->wait4pipe_eof = true; 5723 jr->stall_before_exec = jr->wait4debugger; 5724 jr->wait4debugger = false; 5717 5725 5718 5726 jr = job_dispatch(jr, true); … … 5727 5735 } 5728 5736 5729 job_log(j, LOG_INFO, "Spawned with flags:%s", flags & SPAWN_WANTS_WAIT4DEBUGGER ? " stopped": "");5737 job_log(j, LOG_INFO, "Spawned"); 5730 5738 5731 5739 *child_pid = job_get_pid(jr); 5732 5740 *obsvr_port = jr->j_port; 5733 5741 5734 mig_deallocate( (vm_address_t)charbuf, charbuf_cnt);5742 mig_deallocate(indata, indataCnt); 5735 5743 5736 5744 return BOOTSTRAP_SUCCESS; -
trunk/launchd/src/launchd_mig_types.defs
r23161 r23264 31 31 type logmsg_t = c_string[*:2048]; 32 32 type cmd_t = c_string[512]; 33 type cmd_array_t = ^array [] of cmd_t;34 33 type name_t = c_string[128]; 35 34 type name_array_t = ^array [] of name_t; 36 type _internal_string_t = ^array [] of char;37 35 type bootstrap_status_t = integer_t; 38 36 type bootstrap_status_array_t = ^array [] of bootstrap_status_t; 39 type binpref_t = array [8] of integer_t;40 37 41 38 type job_t = mach_port_t -
trunk/launchd/src/liblaunch_private.h
r23252 r23264 25 25 #include <launch.h> 26 26 #include <unistd.h> 27 #include <quarantine.h> 27 28 28 29 #pragma GCC visibility push(default) … … 46 47 #define LAUNCH_KEY_BATCHQUERY "BatchQuery" 47 48 49 #define LAUNCH_JOBKEY_QUARANTINEDATA "QuarantineData" 50 #define LAUNCH_JOBKEY_SANDBOXPROFILE "SandboxProfile" 51 #define LAUNCH_JOBKEY_SANDBOXFLAGS "SandboxFlags" 52 #define LAUNCH_JOBKEY_SANDBOX_NAMED "Named" 53 48 54 #define LAUNCH_JOBKEY_ENTERKERNELDEBUGGERBEFOREKILL "EnterKernelDebuggerBeforeKill" 49 55 #define LAUNCH_JOBKEY_PERJOBMACHSERVICES "PerJobMachServices" 50 56 #define LAUNCH_JOBKEY_SERVICEIPC "ServiceIPC" 57 #define LAUNCH_JOBKEY_BINARYORDERPREFERENCE "BinaryOrderPreference" 51 58 52 59 #define LAUNCH_JOBKEY_MACH_KUNCSERVER "kUNCServer" … … 107 114 const cpu_type_t * spawn_binpref; 108 115 size_t spawn_binpref_cnt; 116 const qtn_proc_t spawn_quarantine; 117 const char * spawn_seatbelt_profile; 118 const uint64_t * spawn_seatbelt_flags; 109 119 }; 110 120 111 #define spawn_via_launchd(a, b, c) _spawn_via_launchd(a, b, c, 1)121 #define spawn_via_launchd(a, b, c) _spawn_via_launchd(a, b, c, 2) 112 122 pid_t _spawn_via_launchd( 113 123 const char *label, -
trunk/launchd/src/libvproc.c
r23262 r23264 173 173 _spawn_via_launchd(const char *label, const char *const *argv, const struct spawn_via_launchd_attr *spawn_attrs, int struct_version) 174 174 { 175 kern_return_t kr; 175 size_t i, good_enough_size = 10*1024*1024; 176 mach_msg_type_number_t indata_cnt = 0; 177 vm_offset_t indata = 0; 178 mach_port_t obsvr_port = MACH_PORT_NULL; 179 launch_data_t tmp, tmp_array, in_obj; 176 180 const char *const *tmpp; 177 size_t len, buf_len = strlen(label) + 1; 178 char *buf = strdup(label); 179 uint64_t flags = 0; 180 uint32_t argc = 0; 181 uint32_t envc = 0; 182 binpref_t bin_pref; 183 size_t binpref_cnt = 0, binpref_max = sizeof(bin_pref) / sizeof(bin_pref[0]); 181 kern_return_t kr = 1; 182 void *buf = NULL; 184 183 pid_t p = -1; 185 mode_t u_mask = CMASK; 186 mach_port_t obsvr_port = MACH_PORT_NULL; 187 188 memset(&bin_pref, 0, sizeof(bin_pref)); 189 190 for (tmpp = argv; *tmpp; tmpp++) { 191 argc++; 192 len = strlen(*tmpp) + 1; 193 buf = reallocf(buf, buf_len + len); 194 strcpy(buf + buf_len, *tmpp); 195 buf_len += len; 196 } 184 185 if ((in_obj = launch_data_alloc(LAUNCH_DATA_DICTIONARY)) == NULL) { 186 goto out; 187 } 188 189 if ((tmp = launch_data_new_string(label)) == NULL) { 190 goto out; 191 } 192 193 launch_data_dict_insert(in_obj, tmp, LAUNCH_JOBKEY_LABEL); 194 195 if ((tmp_array = launch_data_alloc(LAUNCH_DATA_ARRAY)) == NULL) { 196 goto out; 197 } 198 199 for (i = 0; *argv; i++, argv++) { 200 tmp = launch_data_new_string(*argv); 201 if (tmp == NULL) { 202 goto out; 203 } 204 205 launch_data_array_set_index(tmp_array, tmp, i); 206 } 207 208 launch_data_dict_insert(in_obj, tmp_array, LAUNCH_JOBKEY_PROGRAMARGUMENTS); 197 209 198 210 if (spawn_attrs) switch (struct_version) { 211 case 2: 212 if (spawn_attrs->spawn_quarantine) { 213 char qbuf[QTN_SERIALIZED_DATA_MAX]; 214 size_t qbuf_sz = QTN_SERIALIZED_DATA_MAX; 215 216 if (qtn_proc_to_data(spawn_attrs->spawn_quarantine, qbuf, &qbuf_sz) == 0) { 217 tmp = launch_data_new_opaque(qbuf, qbuf_sz); 218 launch_data_dict_insert(in_obj, tmp, LAUNCH_JOBKEY_QUARANTINEDATA); 219 } 220 } 221 222 if (spawn_attrs->spawn_seatbelt_profile) { 223 tmp = launch_data_new_string(spawn_attrs->spawn_seatbelt_profile); 224 launch_data_dict_insert(in_obj, tmp, LAUNCH_JOBKEY_SANDBOXPROFILE); 225 } 226 227 if (spawn_attrs->spawn_seatbelt_flags) { 228 tmp = launch_data_new_integer(*spawn_attrs->spawn_seatbelt_flags); 229 launch_data_dict_insert(in_obj, tmp, LAUNCH_JOBKEY_SANDBOXFLAGS); 230 } 231 232 /* fall through */ 199 233 case 1: 200 234 if (spawn_attrs->spawn_binpref) { 201 if (spawn_attrs->spawn_binpref_cnt < binpref_max) { 202 binpref_max = spawn_attrs->spawn_binpref_cnt; 235 tmp_array = launch_data_alloc(LAUNCH_DATA_ARRAY); 236 for (i = 0; i < spawn_attrs->spawn_binpref_cnt; i++) { 237 tmp = launch_data_new_integer(spawn_attrs->spawn_binpref[i]); 238 launch_data_array_set_index(tmp_array, tmp, i); 203 239 } 204 205 for (; binpref_cnt < binpref_max; binpref_cnt++) { 206 bin_pref[binpref_cnt] = spawn_attrs->spawn_binpref[binpref_cnt]; 207 } 208 } 209 240 launch_data_dict_insert(in_obj, tmp_array, LAUNCH_JOBKEY_BINARYORDERPREFERENCE); 241 } 242 /* fall through */ 210 243 case 0: 211 244 if (spawn_attrs->spawn_flags & SPAWN_VIA_LAUNCHD_STOPPED) { 212 flags |= SPAWN_WANTS_WAIT4DEBUGGER; 245 tmp = launch_data_new_bool(true); 246 launch_data_dict_insert(in_obj, tmp, LAUNCH_JOBKEY_WAITFORDEBUGGER); 213 247 } 214 248 215 249 if (spawn_attrs->spawn_env) { 250 launch_data_t tmp_dict = launch_data_alloc(LAUNCH_DATA_DICTIONARY); 251 216 252 for (tmpp = spawn_attrs->spawn_env; *tmpp; tmpp++) { 217 envc++; 218 len = strlen(*tmpp) + 1; 219 buf = reallocf(buf, buf_len + len); 220 strcpy(buf + buf_len, *tmpp); 221 buf_len += len; 253 char *eqoff, tmpstr[strlen(*tmpp) + 1]; 254 255 strcpy(tmpstr, *tmpp); 256 257 eqoff = strchr(tmpstr, '='); 258 259 if (!eqoff) { 260 goto out; 261 } 262 263 *eqoff = '\0'; 264 265 launch_data_dict_insert(tmp_dict, launch_data_new_string(eqoff + 1), tmpstr); 222 266 } 267 268 launch_data_dict_insert(in_obj, tmp_dict, LAUNCH_JOBKEY_ENVIRONMENTVARIABLES); 223 269 } 224 270 225 271 if (spawn_attrs->spawn_path) { 226 flags |= SPAWN_HAS_PATH; 227 len = strlen(spawn_attrs->spawn_path) + 1; 228 buf = reallocf(buf, buf_len + len); 229 strcpy(buf + buf_len, spawn_attrs->spawn_path); 230 buf_len += len; 272 tmp = launch_data_new_string(spawn_attrs->spawn_path); 273 launch_data_dict_insert(in_obj, tmp, LAUNCH_JOBKEY_PROGRAM); 231 274 } 232 275 233 276 if (spawn_attrs->spawn_chdir) { 234 flags |= SPAWN_HAS_WDIR; 235 len = strlen(spawn_attrs->spawn_chdir) + 1; 236 buf = reallocf(buf, buf_len + len); 237 strcpy(buf + buf_len, spawn_attrs->spawn_chdir); 238 buf_len += len; 277 tmp = launch_data_new_string(spawn_attrs->spawn_chdir); 278 launch_data_dict_insert(in_obj, tmp, LAUNCH_JOBKEY_WORKINGDIRECTORY); 239 279 } 240 280 241 281 if (spawn_attrs->spawn_umask) { 242 flags |= SPAWN_HAS_UMASK;243 u_mask = *spawn_attrs->spawn_umask;282 tmp = launch_data_new_integer(*spawn_attrs->spawn_umask); 283 launch_data_dict_insert(in_obj, tmp, LAUNCH_JOBKEY_UMASK); 244 284 } 245 285 … … 249 289 } 250 290 251 kr = vproc_mig_spawn(bootstrap_port, buf, buf_len, argc, envc, flags, u_mask, bin_pref, binpref_cnt, &p, &obsvr_port); 291 if (!(buf = malloc(good_enough_size))) { 292 goto out; 293 } 294 295

