xref: /illumos-gate/usr/src/cmd/bart/create.c (revision 6a634c9dca3093f3922e4b7ab826d7bdf17bf78e)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 /*
22  * Copyright (c) 2003, 2010, Oracle and/or its affiliates. All rights reserved.
23  */
24 
25 #include <signal.h>
26 #include <unistd.h>
27 #include <sys/acl.h>
28 #include <sys/statvfs.h>
29 #include <sys/wait.h>
30 #include "bart.h"
31 #include <aclutils.h>
32 
33 static int	sanitize_reloc_root(char *root, size_t bufsize);
34 static int	create_manifest_filelist(char **argv, char *reloc_root);
35 static int	create_manifest_rule(char *reloc_root, FILE *rule_fp);
36 static void	output_manifest(void);
37 static int	eval_file(const char *fname, const struct stat64 *statb,
38 	struct FTW *ftwx);
39 static char	*sanitized_fname(const char *, boolean_t);
40 static char	*get_acl_string(const char *fname, const struct stat64 *statb,
41     int *err_code);
42 static int	generate_hash(int fdin, char *hash_str);
43 static int	read_filelist(char *reloc_root, char **argv, char *buf,
44     size_t bufsize);
45 static int	walker(const char *name, const struct stat64 *sp,
46     int type, struct FTW *ftwx);
47 
48 /*
49  * The following globals are necessary due to the "walker" function
50  * provided by nftw().  Since there is no way to pass them through to the
51  * walker function, they must be global.
52  */
53 static int		compute_chksum = 1, eval_err = 0;
54 static struct rule	*subtree_root;
55 static char		reloc_root[PATH_MAX];
56 static struct statvfs64	parent_vfs;
57 
58 int
bart_create(int argc,char ** argv)59 bart_create(int argc, char **argv)
60 {
61 	boolean_t	filelist_input;
62 	int		ret, c, output_pipe[2];
63 	FILE 		*rules_fd = NULL;
64 	pid_t		pid;
65 
66 	filelist_input = B_FALSE;
67 	reloc_root[0] = '\0';
68 
69 	while ((c = getopt(argc, argv, "Inr:R:")) != EOF) {
70 		switch (c) {
71 		case 'I':
72 			if (rules_fd != NULL) {
73 				(void) fprintf(stderr, "%s", INPUT_ERR);
74 				usage();
75 			}
76 			filelist_input = B_TRUE;
77 			break;
78 
79 		case 'n':
80 			compute_chksum = 0;
81 			break;
82 
83 		case 'r':
84 			if (strcmp(optarg, "-") == 0)
85 				rules_fd = stdin;
86 			else
87 				rules_fd = fopen(optarg, "r");
88 			if (rules_fd == NULL) {
89 				perror(optarg);
90 				usage();
91 			}
92 			break;
93 
94 		case 'R':
95 			(void) strlcpy(reloc_root, optarg, sizeof (reloc_root));
96 			ret = sanitize_reloc_root(reloc_root,
97 			    sizeof (reloc_root));
98 			if (ret == 0)
99 				usage();
100 			break;
101 
102 		case '?':
103 		default :
104 			usage();
105 		}
106 	}
107 	argv += optind;
108 
109 	if (pipe(output_pipe) < 0) {
110 		perror("");
111 		exit(FATAL_EXIT);
112 	}
113 
114 	pid = fork();
115 	if (pid < 0) {
116 		perror(NULL);
117 		exit(FATAL_EXIT);
118 	}
119 
120 	/*
121 	 * Break the creation of a manifest into two parts: the parent process
122 	 * generated the data whereas the child process sorts the data.
123 	 *
124 	 * The processes communicate through the pipe.
125 	 */
126 	if (pid > 0) {
127 		/*
128 		 * Redirect the stdout of this process so it goes into
129 		 * output_pipe[0].  The output of this process will be read
130 		 * by the child, which will sort the output.
131 		 */
132 		if (dup2(output_pipe[0], STDOUT_FILENO) != STDOUT_FILENO) {
133 			perror(NULL);
134 			exit(FATAL_EXIT);
135 		}
136 		(void) close(output_pipe[0]);
137 		(void) close(output_pipe[1]);
138 
139 		if (filelist_input == B_TRUE) {
140 			ret = create_manifest_filelist(argv, reloc_root);
141 		} else {
142 			ret = create_manifest_rule(reloc_root, rules_fd);
143 		}
144 
145 		/* Close stdout so the sort in the child proc will complete */
146 		(void) fclose(stdout);
147 	} else {
148 		/*
149 		 * Redirect the stdin of this process so its read in from
150 		 * the pipe, which is the parent process in this case.
151 		 */
152 		if (dup2(output_pipe[1], STDIN_FILENO) != STDIN_FILENO) {
153 			perror(NULL);
154 			exit(FATAL_EXIT);
155 		}
156 		(void) close(output_pipe[0]);
157 
158 		output_manifest();
159 	}
160 
161 	/* Wait for the child proc (the sort) to complete */
162 	(void) wait(0);
163 
164 	return (ret);
165 }
166 
167 /*
168  * Handle the -R option and sets 'root' to be the absolute path of the
169  * relocatable root.  This is useful when the user specifies '-R ../../foo'.
170  *
171  * Return code is whether or not the location spec'd by the -R flag is a
172  * directory or not.
173  */
174 static int
sanitize_reloc_root(char * root,size_t bufsize)175 sanitize_reloc_root(char *root, size_t bufsize)
176 {
177 	char		pwd[PATH_MAX];
178 
179 	/*
180 	 * First, save the current directory and go to the location
181 	 * specified with the -R option.
182 	 */
183 	(void) getcwd(pwd, sizeof (pwd));
184 	if (chdir(root) < 0) {
185 		/* Failed to change directory, something is wrong.... */
186 		perror(root);
187 		return (0);
188 	}
189 
190 	/*
191 	 * Save the absolute path of the relocatable root directory.
192 	 */
193 	(void) getcwd(root, bufsize);
194 
195 	/*
196 	 * Now, go back to where we started, necessary for picking up a rules
197 	 * file.
198 	 */
199 	if (chdir(pwd) < 0) {
200 		/* Failed to change directory, something is wrong.... */
201 		perror(root);
202 		return (0);
203 	}
204 
205 	/*
206 	 * Make sure the path returned does not have a trailing /. This
207 	 * can only happen when the entire pathname is "/".
208 	 */
209 	if (strcmp(root, "/") == 0)
210 		root[0] = '\0';
211 
212 	/*
213 	 * Since the earlier chdir() succeeded, return success.
214 	 */
215 	return (1);
216 }
217 
218 /*
219  * This is the worker bee which creates the manifest based upon the command
220  * line options supplied by the user.
221  *
222  * NOTE: create_manifest() eventually outputs data to a pipe, which is read in
223  * by the child process.  The child process is running output_manifest(), which
224  * is responsible for generating sorted output.
225  */
226 static int
create_manifest_rule(char * reloc_root,FILE * rule_fp)227 create_manifest_rule(char *reloc_root, FILE *rule_fp)
228 {
229 	struct rule	*root;
230 	int		ret_status = EXIT;
231 	uint_t		flags;
232 
233 	if (compute_chksum)
234 		flags = ATTR_CONTENTS;
235 	else
236 		flags = 0;
237 	ret_status = read_rules(rule_fp, reloc_root, flags, 1);
238 
239 	/* Loop through every single subtree */
240 	for (root = get_first_subtree(); root != NULL;
241 	    root = get_next_subtree(root)) {
242 
243 		/*
244 		 * Check to see if this subtree should have contents
245 		 * checking turned on or off.
246 		 *
247 		 * NOTE: The 'compute_chksum' and 'parent_vfs'
248 		 * are a necessary hack: the variables are used in
249 		 * walker(), both directly and indirectly.  Since
250 		 * the parameters to walker() are defined by nftw(),
251 		 * the globals are really a backdoor mechanism.
252 		 */
253 		ret_status = statvfs64(root->subtree, &parent_vfs);
254 		if (ret_status < 0) {
255 			perror(root->subtree);
256 			continue;
257 		}
258 
259 		/*
260 		 * Walk the subtree and invoke the callback function walker()
261 		 * Use FTW_ANYERR to get FTW_NS and FTW_DNR entries *and*
262 		 * to continue past those errors.
263 		 */
264 		subtree_root = root;
265 		(void) nftw64(root->subtree, &walker, 20, FTW_PHYS|FTW_ANYERR);
266 
267 		/*
268 		 * Ugly but necessary:
269 		 *
270 		 * walker() must return 0, or the tree walk will stop,
271 		 * so warning flags must be set through a global.
272 		 */
273 		if (eval_err == WARNING_EXIT)
274 			ret_status = WARNING_EXIT;
275 
276 	}
277 	return (ret_status);
278 }
279 
280 static int
create_manifest_filelist(char ** argv,char * reloc_root)281 create_manifest_filelist(char **argv, char *reloc_root)
282 {
283 	int	ret_status = EXIT;
284 	char	input_fname[PATH_MAX];
285 
286 	while (read_filelist(reloc_root, argv,
287 	    input_fname, sizeof (input_fname)) != -1) {
288 
289 		struct stat64	stat_buf;
290 		int		ret;
291 
292 		ret = lstat64(input_fname, &stat_buf);
293 		if (ret < 0) {
294 			ret_status = WARNING_EXIT;
295 			perror(input_fname);
296 		} else {
297 			ret = eval_file(input_fname, &stat_buf, NULL);
298 
299 			if (ret == WARNING_EXIT)
300 				ret_status = WARNING_EXIT;
301 		}
302 	}
303 
304 	return (ret_status);
305 }
306 
307 /*
308  * output_manifest() the child process.  It reads in the output from
309  * create_manifest() and sorts it.
310  */
311 static void
output_manifest(void)312 output_manifest(void)
313 {
314 	char	*env[] = {"LC_CTYPE=C", "LC_COLLATE=C", "LC_NUMERIC=C", NULL};
315 	time_t		time_val;
316 	struct tm	*tm;
317 	char		time_buf[1024];
318 
319 	(void) printf("%s", MANIFEST_VER);
320 	time_val = time((time_t)0);
321 	tm = localtime(&time_val);
322 	(void) strftime(time_buf, sizeof (time_buf), "%A, %B %d, %Y (%T)", tm);
323 	(void) printf("! %s\n", time_buf);
324 	(void) printf("%s", FORMAT_STR);
325 	(void) fflush(stdout);
326 	/*
327 	 * Simply run sort and read from the the current stdin, which is really
328 	 * the output of create_manifest().
329 	 * Also, make sure the output is unique, since a given file may be
330 	 * included by several stanzas.
331 	 */
332 	if (execle("/usr/bin/sort", "sort", "-u", NULL, env) < 0) {
333 		perror("");
334 		exit(FATAL_EXIT);
335 	}
336 
337 	/*NOTREACHED*/
338 }
339 
340 /*
341  * Callback function for nftw()
342  */
343 static int
walker(const char * name,const struct stat64 * sp,int type,struct FTW * ftwx)344 walker(const char *name, const struct stat64 *sp, int type, struct FTW *ftwx)
345 {
346 	int			ret;
347 	struct statvfs64	path_vfs;
348 	boolean_t		dir_flag = B_FALSE;
349 	struct rule		*rule;
350 
351 	switch (type) {
352 	case FTW_F:	/* file 		*/
353 		rule = check_rules(name, 'F');
354 		if (rule != NULL) {
355 			if (rule->attr_list & ATTR_CONTENTS)
356 				compute_chksum = 1;
357 			else
358 				compute_chksum = 0;
359 		}
360 		break;
361 	case FTW_SL:	/* symbolic link, FTW_PHYS	*/
362 	case FTW_SLN:	/* symbolic link, ~FTW_PHYS	*/
363 		break;
364 	case FTW_DP:	/* end of directory, FTW_DEPTH	*/
365 	case FTW_D:	/* enter directory, ~FTW_DEPTH	*/
366 		dir_flag = B_TRUE;
367 		ret = statvfs64(name, &path_vfs);
368 		if (ret < 0)
369 			eval_err = WARNING_EXIT;
370 		break;
371 	case FTW_NS:	/* unstatable file	*/
372 		(void) fprintf(stderr, UNKNOWN_FILE, name);
373 		eval_err = WARNING_EXIT;
374 		return (0);
375 	case FTW_DNR:	/* unreadable directory	*/
376 		(void) fprintf(stderr, CANTLIST_DIR, name);
377 		eval_err = WARNING_EXIT;
378 		return (0);
379 	default:
380 		(void) fprintf(stderr, INTERNAL_ERR, name);
381 		eval_err = WARNING_EXIT;
382 		return (0);
383 	}
384 
385 	/* This is the function which really processes the file */
386 	ret = eval_file(name, sp, ftwx);
387 
388 	/*
389 	 * Since the parameters to walker() are constrained by nftw(),
390 	 * need to use a global to reflect a WARNING.  Sigh.
391 	 */
392 	if (ret == WARNING_EXIT)
393 		eval_err = WARNING_EXIT;
394 
395 	/*
396 	 * This is a case of a directory which crosses into a mounted
397 	 * filesystem of a different type, e.g., UFS -> NFS.
398 	 * BART should not walk the new filesystem (by specification), so
399 	 * set this consolidation-private flag so the rest of the subtree
400 	 * under this directory is not waled.
401 	 */
402 	if (dir_flag &&
403 	    (strcmp(parent_vfs.f_basetype, path_vfs.f_basetype) != 0))
404 		ftwx->quit = FTW_PRUNE;
405 
406 	return (0);
407 }
408 
409 /*
410  * This file does the per-file evaluation and is run to generate every entry
411  * in the manifest.
412  *
413  * All output is written to a pipe which is read by the child process,
414  * which is running output_manifest().
415  */
416 static int
eval_file(const char * fname,const struct stat64 * statb,struct FTW * ftwx)417 eval_file(const char *fname, const struct stat64 *statb, struct FTW *ftwx)
418 {
419 	int	fd, ret, err_code, i, result;
420 	char	last_field[PATH_MAX], ftype, *acl_str;
421 	char	*quoted_name;
422 
423 	err_code = EXIT;
424 
425 	switch (statb->st_mode & S_IFMT) {
426 	/* Regular file */
427 	case S_IFREG: ftype = 'F'; break;
428 
429 	/* Directory */
430 	case S_IFDIR: ftype = 'D'; break;
431 
432 	/* Block Device */
433 	case S_IFBLK: ftype = 'B'; break;
434 
435 	/* Character Device */
436 	case S_IFCHR: ftype = 'C'; break;
437 
438 	/* Named Pipe */
439 	case S_IFIFO: ftype = 'P'; break;
440 
441 	/* Socket */
442 	case S_IFSOCK: ftype = 'S'; break;
443 
444 	/* Door */
445 	case S_IFDOOR: ftype = 'O'; break;
446 
447 	/* Symbolic link */
448 	case S_IFLNK: ftype = 'L'; break;
449 
450 	default: ftype = '-'; break;
451 	}
452 
453 	/* First, make sure this file should be cataloged */
454 
455 	if ((subtree_root != NULL) &&
456 	    ((result = exclude_fname(fname, ftype, subtree_root)) !=
457 	    NO_EXCLUDE)) {
458 		if ((result == EXCLUDE_PRUNE) && (ftwx != (struct FTW *)NULL))
459 			ftwx->quit = FTW_PRUNE;
460 		return (err_code);
461 	}
462 	for (i = 0; i < PATH_MAX; i++)
463 		last_field[i] = '\0';
464 
465 	/*
466 	 * Regular files, compute the MD5 checksum and put it into 'last_field'
467 	 * UNLESS instructed to ignore the checksums.
468 	 */
469 	if (ftype == 'F') {
470 		if (compute_chksum) {
471 			fd = open(fname, O_RDONLY|O_LARGEFILE);
472 			if (fd < 0) {
473 				err_code = WARNING_EXIT;
474 				perror(fname);
475 
476 				/* default value since the computution failed */
477 				(void) strcpy(last_field, "-");
478 			} else {
479 				if (generate_hash(fd, last_field) != 0) {
480 					err_code = WARNING_EXIT;
481 					(void) fprintf(stderr, CONTENTS_WARN,
482 					    fname);
483 					(void) strcpy(last_field, "-");
484 				}
485 			}
486 			(void) close(fd);
487 		}
488 		/* Instructed to ignore checksums, just put in a '-' */
489 		else
490 			(void) strcpy(last_field, "-");
491 	}
492 
493 	/*
494 	 * For symbolic links, put the destination of the symbolic link into
495 	 * 'last_field'
496 	 */
497 	if (ftype == 'L') {
498 		ret = readlink(fname, last_field, sizeof (last_field));
499 		if (ret < 0) {
500 			err_code = WARNING_EXIT;
501 			perror(fname);
502 
503 			/* default value since the computation failed */
504 			(void) strcpy(last_field, "-");
505 		}
506 		else
507 			(void) strlcpy(last_field,
508 			    sanitized_fname(last_field, B_FALSE),
509 			    sizeof (last_field));
510 
511 		/*
512 		 * Boundary condition: possible for a symlink to point to
513 		 * nothing [ ln -s '' link_name ].  For this case, set the
514 		 * destination to "\000".
515 		 */
516 		if (strlen(last_field) == 0)
517 			(void) strcpy(last_field, "\\000");
518 	}
519 
520 	acl_str = get_acl_string(fname, statb, &err_code);
521 
522 	/* Sanitize 'fname', so its in the proper format for the manifest */
523 	quoted_name = sanitized_fname(fname, B_TRUE);
524 
525 	/* Start to build the entry.... */
526 	(void) printf("%s %c %d %o %s %x %d %d", quoted_name, ftype,
527 	    (int)statb->st_size, (int)statb->st_mode, acl_str,
528 	    (int)statb->st_mtime, (int)statb->st_uid, (int)statb->st_gid);
529 
530 	/* Finish it off based upon whether or not it's a device node */
531 	if ((ftype == 'B') || (ftype == 'C'))
532 		(void) printf(" %x\n", (int)statb->st_rdev);
533 	else if (strlen(last_field) > 0)
534 		(void) printf(" %s\n", last_field);
535 	else
536 		(void) printf("\n");
537 
538 	/* free the memory consumed */
539 	free(acl_str);
540 	free(quoted_name);
541 
542 	return (err_code);
543 }
544 
545 /*
546  * When creating a manifest, make sure all '?', tabs, space, newline, '/'
547  * and '[' are all properly quoted.  Convert them to a "\ooo" where the 'ooo'
548  * represents their octal value. For filesystem objects, as opposed to symlink
549  * targets, also canonicalize the pathname.
550  */
551 static char *
sanitized_fname(const char * fname,boolean_t canon_path)552 sanitized_fname(const char *fname, boolean_t canon_path)
553 {
554 	const char *ip;
555 	unsigned char ch;
556 	char *op, *quoted_name;
557 
558 	/* Initialize everything */
559 	quoted_name = safe_calloc((4 * PATH_MAX) + 1);
560 	ip = fname;
561 	op = quoted_name;
562 
563 	if (canon_path) {
564 		/*
565 		 * In the case when a relocatable root was used, the relocatable
566 		 * root should *not* be part of the manifest.
567 		 */
568 		ip += strlen(reloc_root);
569 
570 		/*
571 		 * In the case when the '-I' option was used, make sure
572 		 * the quoted_name starts with a '/'.
573 		 */
574 		if (*ip != '/')
575 			*op++ = '/';
576 	}
577 
578 	/* Now walk through 'fname' and build the quoted string */
579 	while ((ch = *ip++) != 0) {
580 		switch (ch) {
581 		/* Quote the following characters */
582 		case ' ':
583 		case '*':
584 		case '\n':
585 		case '?':
586 		case '[':
587 		case '\\':
588 		case '\t':
589 			op += sprintf(op, "\\%.3o", (unsigned char)ch);
590 			break;
591 
592 		/* Otherwise, simply append them */
593 		default:
594 			*op++ = ch;
595 			break;
596 		}
597 	}
598 
599 	*op = 0;
600 
601 	return (quoted_name);
602 }
603 
604 /*
605  * Function responsible for generating the ACL information for a given
606  * file.  Note, the string is put into buffer malloc'd by this function.
607  * It's the responsibility of the caller to free the buffer.  This function
608  * should never return a NULL pointer.
609  */
610 static char *
get_acl_string(const char * fname,const struct stat64 * statb,int * err_code)611 get_acl_string(const char *fname, const struct stat64 *statb, int *err_code)
612 {
613 	acl_t		*aclp;
614 	char		*acltext;
615 	int		error;
616 
617 	if (S_ISLNK(statb->st_mode)) {
618 		return (safe_strdup("-"));
619 	}
620 
621 	/*
622 	 *  Include trivial acl's
623 	 */
624 	error = acl_get(fname, 0, &aclp);
625 
626 	if (error != 0) {
627 		*err_code = WARNING_EXIT;
628 		(void) fprintf(stderr, "%s: %s\n", fname, acl_strerror(error));
629 		return (safe_strdup("-"));
630 	} else {
631 		acltext = acl_totext(aclp, 0);
632 		acl_free(aclp);
633 		if (acltext == NULL)
634 			return (safe_strdup("-"));
635 		else
636 			return (acltext);
637 	}
638 }
639 
640 
641 /*
642  *
643  * description:	This routine reads stdin in BUF_SIZE chunks, uses the bits
644  *		to update the md5 hash buffer, and outputs the chunks
645  *		to stdout.  When stdin is exhausted, the hash is computed,
646  *		converted to a hexadecimal string, and returned.
647  *
648  * returns:	The md5 hash of stdin, or NULL if unsuccessful for any reason.
649  */
650 static int
generate_hash(int fdin,char * hash_str)651 generate_hash(int fdin, char *hash_str)
652 {
653 	unsigned char buf[BUF_SIZE];
654 	unsigned char hash[MD5_DIGEST_LENGTH];
655 	int i, amtread;
656 	MD5_CTX ctx;
657 
658 	MD5Init(&ctx);
659 
660 	for (;;) {
661 		amtread = read(fdin, buf, sizeof (buf));
662 		if (amtread == 0)
663 			break;
664 		if (amtread <  0)
665 			return (1);
666 
667 		/* got some data.  Now update hash */
668 		MD5Update(&ctx, buf, amtread);
669 	}
670 
671 	/* done passing through data, calculate hash */
672 	MD5Final(hash, &ctx);
673 
674 	for (i = 0; i < MD5_DIGEST_LENGTH; i++)
675 		(void) sprintf(hash_str + (i*2), "%2.2x", hash[i]);
676 
677 	return (0);
678 }
679 
680 /*
681  * Used by 'bart create' with the '-I' option.  Return each entry into a 'buf'
682  * with the appropriate exit code: '0' for success and '-1' for failure.
683  */
684 static int
read_filelist(char * reloc_root,char ** argv,char * buf,size_t bufsize)685 read_filelist(char *reloc_root, char **argv, char *buf, size_t bufsize)
686 {
687 	static int		argv_index = -1;
688 	static boolean_t	read_stdinput = B_FALSE;
689 	char			temp_buf[PATH_MAX];
690 	char 			*cp;
691 
692 	/*
693 	 * INITIALIZATION:
694 	 * Setup this code so it knows whether or not to read sdtin.
695 	 * Also, if reading from argv, setup the index, "argv_index"
696 	 */
697 	if (argv_index == -1) {
698 		argv_index = 0;
699 
700 		/* In this case, no args after '-I', so read stdin */
701 		if (argv[0] == NULL)
702 			read_stdinput = B_TRUE;
703 	}
704 
705 	buf[0] = '\0';
706 
707 	if (read_stdinput) {
708 		if (fgets(temp_buf, PATH_MAX, stdin) == NULL)
709 			return (-1);
710 		cp = strtok(temp_buf, "\n");
711 	} else {
712 		cp = argv[argv_index++];
713 	}
714 
715 	if (cp == NULL)
716 		return (-1);
717 
718 	/*
719 	 * Unlike similar code elsewhere, avoid adding a leading
720 	 * slash for relative pathnames.
721 	 */
722 	(void) snprintf(buf, bufsize,
723 	    (reloc_root[0] == '\0' || cp[0] == '/') ? "%s%s" : "%s/%s",
724 	    reloc_root, cp);
725 
726 	return (0);
727 }
728