chiark / gitweb /
doc: Document summary values of TOFU_STATS
[gnupg2.git] / g13 / g13-syshelp.c
1 /* g13-syshelp.c - Helper for disk key management with GnuPG
2  * Copyright (C) 2015 Werner Koch
3  *
4  * This file is part of GnuPG.
5  *
6  * GnuPG is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * GnuPG is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, see <https://www.gnu.org/licenses/>.
18  */
19
20 #include <config.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <errno.h>
25 #include <ctype.h>
26 #include <unistd.h>
27 #include <fcntl.h>
28 #include <limits.h>
29 #ifdef HAVE_PWD_H
30 # include <pwd.h>
31 #endif
32 #include <unistd.h>
33
34 #include "g13-syshelp.h"
35
36 #include <gcrypt.h>
37 #include <assuan.h>
38
39 #include "i18n.h"
40 #include "sysutils.h"
41 #include "asshelp.h"
42 #include "../common/init.h"
43 #include "keyblob.h"
44
45
46 enum cmd_and_opt_values {
47   aNull = 0,
48   oQuiet        = 'q',
49   oVerbose      = 'v',
50   oRecipient    = 'r',
51
52   aGPGConfList  = 500,
53
54   oDebug,
55   oDebugLevel,
56   oDebugAll,
57   oDebugNone,
58   oDebugWait,
59   oDebugAllowCoreDump,
60   oLogFile,
61   oNoLogFile,
62   oAuditLog,
63
64   oOutput,
65
66   oAgentProgram,
67   oGpgProgram,
68   oType,
69
70   oDisplay,
71   oTTYname,
72   oTTYtype,
73   oLCctype,
74   oLCmessages,
75   oXauthority,
76
77   oStatusFD,
78   oLoggerFD,
79
80   oNoVerbose,
81   oNoSecmemWarn,
82   oHomedir,
83   oDryRun,
84   oNoDetach,
85
86   oNoRandomSeedFile,
87   oFakedSystemTime
88  };
89
90
91 static ARGPARSE_OPTS opts[] = {
92
93   ARGPARSE_s_n (oDryRun, "dry-run", N_("do not make any changes")),
94
95   ARGPARSE_s_n (oVerbose, "verbose", N_("verbose")),
96   ARGPARSE_s_n (oQuiet, "quiet",  N_("be somewhat more quiet")),
97
98   ARGPARSE_s_s (oDebug, "debug", "@"),
99   ARGPARSE_s_s (oDebugLevel, "debug-level",
100                 N_("|LEVEL|set the debugging level to LEVEL")),
101   ARGPARSE_s_n (oDebugAll, "debug-all", "@"),
102   ARGPARSE_s_n (oDebugNone, "debug-none", "@"),
103   ARGPARSE_s_i (oDebugWait, "debug-wait", "@"),
104   ARGPARSE_s_n (oDebugAllowCoreDump, "debug-allow-core-dump", "@"),
105
106   ARGPARSE_end ()
107 };
108
109
110 /* The list of supported debug flags.  */
111 static struct debug_flags_s debug_flags [] =
112   {
113     { DBG_MOUNT_VALUE  , "mount"  },
114     { DBG_CRYPTO_VALUE , "crypto"  },
115     { DBG_MEMORY_VALUE , "memory"  },
116     { DBG_MEMSTAT_VALUE, "memstat" },
117     { DBG_IPC_VALUE    , "ipc"     },
118     { 0, NULL }
119   };
120
121
122 /* The timer tick interval used by the idle task.  */
123 #define TIMERTICK_INTERVAL_SEC     (1)
124
125 /* It is possible that we are currently running under setuid permissions.  */
126 static int maybe_setuid = 1;
127
128 /* Helper to implement --debug-level and --debug.  */
129 static const char *debug_level;
130 static unsigned int debug_value;
131
132
133 /* Local prototypes.  */
134 static void g13_syshelp_deinit_default_ctrl (ctrl_t ctrl);
135 static void release_tab_items (tab_item_t tab);
136 static tab_item_t parse_g13tab (const char *username);
137
138
139 \f
140 static const char *
141 my_strusage( int level )
142 {
143   const char *p;
144
145   switch (level)
146     {
147     case 11: p = "@G13@-syshelp (@GNUPG@)";
148       break;
149     case 13: p = VERSION; break;
150     case 17: p = PRINTABLE_OS_NAME; break;
151     case 19: p = _("Please report bugs to <" PACKAGE_BUGREPORT ">.\n");
152       break;
153     case 1:
154     case 40: p = _("Usage: @G13@-syshelp [options] [files] (-h for help)");
155       break;
156     case 41:
157       p = _("Syntax: @G13@-syshelp [options] [files]\n"
158             "Helper to perform root-only tasks for g13\n");
159       break;
160
161     case 31: p = "\nHome: "; break;
162     case 32: p = gnupg_homedir (); break;
163
164     default: p = NULL; break;
165     }
166   return p;
167 }
168
169
170 /* Setup the debugging.  With a DEBUG_LEVEL of NULL only the active
171    debug flags are propagated to the subsystems.  With DEBUG_LEVEL
172    set, a specific set of debug flags is set; and individual debugging
173    flags will be added on top.  */
174 static void
175 set_debug (void)
176 {
177   int numok = (debug_level && digitp (debug_level));
178   int numlvl = numok? atoi (debug_level) : 0;
179
180   if (!debug_level)
181     ;
182   else if (!strcmp (debug_level, "none") || (numok && numlvl < 1))
183     opt.debug = 0;
184   else if (!strcmp (debug_level, "basic") || (numok && numlvl <= 2))
185     opt.debug = DBG_IPC_VALUE|DBG_MOUNT_VALUE;
186   else if (!strcmp (debug_level, "advanced") || (numok && numlvl <= 5))
187     opt.debug = DBG_IPC_VALUE|DBG_MOUNT_VALUE;
188   else if (!strcmp (debug_level, "expert") || (numok && numlvl <= 8))
189     opt.debug = (DBG_IPC_VALUE|DBG_MOUNT_VALUE|DBG_CRYPTO_VALUE);
190   else if (!strcmp (debug_level, "guru") || numok)
191     {
192       opt.debug = ~0;
193       /* if (numok) */
194       /*   opt.debug &= ~(DBG_HASHING_VALUE); */
195     }
196   else
197     {
198       log_error (_("invalid debug-level '%s' given\n"), debug_level);
199       g13_exit(2);
200     }
201
202   opt.debug |= debug_value;
203
204   if (opt.debug && !opt.verbose)
205     opt.verbose = 1;
206   if (opt.debug)
207     opt.quiet = 0;
208
209   if (opt.debug & DBG_CRYPTO_VALUE )
210     gcry_control (GCRYCTL_SET_DEBUG_FLAGS, 1);
211   gcry_control (GCRYCTL_SET_VERBOSITY, (int)opt.verbose);
212
213   if (opt.debug)
214     parse_debug_flag (NULL, &opt.debug, debug_flags);
215 }
216
217
218 int
219 main ( int argc, char **argv)
220 {
221   ARGPARSE_ARGS pargs;
222   int orig_argc;
223   char **orig_argv;
224   gpg_error_t err = 0;
225   /* const char *fname; */
226   int may_coredump;
227   FILE *configfp = NULL;
228   char *configname = NULL;
229   unsigned configlineno;
230   int parse_debug = 0;
231   int no_more_options = 0;
232   int default_config =1;
233   char *logfile = NULL;
234   /* int debug_wait = 0; */
235   int use_random_seed = 1;
236   /* int nodetach = 0; */
237   /* int nokeysetup = 0; */
238   struct server_control_s ctrl;
239
240   /*mtrace();*/
241
242   early_system_init ();
243   gnupg_reopen_std (G13_NAME "-syshelp");
244   set_strusage (my_strusage);
245   gcry_control (GCRYCTL_SUSPEND_SECMEM_WARN);
246
247   log_set_prefix (G13_NAME "-syshelp", GPGRT_LOG_WITH_PREFIX);
248
249   /* Make sure that our subsystems are ready.  */
250   i18n_init ();
251   init_common_subsystems (&argc, &argv);
252
253   /* Take extra care of the random pool.  */
254   gcry_control (GCRYCTL_USE_SECURE_RNDPOOL);
255
256   may_coredump = disable_core_dumps ();
257
258   g13_init_signals ();
259
260   dotlock_create (NULL, 0); /* Register locking cleanup.  */
261
262   opt.session_env = session_env_new ();
263   if (!opt.session_env)
264     log_fatal ("error allocating session environment block: %s\n",
265                strerror (errno));
266
267   /* Fixme: We enable verbose mode here because there is currently no
268      way to do this when starting g13-syshelp.  To fix that we should
269      add a g13-syshelp.conf file in /etc/gnupg.  */
270   opt.verbose = 1;
271
272   /* First check whether we have a debug option on the commandline.  */
273   orig_argc = argc;
274   orig_argv = argv;
275   pargs.argc = &argc;
276   pargs.argv = &argv;
277   pargs.flags= (ARGPARSE_FLAG_KEEP | ARGPARSE_FLAG_NOVERSION);
278   while (arg_parse( &pargs, opts))
279     {
280       if (pargs.r_opt == oDebug || pargs.r_opt == oDebugAll)
281         parse_debug++;
282     }
283
284   /* Initialize the secure memory. */
285   gcry_control (GCRYCTL_INIT_SECMEM, 16384, 0);
286   maybe_setuid = 0;
287
288   /*
289      Now we are now working under our real uid
290   */
291
292   /* Setup malloc hooks. */
293   {
294     struct assuan_malloc_hooks malloc_hooks;
295
296     malloc_hooks.malloc = gcry_malloc;
297     malloc_hooks.realloc = gcry_realloc;
298     malloc_hooks.free = gcry_free;
299     assuan_set_malloc_hooks (&malloc_hooks);
300   }
301
302   /* Prepare libassuan.  */
303   assuan_set_gpg_err_source (GPG_ERR_SOURCE_DEFAULT);
304   /*assuan_set_system_hooks (ASSUAN_SYSTEM_NPTH);*/
305   setup_libassuan_logging (&opt.debug, NULL);
306
307   /* Setup a default control structure for command line mode.  */
308   memset (&ctrl, 0, sizeof ctrl);
309   g13_syshelp_init_default_ctrl (&ctrl);
310   ctrl.no_server = 1;
311   ctrl.status_fd = -1; /* No status output. */
312
313   if (default_config )
314     configname = make_filename (gnupg_sysconfdir (),
315                                 G13_NAME"-syshelp.conf", NULL);
316
317   argc        = orig_argc;
318   argv        = orig_argv;
319   pargs.argc  = &argc;
320   pargs.argv  = &argv;
321   pargs.flags =  1;  /* Do not remove the args.  */
322
323  next_pass:
324   if (configname)
325     {
326       configlineno = 0;
327       configfp = fopen (configname, "r");
328       if (!configfp)
329         {
330           if (default_config)
331             {
332               if (parse_debug)
333                 log_info (_("NOTE: no default option file '%s'\n"), configname);
334             }
335           else
336             {
337               log_error (_("option file '%s': %s\n"),
338                          configname, strerror(errno));
339               g13_exit(2);
340             }
341           xfree (configname);
342           configname = NULL;
343         }
344       if (parse_debug && configname)
345         log_info (_("reading options from '%s'\n"), configname);
346       default_config = 0;
347     }
348
349   while (!no_more_options
350          && optfile_parse (configfp, configname, &configlineno, &pargs, opts))
351     {
352       switch (pargs.r_opt)
353         {
354         case oQuiet: opt.quiet = 1; break;
355
356         case oDryRun: opt.dry_run = 1; break;
357
358         case oVerbose:
359           opt.verbose++;
360           gcry_control (GCRYCTL_SET_VERBOSITY, (int)opt.verbose);
361           break;
362         case oNoVerbose:
363           opt.verbose = 0;
364           gcry_control (GCRYCTL_SET_VERBOSITY, (int)opt.verbose);
365           break;
366
367         case oLogFile: logfile = pargs.r.ret_str; break;
368         case oNoLogFile: logfile = NULL; break;
369
370         case oNoDetach: /*nodetach = 1; */break;
371
372         case oDebug:
373           if (parse_debug_flag (pargs.r.ret_str, &opt.debug, debug_flags))
374             {
375               pargs.r_opt = ARGPARSE_INVALID_ARG;
376               pargs.err = ARGPARSE_PRINT_ERROR;
377             }
378             break;
379         case oDebugAll: debug_value = ~0; break;
380         case oDebugNone: debug_value = 0; break;
381         case oDebugLevel: debug_level = pargs.r.ret_str; break;
382         case oDebugWait: /*debug_wait = pargs.r.ret_int; */break;
383         case oDebugAllowCoreDump:
384           may_coredump = enable_core_dumps ();
385           break;
386
387         case oStatusFD: ctrl.status_fd = pargs.r.ret_int; break;
388         case oLoggerFD: log_set_fd (pargs.r.ret_int ); break;
389
390         case oHomedir: gnupg_set_homedir (pargs.r.ret_str); break;
391
392         case oFakedSystemTime:
393           {
394             time_t faked_time = isotime2epoch (pargs.r.ret_str);
395             if (faked_time == (time_t)(-1))
396               faked_time = (time_t)strtoul (pargs.r.ret_str, NULL, 10);
397             gnupg_set_time (faked_time, 0);
398           }
399           break;
400
401         case oNoSecmemWarn: gcry_control (GCRYCTL_DISABLE_SECMEM_WARN); break;
402
403         case oNoRandomSeedFile: use_random_seed = 0; break;
404
405         default:
406           pargs.err = configfp? ARGPARSE_PRINT_WARNING:ARGPARSE_PRINT_ERROR;
407           break;
408         }
409     }
410
411   if (configfp)
412     {
413       fclose (configfp);
414       configfp = NULL;
415       /* Keep a copy of the config filename. */
416       opt.config_filename = configname;
417       configname = NULL;
418       goto next_pass;
419     }
420   xfree (configname);
421   configname = NULL;
422
423   if (!opt.config_filename)
424     opt.config_filename = make_filename (gnupg_homedir (),
425                                          G13_NAME".conf", NULL);
426
427   if (log_get_errorcount(0))
428     g13_exit(2);
429
430   /* Now that we have the options parsed we need to update the default
431      control structure.  */
432   g13_syshelp_init_default_ctrl (&ctrl);
433
434   if (may_coredump && !opt.quiet)
435     log_info (_("WARNING: program may create a core file!\n"));
436
437   if (logfile)
438     {
439       log_set_file (logfile);
440       log_set_prefix (NULL, GPGRT_LOG_WITH_PREFIX | GPGRT_LOG_WITH_TIME | GPGRT_LOG_WITH_PID);
441     }
442
443   if (gnupg_faked_time_p ())
444     {
445       gnupg_isotime_t tbuf;
446
447       log_info (_("WARNING: running with faked system time: "));
448       gnupg_get_isotime (tbuf);
449       dump_isotime (tbuf);
450       log_printf ("\n");
451     }
452
453   /* Print any pending secure memory warnings.  */
454   gcry_control (GCRYCTL_RESUME_SECMEM_WARN);
455
456   /* Setup the debug flags for all subsystems.  */
457   set_debug ();
458
459   /* Install a regular exit handler to make real sure that the secure
460      memory gets wiped out.  */
461   g13_install_emergency_cleanup ();
462
463   /* Terminate if we found any error until now.  */
464   if (log_get_errorcount(0))
465     g13_exit (2);
466
467   /* Set the standard GnuPG random seed file.  */
468   if (use_random_seed)
469     {
470       char *p = make_filename (gnupg_homedir (), "random_seed", NULL);
471       gcry_control (GCRYCTL_SET_RANDOM_SEED_FILE, p);
472       xfree(p);
473     }
474
475   /* Get the UID of the caller.  */
476 #if defined(HAVE_PWD_H) && defined(HAVE_GETPWUID)
477   {
478     const char *uidstr;
479     struct passwd *pwd = NULL;
480
481     uidstr = getenv ("USERV_UID");
482
483     /* Print a quick note if we are not started via userv.  */
484     if (!uidstr)
485       {
486         if (getuid ())
487           {
488             log_info ("WARNING: Not started via userv\n");
489             ctrl.fail_all_cmds = 1;
490           }
491         ctrl.client.uid = getuid ();
492       }
493     else
494       {
495         unsigned long myuid;
496
497         errno = 0;
498         myuid = strtoul (uidstr, NULL, 10);
499         if (myuid == ULONG_MAX && errno)
500           {
501             log_info ("WARNING: Started via broken userv: %s\n",
502                       strerror (errno));
503             ctrl.fail_all_cmds = 1;
504             ctrl.client.uid = getuid ();
505           }
506         else
507           ctrl.client.uid = (uid_t)myuid;
508       }
509
510     pwd = getpwuid (ctrl.client.uid);
511     if (!pwd || !*pwd->pw_name)
512       {
513         log_info ("WARNING: Name for UID not found: %s\n", strerror (errno));
514         ctrl.fail_all_cmds = 1;
515         ctrl.client.uname = xstrdup ("?");
516       }
517     else
518       ctrl.client.uname = xstrdup (pwd->pw_name);
519
520     /* Check that the user name does not contain a directory
521        separator. */
522     if (strchr (ctrl.client.uname, '/'))
523       {
524         log_info ("WARNING: Invalid user name passed\n");
525         ctrl.fail_all_cmds = 1;
526       }
527   }
528 #else /*!HAVE_PWD_H || !HAVE_GETPWUID*/
529   log_info ("WARNING: System does not support required syscalls\n");
530   ctrl.fail_all_cmds = 1;
531   ctrl.client.uid = getuid ();
532   ctrl.client.uname = xstrdup ("?");
533 #endif /*!HAVE_PWD_H || !HAVE_GETPWUID*/
534
535   /* Read the table entries for this user.  */
536   if (!ctrl.fail_all_cmds
537       && !(ctrl.client.tab = parse_g13tab (ctrl.client.uname)))
538     ctrl.fail_all_cmds = 1;
539
540   /* Start the server.  */
541   err = syshelp_server (&ctrl);
542   if (err)
543     log_error ("server exited with error: %s <%s>\n",
544                gpg_strerror (err), gpg_strsource (err));
545
546   /* Cleanup.  */
547   g13_syshelp_deinit_default_ctrl (&ctrl);
548   g13_exit (0);
549   return 8; /*NOTREACHED*/
550 }
551
552
553 /* Store defaults into the per-connection CTRL object.  */
554 void
555 g13_syshelp_init_default_ctrl (ctrl_t ctrl)
556 {
557   ctrl->conttype = CONTTYPE_DM_CRYPT;
558 }
559
560 /* Release all resources allocated by default in the CTRl object.  */
561 static void
562 g13_syshelp_deinit_default_ctrl (ctrl_t ctrl)
563 {
564   xfree (ctrl->client.uname);
565   release_tab_items (ctrl->client.tab);
566 }
567
568
569 /* Release the list of g13tab itejms at TAB.  */
570 static void
571 release_tab_items (tab_item_t tab)
572 {
573   while (tab)
574     {
575       tab_item_t next = tab->next;
576       xfree (tab->mountpoint);
577       xfree (tab);
578       tab = next;
579     }
580 }
581
582
583 void
584 g13_syshelp_i_know_what_i_am_doing (void)
585 {
586   const char * const yesfile = "Yes-g13-I-know-what-I-am-doing";
587   char *fname;
588
589   fname = make_filename (gnupg_sysconfdir (), yesfile, NULL);
590   if (access (fname, F_OK))
591     {
592       log_info ("*******************************************************\n");
593       log_info ("* The G13 support for DM-Crypt is new and not matured.\n");
594       log_info ("* Bugs or improper use may delete all your disks!\n");
595       log_info ("* To confirm that you are ware of this risk, create\n");
596       log_info ("* the file '%s'.\n", fname);
597       log_info ("*******************************************************\n");
598       exit (1);
599     }
600   xfree (fname);
601 }
602
603
604 /* Parse the /etc/gnupg/g13tab for user USERNAME.  Return a table for
605    the user on success.  Return NULL on error and print
606    diagnostics. */
607 static tab_item_t
608 parse_g13tab (const char *username)
609 {
610   gpg_error_t err;
611   int c, n;
612   char line[512];
613   char *p;
614   char *fname;
615   estream_t fp;
616   int lnr;
617   char **words = NULL;
618   tab_item_t table = NULL;
619   tab_item_t *tabletail, ti;
620
621   fname = make_filename (gnupg_sysconfdir (), G13_NAME"tab", NULL);
622   fp = es_fopen (fname, "r");
623   if (!fp)
624     {
625       err = gpg_error_from_syserror ();
626       log_error (_("error opening '%s': %s\n"), fname, gpg_strerror (err));
627       goto leave;
628     }
629
630   tabletail = &table;
631   err = 0;
632   lnr = 0;
633   while (es_fgets (line, DIM(line)-1, fp))
634     {
635       lnr++;
636       n = strlen (line);
637       if (!n || line[n-1] != '\n')
638         {
639           /* Eat until end of line. */
640           while ((c=es_getc (fp)) != EOF && c != '\n')
641             ;
642           err = gpg_error (*line? GPG_ERR_LINE_TOO_LONG
643                            : GPG_ERR_INCOMPLETE_LINE);
644           log_error (_("file '%s', line %d: %s\n"),
645                      fname, lnr, gpg_strerror (err));
646           continue;
647         }
648       line[--n] = 0; /* Chop the LF. */
649       if (n && line[n-1] == '\r')
650         line[--n] = 0; /* Chop an optional CR. */
651
652       /* Allow for empty lines and spaces */
653       for (p=line; spacep (p); p++)
654         ;
655       if (!*p || *p == '#')
656         continue;
657
658       /* Parse the line.  The format is
659        * <username> <blockdev> [<label>|"-" [<mountpoint>]]
660        */
661       xfree (words);
662       words = strtokenize (p, " \t");
663       if (!words)
664         {
665           err = gpg_error_from_syserror ();
666           break;
667         }
668       if (!words[0] || !words[1])
669         {
670           log_error (_("file '%s', line %d: %s\n"),
671                      fname, lnr, gpg_strerror (GPG_ERR_SYNTAX));
672           continue;
673         }
674       if (!(*words[1] == '/'
675             || !strncmp (words[1], "PARTUUID=", 9)
676             || !strncmp (words[1], "partuuid=", 9)))
677         {
678           log_error (_("file '%s', line %d: %s\n"),
679                      fname, lnr, "Invalid block device syntax");
680           continue;
681         }
682       if (words[2])
683         {
684           if (strlen (words[2]) > 16 || strchr (words[2], '/'))
685             {
686               log_error (_("file '%s', line %d: %s\n"),
687                          fname, lnr, "Label too long or invalid syntax");
688               continue;
689             }
690
691           if (words[3] && *words[3] != '/')
692             {
693               log_error (_("file '%s', line %d: %s\n"),
694                          fname, lnr, "Invalid mountpoint syntax");
695               continue;
696             }
697         }
698       if (strcmp (words[0], username))
699         continue; /* Skip entries for other usernames!  */
700
701       ti = xtrymalloc (sizeof *ti + strlen (words[1]));
702       if (!ti)
703         {
704           err = gpg_error_from_syserror ();
705           break;
706         }
707       ti->next = NULL;
708       ti->label = NULL;
709       ti->mountpoint = NULL;
710       strcpy (ti->blockdev, *words[1]=='/'? words[1] : words[1]+9);
711       if (words[2])
712         {
713           if (strcmp (words[2], "-")
714               && !(ti->label = xtrystrdup (words[2])))
715             {
716               err = gpg_error_from_syserror ();
717               xfree (ti);
718               break;
719             }
720           if (words[3] && !(ti->mountpoint = xtrystrdup (words[3])))
721             {
722               err = gpg_error_from_syserror ();
723               xfree (ti->label);
724               xfree (ti);
725               break;
726             }
727         }
728       *tabletail = ti;
729       tabletail = &ti->next;
730     }
731
732   if (!err && !es_feof (fp))
733     err = gpg_error_from_syserror ();
734   if (err)
735     log_error (_("error reading '%s', line %d: %s\n"),
736                fname, lnr, gpg_strerror (err));
737
738  leave:
739   xfree (words);
740   es_fclose (fp);
741   xfree (fname);
742   if (err)
743     {
744       release_tab_items (table);
745       return NULL;
746     }
747   return table;
748 }