| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "common.h" |
| 4 | #include "sentry-native/sentry-native.h" |
| 5 | #include <sched.h> |
| 6 | |
| 7 | char *pidfile = NULL; |
| 8 | |
| 9 | static void fix_directory_file_permissions(const char *dirname, uid_t uid, gid_t gid, bool recursive) |
| 10 | { |
| 11 | char filename[FILENAME_MAX + 1]; |
| 12 | |
| 13 | DIR *dir = opendir(dirname); |
| 14 | if (!dir) |
| 15 | return; |
| 16 | |
| 17 | struct dirent *de = NULL; |
| 18 | |
| 19 | while ((de = readdir(dir))) { |
| 20 | if (de->d_type == DT_DIR && (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))) |
| 21 | continue; |
| 22 | |
| 23 | (void) snprintfz(filename, FILENAME_MAX, "%s/%s", dirname, de->d_name); |
| 24 | if (de->d_type == DT_REG || recursive) { |
| 25 | if (chown(filename, uid, gid) == -1) |
| 26 | netdata_log_error("Cannot chown %s '%s' to %u:%u", de->d_type == DT_DIR ? "directory" : "file", filename, (unsigned int)uid, (unsigned int)gid); |
| 27 | } |
| 28 | |
| 29 | if (de->d_type == DT_DIR && recursive) |
| 30 | fix_directory_file_permissions(filename, uid, gid, recursive); |
| 31 | } |
| 32 | |
| 33 | closedir(dir); |
| 34 | } |
| 35 | |
| 36 | static void change_dir_ownership(const char *dir, uid_t uid, gid_t gid, bool recursive) { |
| 37 | if(!dir || !*dir) return; |
| 38 | |
| 39 | if (chown(dir, uid, gid) == -1) |
| 40 | netdata_log_error("Cannot chown directory '%s' to %u:%u", dir, (unsigned int)uid, (unsigned int)gid); |
| 41 | |
| 42 | fix_directory_file_permissions(dir, uid, gid, recursive); |
| 43 | } |
| 44 | |
| 45 | static inline void clean_directory(const char *dirname) |
| 46 | { |
| 47 | DIR *dir = opendir(dirname); |
| 48 | if(!dir) return; |
| 49 | |
| 50 | int dir_fd = dirfd(dir); |
| 51 | struct dirent *de = NULL; |
| 52 | |
| 53 | while((de = readdir(dir))) |
| 54 | if(de->d_type == DT_REG) |
| 55 | if (unlinkat(dir_fd, de->d_name, 0)) |
| 56 | netdata_log_error("Cannot delete %s/%s", dirname, de->d_name); |
| 57 | |
| 58 | closedir(dir); |
| 59 | } |
| 60 | |
| 61 | static void prepare_required_directories(uid_t uid, gid_t gid) { |
| 62 | change_dir_ownership(os_run_dir(true), uid, gid, false); |
| 63 | change_dir_ownership(netdata_configured_cache_dir, uid, gid, true); |
| 64 | change_dir_ownership(netdata_configured_varlib_dir, uid, gid, false); |
| 65 | change_dir_ownership(netdata_configured_log_dir, uid, gid, false); |
| 66 | change_dir_ownership(netdata_configured_cloud_dir, uid, gid, false); |
| 67 | |
| 68 | char filename[FILENAME_MAX + 1]; |
| 69 | snprintfz(filename, FILENAME_MAX, "%s/registry", netdata_configured_varlib_dir); |
| 70 | change_dir_ownership(filename, uid, gid, false); |
| 71 | } |
| 72 | |
| 73 | static int become_user(const char *username, int pid_fd) { |
| 74 | int am_i_root = (getuid() == 0)?1:0; |
| 75 | |
| 76 | struct passwd *pw = getpwnam(username); |
| 77 | if(!pw) { |
| 78 | netdata_log_error("User %s is not present.", username); |
| 79 | return -1; |
| 80 | } |
| 81 | |
| 82 | uid_t uid = pw->pw_uid; |
| 83 | gid_t gid = pw->pw_gid; |
| 84 | |
| 85 | prepare_required_directories(uid, gid); |
| 86 | |
| 87 | if(pidfile && *pidfile) { |
| 88 | if(chown(pidfile, uid, gid) == -1) |
| 89 | netdata_log_error("Cannot chown '%s' to %u:%u", pidfile, (unsigned int)uid, (unsigned int)gid); |
| 90 | } |
| 91 | |
| 92 | int ngroups = (int)sysconf(_SC_NGROUPS_MAX); |
| 93 | gid_t *supplementary_groups = NULL; |
| 94 | if(ngroups > 0) { |
| 95 | supplementary_groups = mallocz(sizeof(gid_t) * ngroups); |
| 96 | if(os_getgrouplist(username, gid, supplementary_groups, &ngroups) == -1) { |
| 97 | if(am_i_root) |
| 98 | netdata_log_error("Cannot get supplementary groups of user '%s'.", username); |
| 99 | |
| 100 | ngroups = 0; |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | nd_log_chown_log_files(uid, gid); |
| 105 | chown_open_file(STDOUT_FILENO, uid, gid); |
| 106 | chown_open_file(STDERR_FILENO, uid, gid); |
| 107 | chown_open_file(pid_fd, uid, gid); |
| 108 | |
| 109 | if(supplementary_groups && ngroups > 0) { |
| 110 | if(setgroups((size_t)ngroups, supplementary_groups) == -1) { |
| 111 | if(am_i_root) |
| 112 | netdata_log_error("Cannot set supplementary groups for user '%s'", username); |
| 113 | } |
| 114 | ngroups = 0; |
| 115 | } |
| 116 | |
| 117 | if(supplementary_groups) |
| 118 | freez(supplementary_groups); |
| 119 | |
| 120 | #if !defined(FSANITIZE_ADDRESS) |
| 121 | if(os_setresgid(gid, gid, gid) != 0) { |
| 122 | netdata_log_error("Cannot switch to user's %s group (gid: %u).", username, gid); |
| 123 | return -1; |
| 124 | } |
| 125 | |
| 126 | if(os_setresuid(uid, uid, uid) != 0) { |
| 127 | netdata_log_error("Cannot switch to user %s (uid: %u).", username, uid); |
| 128 | return -1; |
| 129 | } |
| 130 | |
| 131 | if(setgid(gid) != 0) { |
| 132 | netdata_log_error("Cannot switch to user's %s group (gid: %u).", username, gid); |
| 133 | return -1; |
| 134 | } |
| 135 | |
| 136 | if(setegid(gid) != 0) { |
| 137 | netdata_log_error("Cannot effectively switch to user's %s group (gid: %u).", username, gid); |
| 138 | return -1; |
| 139 | } |
| 140 | |
| 141 | if(setuid(uid) != 0) { |
| 142 | netdata_log_error("Cannot switch to user %s (uid: %u).", username, uid); |
| 143 | return -1; |
| 144 | } |
| 145 | if(seteuid(uid) != 0) { |
| 146 | netdata_log_error("Cannot effectively switch to user %s (uid: %u).", username, uid); |
| 147 | return -1; |
| 148 | } |
| 149 | #else |
| 150 | fprintf(stderr, "Running with a Sanitizer, skipping setuid/setgid\n"); |
| 151 | #endif |
| 152 | |
| 153 | return(0); |
| 154 | } |
| 155 | |
| 156 | #ifndef OOM_SCORE_ADJ_MAX |
| 157 | #define OOM_SCORE_ADJ_MAX (1000) |
| 158 | #endif |
| 159 | #ifndef OOM_SCORE_ADJ_MIN |
| 160 | #define OOM_SCORE_ADJ_MIN (-1000) |
| 161 | #endif |
| 162 | |
| 163 | static void oom_score_adj(void) { |
| 164 | char buf[30 + 1]; |
| 165 | long long int old_score, wanted_score = 0, final_score = 0; |
| 166 | |
| 167 | // read the existing score |
| 168 | if(read_single_signed_number_file("/proc/self/oom_score_adj", &old_score)) { |
| 169 | netdata_log_error("Out-Of-Memory (OOM) score setting is not supported on this system."); |
| 170 | return; |
| 171 | } |
| 172 | |
| 173 | if (old_score != 0) { |
| 174 | wanted_score = old_score; |
| 175 | analytics_report_oom_score(old_score); |
| 176 | } |
| 177 | |
| 178 | // check the environment |
| 179 | const char *s = getenv("OOMScoreAdjust"); |
| 180 | if(!s || !*s) { |
| 181 | snprintfz(buf, sizeof(buf) - 1, "%d", (int)wanted_score); |
| 182 | s = buf; |
| 183 | } |
| 184 | |
| 185 | // check netdata.conf configuration |
| 186 | s = inicfg_get(&netdata_config, CONFIG_SECTION_GLOBAL, "OOM score", s); |
| 187 | if(s && *s && (isdigit((uint8_t)*s) || *s == '-' || *s == '+')) |
| 188 | wanted_score = atoll(s); |
| 189 | else if(s && !strcmp(s, "keep")) { |
| 190 | netdata_log_info("Out-Of-Memory (OOM) kept as-is (running with %d)", (int) old_score); |
| 191 | return; |
| 192 | } |
| 193 | else { |
| 194 | netdata_log_info("Out-Of-Memory (OOM) score not changed due to non-numeric setting: '%s' (running with %d)", s, (int)old_score); |
| 195 | return; |
| 196 | } |
| 197 | |
| 198 | if(wanted_score < OOM_SCORE_ADJ_MIN) { |
| 199 | netdata_log_error("Wanted Out-Of-Memory (OOM) score %d is too small. Using %d", (int)wanted_score, (int)OOM_SCORE_ADJ_MIN); |
| 200 | wanted_score = OOM_SCORE_ADJ_MIN; |
| 201 | } |
| 202 | |
| 203 | if(wanted_score > OOM_SCORE_ADJ_MAX) { |
| 204 | netdata_log_error("Wanted Out-Of-Memory (OOM) score %d is too big. Using %d", (int)wanted_score, (int)OOM_SCORE_ADJ_MAX); |
| 205 | wanted_score = OOM_SCORE_ADJ_MAX; |
| 206 | } |
| 207 | |
| 208 | if(old_score == wanted_score) { |
| 209 | netdata_log_info("Out-Of-Memory (OOM) score is already set to the wanted value %d", (int)old_score); |
| 210 | return; |
| 211 | } |
| 212 | |
| 213 | int written = 0; |
| 214 | int fd = open("/proc/self/oom_score_adj", O_WRONLY | O_CLOEXEC); |
| 215 | if(fd != -1) { |
| 216 | snprintfz(buf, sizeof(buf) - 1, "%d", (int)wanted_score); |
| 217 | ssize_t len = strlen(buf); |
| 218 | if(len > 0 && write(fd, buf, (size_t)len) == len) written = 1; |
| 219 | close(fd); |
| 220 | |
| 221 | if(written) { |
| 222 | if(read_single_signed_number_file("/proc/self/oom_score_adj", &final_score)) |
| 223 | netdata_log_error("Adjusted my Out-Of-Memory (OOM) score to %d, but cannot verify it.", (int)wanted_score); |
| 224 | else if(final_score == wanted_score) |
| 225 | netdata_log_info("Adjusted my Out-Of-Memory (OOM) score from %d to %d.", (int)old_score, (int)final_score); |
| 226 | else |
| 227 | netdata_log_error("Adjusted my Out-Of-Memory (OOM) score from %d to %d, but it has been set to %d.", (int)old_score, (int)wanted_score, (int)final_score); |
| 228 | analytics_report_oom_score(final_score); |
| 229 | } |
| 230 | else |
| 231 | netdata_log_error("Failed to adjust my Out-Of-Memory (OOM) score to %d. Running with %d. (systemd systems may change it via netdata.service)", (int)wanted_score, (int)old_score); |
| 232 | } |
| 233 | else |
| 234 | netdata_log_error("Failed to adjust my Out-Of-Memory (OOM) score. Cannot open /proc/self/oom_score_adj for writing."); |
| 235 | } |
| 236 | |
| 237 | static void process_nice_level(void) { |
| 238 | #ifdef HAVE_NICE |
| 239 | int nice_level = (int)inicfg_get_number(&netdata_config, CONFIG_SECTION_GLOBAL, "process nice level", 0); |
| 240 | if(nice(nice_level) == -1) |
| 241 | netdata_log_error("Cannot set netdata CPU nice level to %d.", nice_level); |
| 242 | else |
| 243 | netdata_log_debug(D_SYSTEM, "Set netdata nice level to %d.", nice_level); |
| 244 | #endif // HAVE_NICE |
| 245 | } |
| 246 | |
| 247 | #define SCHED_FLAG_NONE 0x00 |
| 248 | #define SCHED_FLAG_PRIORITY_CONFIGURABLE 0x01 // the priority is user configurable |
| 249 | #define SCHED_FLAG_KEEP_AS_IS 0x04 // do not attempt to set policy, priority or nice() |
| 250 | #define SCHED_FLAG_USE_NICE 0x08 // use nice() after setting this policy |
| 251 | |
| 252 | struct sched_def { |
| 253 | char *name; |
| 254 | int policy; |
| 255 | int priority; |
| 256 | uint8_t flags; |
| 257 | } scheduler_defaults[] = { |
| 258 | |
| 259 | // the order of array members is important! |
| 260 | // the first defined is the default used by netdata |
| 261 | |
| 262 | // the available members are important too! |
| 263 | // these are all the possible scheduling policies supported by netdata |
| 264 | |
| 265 | // do not change the scheduling priority |
| 266 | { "keep", 0, 0, SCHED_FLAG_KEEP_AS_IS }, |
| 267 | { "none", 0, 0, SCHED_FLAG_KEEP_AS_IS }, |
| 268 | |
| 269 | #ifdef SCHED_BATCH |
| 270 | { "batch", SCHED_BATCH, 0, SCHED_FLAG_USE_NICE }, |
| 271 | #endif |
| 272 | |
| 273 | #ifdef SCHED_OTHER |
| 274 | { "other", SCHED_OTHER, 0, SCHED_FLAG_USE_NICE }, |
| 275 | { "nice", SCHED_OTHER, 0, SCHED_FLAG_USE_NICE }, |
| 276 | #endif |
| 277 | |
| 278 | #ifdef SCHED_IDLE |
| 279 | { "idle", SCHED_IDLE, 0, SCHED_FLAG_NONE }, |
| 280 | #endif |
| 281 | |
| 282 | #ifdef SCHED_RR |
| 283 | { "rr", SCHED_RR, 0, SCHED_FLAG_PRIORITY_CONFIGURABLE }, |
| 284 | #endif |
| 285 | |
| 286 | #ifdef SCHED_FIFO |
| 287 | { "fifo", SCHED_FIFO, 0, SCHED_FLAG_PRIORITY_CONFIGURABLE }, |
| 288 | #endif |
| 289 | |
| 290 | // array termination |
| 291 | { NULL, 0, 0, 0 } |
| 292 | }; |
| 293 | |
| 294 | |
| 295 | #ifdef HAVE_SCHED_GETSCHEDULER |
| 296 | static void sched_getscheduler_report(void) { |
| 297 | int sched = sched_getscheduler(0); |
| 298 | if(sched == -1) { |
| 299 | netdata_log_error("Cannot get my current process scheduling policy."); |
| 300 | return; |
| 301 | } |
| 302 | else { |
| 303 | int i; |
| 304 | for(i = 0 ; scheduler_defaults[i].name ; i++) { |
| 305 | if(scheduler_defaults[i].policy == sched) { |
| 306 | if(scheduler_defaults[i].flags & SCHED_FLAG_PRIORITY_CONFIGURABLE) { |
| 307 | struct sched_param param; |
| 308 | if(sched_getparam(0, ¶m) == -1) { |
| 309 | netdata_log_error("Cannot get the process scheduling priority for my policy '%s'", scheduler_defaults[i].name); |
| 310 | return; |
| 311 | } |
| 312 | else { |
| 313 | netdata_log_info("Running with process scheduling policy '%s', priority %d", scheduler_defaults[i].name, param.sched_priority); |
| 314 | } |
| 315 | } |
| 316 | else if(scheduler_defaults[i].flags & SCHED_FLAG_USE_NICE) { |
| 317 | #ifdef HAVE_GETPRIORITY |
| 318 | int n = getpriority(PRIO_PROCESS, 0); |
| 319 | netdata_log_info("Running with process scheduling policy '%s', nice level %d", scheduler_defaults[i].name, n); |
| 320 | #else // !HAVE_GETPRIORITY |
| 321 | netdata_log_info("Running with process scheduling policy '%s'", scheduler_defaults[i].name); |
| 322 | #endif // !HAVE_GETPRIORITY |
| 323 | } |
| 324 | else { |
| 325 | netdata_log_info("Running with process scheduling policy '%s'", scheduler_defaults[i].name); |
| 326 | } |
| 327 | |
| 328 | return; |
| 329 | } |
| 330 | } |
| 331 | } |
| 332 | } |
| 333 | #endif /* HAVE_SCHED_GETSCHEDULER */ |
| 334 | |
| 335 | #ifdef HAVE_SCHED_SETSCHEDULER |
| 336 | |
| 337 | static void sched_setscheduler_set(void) { |
| 338 | |
| 339 | if(scheduler_defaults[0].name) { |
| 340 | const char *name = scheduler_defaults[0].name; |
| 341 | int policy = scheduler_defaults[0].policy, priority = scheduler_defaults[0].priority; |
| 342 | uint8_t flags = scheduler_defaults[0].flags; |
| 343 | int found = 0; |
| 344 | |
| 345 | // read the configuration |
| 346 | name = inicfg_get(&netdata_config, CONFIG_SECTION_GLOBAL, "process scheduling policy", name); |
| 347 | int i; |
| 348 | for(i = 0 ; scheduler_defaults[i].name ; i++) { |
| 349 | if(!strcmp(name, scheduler_defaults[i].name)) { |
| 350 | found = 1; |
| 351 | policy = scheduler_defaults[i].policy; |
| 352 | priority = scheduler_defaults[i].priority; |
| 353 | flags = scheduler_defaults[i].flags; |
| 354 | |
| 355 | if(flags & SCHED_FLAG_KEEP_AS_IS) |
| 356 | goto report; |
| 357 | |
| 358 | if(flags & SCHED_FLAG_PRIORITY_CONFIGURABLE) |
| 359 | priority = (int)inicfg_get_number(&netdata_config, CONFIG_SECTION_GLOBAL, "process scheduling priority", priority); |
| 360 | |
| 361 | #ifdef HAVE_SCHED_GET_PRIORITY_MIN |
| 362 | errno_clear(); |
| 363 | if(priority < sched_get_priority_min(policy)) { |
| 364 | netdata_log_error("scheduler %s (%d) priority %d is below the minimum %d. Using the minimum.", name, policy, priority, sched_get_priority_min(policy)); |
| 365 | priority = sched_get_priority_min(policy); |
| 366 | } |
| 367 | #endif |
| 368 | #ifdef HAVE_SCHED_GET_PRIORITY_MAX |
| 369 | errno_clear(); |
| 370 | if(priority > sched_get_priority_max(policy)) { |
| 371 | netdata_log_error("scheduler %s (%d) priority %d is above the maximum %d. Using the maximum.", name, policy, priority, sched_get_priority_max(policy)); |
| 372 | priority = sched_get_priority_max(policy); |
| 373 | } |
| 374 | #endif |
| 375 | break; |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | if(!found) { |
| 380 | netdata_log_error("Unknown scheduling policy '%s' - falling back to nice", name); |
| 381 | goto fallback; |
| 382 | } |
| 383 | |
| 384 | const struct sched_param param = { |
| 385 | .sched_priority = priority |
| 386 | }; |
| 387 | |
| 388 | errno_clear(); |
| 389 | i = sched_setscheduler(0, policy, ¶m); |
| 390 | if(i != 0) { |
| 391 | netdata_log_error("Cannot adjust netdata scheduling policy to %s (%d), with priority %d. Falling back to nice.", |
| 392 | name, |
| 393 | policy, |
| 394 | priority); |
| 395 | } |
| 396 | else { |
| 397 | netdata_log_info("Adjusted netdata scheduling policy to %s (%d), with priority %d.", name, policy, priority); |
| 398 | if(!(flags & SCHED_FLAG_USE_NICE)) |
| 399 | goto report; |
| 400 | } |
| 401 | } |
| 402 | |
| 403 | fallback: |
| 404 | process_nice_level(); |
| 405 | |
| 406 | report: |
| 407 | sched_getscheduler_report(); |
| 408 | } |
| 409 | #else /* HAVE_SCHED_SETSCHEDULER */ |
| 410 | static void sched_setscheduler_set(void) { |
| 411 | process_nice_level(); |
| 412 | } |
| 413 | #endif /* HAVE_SCHED_SETSCHEDULER */ |
| 414 | |
| 415 | int become_daemon(int dont_fork, const char *user) { |
| 416 | if(!dont_fork) { |
| 417 | daemon_status_file_startup_step("startup(become daemon - fork1)"); |
| 418 | int i = fork(); |
| 419 | if(i == -1) { |
| 420 | fatal("cannot fork"); |
| 421 | exit(1); |
| 422 | } |
| 423 | if(i != 0) { |
| 424 | // the parent |
| 425 | exit(0); |
| 426 | } |
| 427 | |
| 428 | // the child |
| 429 | gettid_uncached(); |
| 430 | nd_initialize_signals(false); |
| 431 | #ifdef HAVE_LIBBACKTRACE |
| 432 | stacktrace_flush(); |
| 433 | #endif |
| 434 | |
| 435 | // become session leader |
| 436 | if (setsid() < 0) { |
| 437 | fatal("Cannot become session leader."); |
| 438 | exit(2); |
| 439 | } |
| 440 | |
| 441 | // fork() again |
| 442 | daemon_status_file_startup_step("startup(become daemon - fork2)"); |
| 443 | i = fork(); |
| 444 | if(i == -1) { |
| 445 | fatal("cannot fork for a second time"); |
| 446 | exit(1); |
| 447 | } |
| 448 | if(i != 0) { |
| 449 | // the parent |
| 450 | exit(0); |
| 451 | } |
| 452 | |
| 453 | // the child |
| 454 | gettid_uncached(); |
| 455 | nd_initialize_signals(false); |
| 456 | #ifdef HAVE_LIBBACKTRACE |
| 457 | stacktrace_flush(); |
| 458 | #endif |
| 459 | } |
| 460 | |
| 461 | // generate our pid file |
| 462 | daemon_status_file_startup_step("startup(become daemon - write pid)"); |
| 463 | |
| 464 | int pidfd = -1; |
| 465 | if(pidfile && *pidfile) { |
| 466 | pidfd = open(pidfile, O_WRONLY | O_CREAT | O_CLOEXEC, 0644); |
| 467 | if(pidfd >= 0) { |
| 468 | if(ftruncate(pidfd, 0) != 0) |
| 469 | netdata_log_error("Cannot truncate pidfile '%s'.", pidfile); |
| 470 | |
| 471 | char b[100]; |
| 472 | sprintf(b, "%d\n", getpid()); |
| 473 | ssize_t i = write(pidfd, b, strlen(b)); |
| 474 | if(i <= 0) |
| 475 | netdata_log_error("Cannot write pidfile '%s'.", pidfile); |
| 476 | } |
| 477 | else |
| 478 | netdata_log_error("Failed to open pidfile '%s'.", pidfile); |
| 479 | } |
| 480 | |
| 481 | // Set new file permissions |
| 482 | umask(0007); |
| 483 | |
| 484 | // adjust my Out-Of-Memory score |
| 485 | daemon_status_file_startup_step("startup(become daemon - oom)"); |
| 486 | oom_score_adj(); |
| 487 | |
| 488 | // never become a problem |
| 489 | daemon_status_file_startup_step("startup(become daemon - sched)"); |
| 490 | sched_setscheduler_set(); |
| 491 | |
| 492 | if(user && *user) { |
| 493 | daemon_status_file_startup_step("startup(become daemon - user)"); |
| 494 | if(become_user(user, pidfd) != 0) { |
| 495 | netdata_log_error("Cannot switch to user '%s'. Continuing with current privileges.", user); |
| 496 | } |
| 497 | else |
| 498 | netdata_log_info("Successfully switched to user '%s'.", user); |
| 499 | } |
| 500 | else { |
| 501 | daemon_status_file_startup_step("startup(become daemon - dirs)"); |
| 502 | prepare_required_directories(getuid(), getgid()); |
| 503 | } |
| 504 | |
| 505 | daemon_status_file_startup_step("startup(become daemon - done)"); |
| 506 | if(pidfd != -1) |
| 507 | close(pidfd); |
| 508 | |
| 509 | return(0); |
| 510 | } |