xref: /freebsd/contrib/kyua/utils/process/isolation_test.cpp (revision 6be3386466ab79a84b48429ae66244f21526d3df)
1 // Copyright 2014 The Kyua Authors.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are
6 // met:
7 //
8 // * Redistributions of source code must retain the above copyright
9 //   notice, this list of conditions and the following disclaimer.
10 // * Redistributions in binary form must reproduce the above copyright
11 //   notice, this list of conditions and the following disclaimer in the
12 //   documentation and/or other materials provided with the distribution.
13 // * Neither the name of Google Inc. nor the names of its contributors
14 //   may be used to endorse or promote products derived from this software
15 //   without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 
29 #include "utils/process/isolation.hpp"
30 
31 extern "C" {
32 #include <sys/types.h>
33 #include <sys/resource.h>
34 #include <sys/stat.h>
35 
36 #include <unistd.h>
37 }
38 
39 #include <cerrno>
40 #include <cstdlib>
41 #include <fstream>
42 #include <iostream>
43 
44 #include <atf-c++.hpp>
45 
46 #include "utils/defs.hpp"
47 #include "utils/env.hpp"
48 #include "utils/format/macros.hpp"
49 #include "utils/fs/operations.hpp"
50 #include "utils/fs/path.hpp"
51 #include "utils/optional.ipp"
52 #include "utils/passwd.hpp"
53 #include "utils/process/child.ipp"
54 #include "utils/process/status.hpp"
55 #include "utils/sanity.hpp"
56 #include "utils/test_utils.ipp"
57 
58 namespace fs = utils::fs;
59 namespace passwd = utils::passwd;
60 namespace process = utils::process;
61 
62 using utils::none;
63 using utils::optional;
64 
65 
66 namespace {
67 
68 
69 /// Runs the given hook in a subprocess.
70 ///
71 /// \param hook The code to run in the subprocess.
72 ///
73 /// \return The status of the subprocess for further validation.
74 ///
75 /// \post The subprocess.stdout and subprocess.stderr files, created in the
76 /// current directory, contain the output of the subprocess.
77 template< typename Hook >
78 static process::status
79 fork_and_run(Hook hook)
80 {
81     std::auto_ptr< process::child > child = process::child::fork_files(
82         hook, fs::path("subprocess.stdout"), fs::path("subprocess.stderr"));
83     const process::status status = child->wait();
84 
85     atf::utils::cat_file("subprocess.stdout", "isolated child stdout: ");
86     atf::utils::cat_file("subprocess.stderr", "isolated child stderr: ");
87 
88     return status;
89 }
90 
91 
92 /// Subprocess that validates the cleanliness of the environment.
93 ///
94 /// \post Exits with success if the environment is clean; failure otherwise.
95 static void
96 check_clean_environment(void)
97 {
98     fs::mkdir(fs::path("some-directory"), 0755);
99     process::isolate_child(none, fs::path("some-directory"));
100 
101     bool failed = false;
102 
103     const char* empty[] = { "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
104                             "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC",
105                             "LC_TIME", NULL };
106     const char** iter;
107     for (iter = empty; *iter != NULL; ++iter) {
108         if (utils::getenv(*iter)) {
109             failed = true;
110             std::cout << F("%s was not unset\n") % *iter;
111         }
112     }
113 
114     if (utils::getenv_with_default("HOME", "") != "some-directory") {
115         failed = true;
116         std::cout << "HOME was not set to the work directory\n";
117     }
118 
119     if (utils::getenv_with_default("TMPDIR", "") != "some-directory") {
120         failed = true;
121         std::cout << "TMPDIR was not set to the work directory\n";
122     }
123 
124     if (utils::getenv_with_default("TZ", "") != "UTC") {
125         failed = true;
126         std::cout << "TZ was not set to UTC\n";
127     }
128 
129     if (utils::getenv_with_default("LEAVE_ME_ALONE", "") != "kill-some-day") {
130         failed = true;
131         std::cout << "LEAVE_ME_ALONE was modified while it should not have "
132             "been\n";
133     }
134 
135     std::exit(failed ? EXIT_FAILURE : EXIT_SUCCESS);
136 }
137 
138 
139 /// Subprocess that checks if user privileges are dropped.
140 class check_drop_privileges {
141     /// The user to drop the privileges to.
142     const passwd::user _unprivileged_user;
143 
144 public:
145     /// Constructor.
146     ///
147     /// \param unprivileged_user The user to drop the privileges to.
148     check_drop_privileges(const passwd::user& unprivileged_user) :
149         _unprivileged_user(unprivileged_user)
150     {
151     }
152 
153     /// Body of the subprocess.
154     ///
155     /// \post Exits with success if the process has dropped privileges as
156     /// expected.
157     void
158     operator()(void) const
159     {
160         fs::mkdir(fs::path("subdir"), 0755);
161         process::isolate_child(utils::make_optional(_unprivileged_user),
162                                fs::path("subdir"));
163 
164         if (::getuid() == 0) {
165             std::cout << "UID is still 0\n";
166             std::exit(EXIT_FAILURE);
167         }
168 
169         if (::getgid() == 0) {
170             std::cout << "GID is still 0\n";
171             std::exit(EXIT_FAILURE);
172         }
173 
174         ::gid_t groups[1];
175         if (::getgroups(1, groups) == -1) {
176             // Should only fail if we get more than one group notifying about
177             // not enough space in the groups variable to store the whole
178             // result.
179             INV(errno == EINVAL);
180             std::exit(EXIT_FAILURE);
181         }
182         if (groups[0] == 0) {
183             std::cout << "Primary group is still 0\n";
184             std::exit(EXIT_FAILURE);
185         }
186 
187         std::ofstream output("file.txt");
188         if (!output) {
189             std::cout << "Cannot write to isolated directory; owner not "
190                 "changed?\n";
191             std::exit(EXIT_FAILURE);
192         }
193 
194         std::exit(EXIT_SUCCESS);
195     }
196 };
197 
198 
199 /// Subprocess that dumps core to validate core dumping abilities.
200 static void
201 check_enable_core_dumps(void)
202 {
203     process::isolate_child(none, fs::path("."));
204     std::abort();
205 }
206 
207 
208 /// Subprocess that checks if the work directory is entered.
209 class check_enter_work_directory {
210     /// Directory to enter.  May be releative.
211     const fs::path _directory;
212 
213 public:
214     /// Constructor.
215     ///
216     /// \param directory Directory to enter.
217     check_enter_work_directory(const fs::path& directory) :
218         _directory(directory)
219     {
220     }
221 
222     /// Body of the subprocess.
223     ///
224     /// \post Exits with success if the process has entered the given work
225     /// directory; false otherwise.
226     void
227     operator()(void) const
228     {
229         const fs::path exp_subdir = fs::current_path() / _directory;
230         process::isolate_child(none, _directory);
231         std::exit(fs::current_path() == exp_subdir ?
232                   EXIT_SUCCESS : EXIT_FAILURE);
233     }
234 };
235 
236 
237 /// Subprocess that validates that it owns a session.
238 ///
239 /// \post Exits with success if the process lives in its own session;
240 /// failure otherwise.
241 static void
242 check_new_session(void)
243 {
244     process::isolate_child(none, fs::path("."));
245     std::exit(::getsid(::getpid()) == ::getpid() ? EXIT_SUCCESS : EXIT_FAILURE);
246 }
247 
248 
249 /// Subprocess that validates the disconnection from any terminal.
250 ///
251 /// \post Exits with success if the environment is clean; failure otherwise.
252 static void
253 check_no_terminal(void)
254 {
255     process::isolate_child(none, fs::path("."));
256 
257     const char* const args[] = {
258         "/bin/sh",
259         "-i",
260         "-c",
261         "echo success",
262         NULL
263     };
264     ::execv("/bin/sh", UTILS_UNCONST(char*, args));
265     std::abort();
266 }
267 
268 
269 /// Subprocess that validates that it has become the leader of a process group.
270 ///
271 /// \post Exits with success if the process lives in its own process group;
272 /// failure otherwise.
273 static void
274 check_process_group(void)
275 {
276     process::isolate_child(none, fs::path("."));
277     std::exit(::getpgid(::getpid()) == ::getpid() ?
278               EXIT_SUCCESS : EXIT_FAILURE);
279 }
280 
281 
282 /// Subprocess that validates that the umask has been reset.
283 ///
284 /// \post Exits with success if the umask matches the expected value; failure
285 /// otherwise.
286 static void
287 check_umask(void)
288 {
289     process::isolate_child(none, fs::path("."));
290     std::exit(::umask(0) == 0022 ? EXIT_SUCCESS : EXIT_FAILURE);
291 }
292 
293 
294 }  // anonymous namespace
295 
296 
297 ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__clean_environment);
298 ATF_TEST_CASE_BODY(isolate_child__clean_environment)
299 {
300     utils::setenv("HOME", "/non-existent/directory");
301     utils::setenv("TMPDIR", "/non-existent/directory");
302     utils::setenv("LANG", "C");
303     utils::setenv("LC_ALL", "C");
304     utils::setenv("LC_COLLATE", "C");
305     utils::setenv("LC_CTYPE", "C");
306     utils::setenv("LC_MESSAGES", "C");
307     utils::setenv("LC_MONETARY", "C");
308     utils::setenv("LC_NUMERIC", "C");
309     utils::setenv("LC_TIME", "C");
310     utils::setenv("LEAVE_ME_ALONE", "kill-some-day");
311     utils::setenv("TZ", "EST+5");
312 
313     const process::status status = fork_and_run(check_clean_environment);
314     ATF_REQUIRE(status.exited());
315     ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
316 }
317 
318 
319 ATF_TEST_CASE(isolate_child__other_user_when_unprivileged);
320 ATF_TEST_CASE_HEAD(isolate_child__other_user_when_unprivileged)
321 {
322     set_md_var("require.user", "unprivileged");
323 }
324 ATF_TEST_CASE_BODY(isolate_child__other_user_when_unprivileged)
325 {
326     const passwd::user user = passwd::current_user();
327 
328     passwd::user other_user = user;
329     other_user.uid += 1;
330     other_user.gid += 1;
331     process::isolate_child(utils::make_optional(other_user), fs::path("."));
332 
333     ATF_REQUIRE_EQ(user.uid, ::getuid());
334     ATF_REQUIRE_EQ(user.gid, ::getgid());
335 }
336 
337 
338 ATF_TEST_CASE(isolate_child__drop_privileges);
339 ATF_TEST_CASE_HEAD(isolate_child__drop_privileges)
340 {
341     set_md_var("require.config", "unprivileged-user");
342     set_md_var("require.user", "root");
343 }
344 ATF_TEST_CASE_BODY(isolate_child__drop_privileges)
345 {
346     const passwd::user unprivileged_user = passwd::find_user_by_name(
347         get_config_var("unprivileged-user"));
348 
349     const process::status status = fork_and_run(check_drop_privileges(
350         unprivileged_user));
351     ATF_REQUIRE(status.exited());
352     ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
353 }
354 
355 
356 ATF_TEST_CASE(isolate_child__drop_privileges_fail_uid);
357 ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_uid)
358 {
359     set_md_var("require.user", "unprivileged");
360 }
361 ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_uid)
362 {
363     // Fake the current user as root so that we bypass the protections in
364     // isolate_child that prevent us from attempting a user switch when we are
365     // not root.  We do this so we can trigger the setuid failure.
366     passwd::user root = passwd::user("root", 0, 0);
367     ATF_REQUIRE(root.is_root());
368     passwd::set_current_user_for_testing(root);
369 
370     passwd::user unprivileged_user = passwd::current_user();
371     unprivileged_user.uid += 1;
372 
373     const process::status status = fork_and_run(check_drop_privileges(
374         unprivileged_user));
375     ATF_REQUIRE(status.exited());
376     ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());
377     ATF_REQUIRE(atf::utils::grep_file("(chown|setuid).*failed",
378                                       "subprocess.stderr"));
379 }
380 
381 
382 ATF_TEST_CASE(isolate_child__drop_privileges_fail_gid);
383 ATF_TEST_CASE_HEAD(isolate_child__drop_privileges_fail_gid)
384 {
385     set_md_var("require.user", "unprivileged");
386 }
387 ATF_TEST_CASE_BODY(isolate_child__drop_privileges_fail_gid)
388 {
389     // Fake the current user as root so that we bypass the protections in
390     // isolate_child that prevent us from attempting a user switch when we are
391     // not root.  We do this so we can trigger the setgid failure.
392     passwd::user root = passwd::user("root", 0, 0);
393     ATF_REQUIRE(root.is_root());
394     passwd::set_current_user_for_testing(root);
395 
396     passwd::user unprivileged_user = passwd::current_user();
397     unprivileged_user.gid += 1;
398 
399     const process::status status = fork_and_run(check_drop_privileges(
400         unprivileged_user));
401     ATF_REQUIRE(status.exited());
402     ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());
403     ATF_REQUIRE(atf::utils::grep_file("(chown|setgid).*failed",
404                                       "subprocess.stderr"));
405 }
406 
407 
408 ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enable_core_dumps);
409 ATF_TEST_CASE_BODY(isolate_child__enable_core_dumps)
410 {
411     utils::require_run_coredump_tests(this);
412 
413     struct ::rlimit rl;
414     if (::getrlimit(RLIMIT_CORE, &rl) == -1)
415         fail("Failed to query the core size limit");
416     if (rl.rlim_cur == 0 || rl.rlim_max == 0)
417         skip("Maximum core size is zero; cannot run test");
418     rl.rlim_cur = 0;
419     if (::setrlimit(RLIMIT_CORE, &rl) == -1)
420         fail("Failed to lower the core size limit");
421 
422     const process::status status = fork_and_run(check_enable_core_dumps);
423     ATF_REQUIRE(status.signaled());
424     ATF_REQUIRE(status.coredump());
425 }
426 
427 
428 ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory);
429 ATF_TEST_CASE_BODY(isolate_child__enter_work_directory)
430 {
431     const fs::path directory("some/sub/directory");
432     fs::mkdir_p(directory, 0755);
433     const process::status status = fork_and_run(
434         check_enter_work_directory(directory));
435     ATF_REQUIRE(status.exited());
436     ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
437 }
438 
439 
440 ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__enter_work_directory_failure);
441 ATF_TEST_CASE_BODY(isolate_child__enter_work_directory_failure)
442 {
443     const fs::path directory("some/sub/directory");
444     const process::status status = fork_and_run(
445         check_enter_work_directory(directory));
446     ATF_REQUIRE(status.exited());
447     ATF_REQUIRE_EQ(process::exit_isolation_failure, status.exitstatus());
448     ATF_REQUIRE(atf::utils::grep_file("chdir\\(some/sub/directory\\) failed",
449                                       "subprocess.stderr"));
450 }
451 
452 
453 ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__new_session);
454 ATF_TEST_CASE_BODY(isolate_child__new_session)
455 {
456     const process::status status = fork_and_run(check_new_session);
457     ATF_REQUIRE(status.exited());
458     ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
459 }
460 
461 
462 ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__no_terminal);
463 ATF_TEST_CASE_BODY(isolate_child__no_terminal)
464 {
465     const process::status status = fork_and_run(check_no_terminal);
466     ATF_REQUIRE(status.exited());
467     ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
468 }
469 
470 
471 ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__process_group);
472 ATF_TEST_CASE_BODY(isolate_child__process_group)
473 {
474     const process::status status = fork_and_run(check_process_group);
475     ATF_REQUIRE(status.exited());
476     ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
477 }
478 
479 
480 ATF_TEST_CASE_WITHOUT_HEAD(isolate_child__reset_umask);
481 ATF_TEST_CASE_BODY(isolate_child__reset_umask)
482 {
483     const process::status status = fork_and_run(check_umask);
484     ATF_REQUIRE(status.exited());
485     ATF_REQUIRE_EQ(EXIT_SUCCESS, status.exitstatus());
486 }
487 
488 
489 /// Executes isolate_path() and compares the on-disk changes to expected values.
490 ///
491 /// \param unprivileged_user The user to pass to isolate_path; may be none.
492 /// \param exp_uid Expected UID or none to expect the old value.
493 /// \param exp_gid Expected GID or none to expect the old value.
494 static void
495 do_isolate_path_test(const optional< passwd::user >& unprivileged_user,
496                      const optional< uid_t >& exp_uid,
497                      const optional< gid_t >& exp_gid)
498 {
499     const fs::path dir("dir");
500     fs::mkdir(dir, 0755);
501     struct ::stat old_sb;
502     ATF_REQUIRE(::stat(dir.c_str(), &old_sb) != -1);
503 
504     process::isolate_path(unprivileged_user, dir);
505 
506     struct ::stat new_sb;
507     ATF_REQUIRE(::stat(dir.c_str(), &new_sb) != -1);
508 
509     if (exp_uid)
510         ATF_REQUIRE_EQ(exp_uid.get(), new_sb.st_uid);
511     else
512         ATF_REQUIRE_EQ(old_sb.st_uid, new_sb.st_uid);
513 
514     if (exp_gid)
515         ATF_REQUIRE_EQ(exp_gid.get(), new_sb.st_gid);
516     else
517         ATF_REQUIRE_EQ(old_sb.st_gid, new_sb.st_gid);
518 }
519 
520 
521 ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__no_user);
522 ATF_TEST_CASE_BODY(isolate_path__no_user)
523 {
524     do_isolate_path_test(none, none, none);
525 }
526 
527 
528 ATF_TEST_CASE_WITHOUT_HEAD(isolate_path__same_user);
529 ATF_TEST_CASE_BODY(isolate_path__same_user)
530 {
531     do_isolate_path_test(utils::make_optional(passwd::current_user()),
532                          none, none);
533 }
534 
535 
536 ATF_TEST_CASE(isolate_path__other_user_when_unprivileged);
537 ATF_TEST_CASE_HEAD(isolate_path__other_user_when_unprivileged)
538 {
539     set_md_var("require.user", "unprivileged");
540 }
541 ATF_TEST_CASE_BODY(isolate_path__other_user_when_unprivileged)
542 {
543     passwd::user user = passwd::current_user();
544     user.uid += 1;
545     user.gid += 1;
546 
547     do_isolate_path_test(utils::make_optional(user), none, none);
548 }
549 
550 
551 ATF_TEST_CASE(isolate_path__drop_privileges);
552 ATF_TEST_CASE_HEAD(isolate_path__drop_privileges)
553 {
554     set_md_var("require.config", "unprivileged-user");
555     set_md_var("require.user", "root");
556 }
557 ATF_TEST_CASE_BODY(isolate_path__drop_privileges)
558 {
559     const passwd::user unprivileged_user = passwd::find_user_by_name(
560         get_config_var("unprivileged-user"));
561     do_isolate_path_test(utils::make_optional(unprivileged_user),
562                          utils::make_optional(unprivileged_user.uid),
563                          utils::make_optional(unprivileged_user.gid));
564 }
565 
566 
567 ATF_TEST_CASE(isolate_path__drop_privileges_only_uid);
568 ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_uid)
569 {
570     set_md_var("require.config", "unprivileged-user");
571     set_md_var("require.user", "root");
572 }
573 ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_uid)
574 {
575     passwd::user unprivileged_user = passwd::find_user_by_name(
576         get_config_var("unprivileged-user"));
577     unprivileged_user.gid = ::getgid();
578     do_isolate_path_test(utils::make_optional(unprivileged_user),
579                          utils::make_optional(unprivileged_user.uid),
580                          none);
581 }
582 
583 
584 ATF_TEST_CASE(isolate_path__drop_privileges_only_gid);
585 ATF_TEST_CASE_HEAD(isolate_path__drop_privileges_only_gid)
586 {
587     set_md_var("require.config", "unprivileged-user");
588     set_md_var("require.user", "root");
589 }
590 ATF_TEST_CASE_BODY(isolate_path__drop_privileges_only_gid)
591 {
592     passwd::user unprivileged_user = passwd::find_user_by_name(
593         get_config_var("unprivileged-user"));
594     unprivileged_user.uid = ::getuid();
595     do_isolate_path_test(utils::make_optional(unprivileged_user),
596                          none,
597                          utils::make_optional(unprivileged_user.gid));
598 }
599 
600 
601 ATF_INIT_TEST_CASES(tcs)
602 {
603     ATF_ADD_TEST_CASE(tcs, isolate_child__clean_environment);
604     ATF_ADD_TEST_CASE(tcs, isolate_child__other_user_when_unprivileged);
605     ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges);
606     ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_uid);
607     ATF_ADD_TEST_CASE(tcs, isolate_child__drop_privileges_fail_gid);
608     ATF_ADD_TEST_CASE(tcs, isolate_child__enable_core_dumps);
609     ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory);
610     ATF_ADD_TEST_CASE(tcs, isolate_child__enter_work_directory_failure);
611     ATF_ADD_TEST_CASE(tcs, isolate_child__new_session);
612     ATF_ADD_TEST_CASE(tcs, isolate_child__no_terminal);
613     ATF_ADD_TEST_CASE(tcs, isolate_child__process_group);
614     ATF_ADD_TEST_CASE(tcs, isolate_child__reset_umask);
615 
616     ATF_ADD_TEST_CASE(tcs, isolate_path__no_user);
617     ATF_ADD_TEST_CASE(tcs, isolate_path__same_user);
618     ATF_ADD_TEST_CASE(tcs, isolate_path__other_user_when_unprivileged);
619     ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges);
620     ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_uid);
621     ATF_ADD_TEST_CASE(tcs, isolate_path__drop_privileges_only_gid);
622 }
623