1/* 2 * Copyright (c) 2011-2014 Apple Inc. All Rights Reserved. 3 * 4 * @APPLE_LICENSE_HEADER_START@ 5 * 6 * This file contains Original Code and/or Modifications of Original Code 7 * as defined in and that are subject to the Apple Public Source License 8 * Version 2.0 (the 'License'). You may not use this file except in 9 * compliance with the License. Please obtain a copy of the License at 10 * http://www.opensource.apple.com/apsl/ and read it before using this 11 * file. 12 * 13 * The Original Code and all software distributed under the License are 14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 * Please see the License for the specific language governing rights and 19 * limitations under the License. 20 * 21 * @APPLE_LICENSE_HEADER_END@ 22 */ 23 24// 25// spctl - command-line access to system policy control (SecAssessment) 26// 27#include "spctl.h" 28#include "cs_utils.h" 29#include <security_utilities/unix++.h> 30#include <getopt.h> 31 32using namespace UnixPlusPlus; 33 34 35// 36// Operational mode 37// 38enum Operation { 39 doNothing, // none given (print usage) 40 doAssess, // assessment operation 41 doStatus, // master query status 42 doMasterEnable, // master honor assessment rejects 43 doMasterDisable, // master bypass assessment rejects 44 doDevIDStatus, // query devid status 45 doDevIDEnable, // devid honor assessment rejects 46 doDevIDDisable, // devid bypass assessment rejects 47 doAdd, // add authority rule 48 doRemove, // remove rule(s) 49 doRuleEnable, // (re)enable rule(s) 50 doRuleDisable, // disable rule(s) 51 doList, // list authority rules 52 doPurge, // purge object cache 53}; 54Operation operation = doNothing; 55 56 57// 58// Specification type 59// 60enum Specification { 61 specPath, // path to file(s) 62 specRequirement, // code requirement(s) 63 specAnchor, // path to anchor certificate(s) 64 specHash, // CodeDirectory hash(es) 65 specRule, // (removal by) rule number 66}; 67Specification specification = specPath; 68 69 70// 71// Command-line arguments and options 72// 73const char *assessmentType; 74SecAssessmentFlags assessmentFlags; 75SecAssessmentFlags outcomeFlags; 76const char *featureCheck; 77const char *label; 78const char *priority; 79const char *remarks; 80bool rawOutput; 81CFMutableDictionaryRef context = makeCFMutableDictionary(); 82// additional variables declared in cs_utils 83 84 85// 86// Feature set 87// 88static const char *features[] = { 89 NULL // sentinel 90}; 91 92 93// 94// Local functions 95// 96static void usage(); 97static void checkFeatures(const char *arg); 98 99static void assess(const char *target); 100static void addAuthority(const char *target); 101static void removeAuthority(const char *target); 102static void enableAuthority(const char *target); 103static void disableAuthority(const char *target); 104static void listAuthority(const char *target); 105static void status(Operation op); 106 107static CFTypeRef typeKey(const char *type); 108static string hashArgument(const char *s); 109static string fileHash(const char *path); 110 111 112// 113// Command-line options 114// 115enum { 116 optNone = 0, // null (no, absent) option 117 optAdd, 118 optAnchor, 119 optContext, 120 optContinue, 121 optDirect, 122 optEnforce, 123 optRuleEnable, 124 optRuleDisable, 125 optMasterEnable, 126 optMasterDisable, 127 optDevIDStatus, 128 optDevIDEnable, 129 optDevIDDisable, 130 optFeatures, 131 optHash, 132 optIgnoreCache, 133 optLabel, 134 optNoCache, 135 optPath, 136 optPriority, 137 optPurge, 138 optRawOutput, 139 optRemarks, 140 optRemove, 141 optRequirement, 142 optRule, 143 optStatus, 144}; 145 146const struct option options[] = { 147 { "add", no_argument, NULL, optAdd }, 148 { "anchor", no_argument, NULL, optAnchor }, 149 { "assess", no_argument, NULL, 'a' }, 150 { "context", required_argument, NULL, optContext }, 151 { "continue", no_argument, NULL, optContinue }, 152 { "direct", no_argument, NULL, 'D' }, 153 { "status", optional_argument, NULL, optStatus }, 154 { "enable", no_argument, NULL, optRuleEnable }, 155 { "enforce-assessment", no_argument, NULL, optEnforce }, 156 { "disable", no_argument, NULL, optRuleDisable }, 157 { "master-enable", no_argument, NULL, optMasterEnable }, 158 { "master-disable", no_argument, NULL, optMasterDisable }, 159 { "test-devid-status", no_argument, NULL, optDevIDStatus }, 160 { "test-devid-enable", no_argument, NULL, optDevIDEnable }, 161 { "test-devid-disable", no_argument, NULL, optDevIDDisable }, 162 { "features", optional_argument, NULL, optFeatures }, 163 { "hash", no_argument, NULL, optHash }, 164 { "ignore-cache", no_argument, NULL, optIgnoreCache }, 165 { "label", required_argument, NULL, optLabel }, 166 { "list", no_argument, NULL, 'l' }, 167 { "no-cache", no_argument, NULL, optNoCache }, 168 { "path", no_argument, NULL, optPath }, 169 { "priority", required_argument, NULL, optPriority }, 170 { "purge", no_argument, NULL, optPurge }, 171 { "raw", no_argument, NULL, optRawOutput }, 172 { "remarks", required_argument, NULL, optRemarks }, 173 { "remove", no_argument, NULL, optRemove }, 174 { "requirement", no_argument, NULL, optRequirement }, 175 { "rule", no_argument, NULL, optRule }, 176 { "type", required_argument, NULL, 't' }, 177 { "verbose", optional_argument, NULL, 'v' }, 178 { } 179}; 180 181 182// 183// main command-line driver 184// 185int main(int argc, char *argv[]) 186{ 187 try { 188 int arg, argslot; 189 while (argslot = -1, 190 (arg = getopt_long(argc, argv, "aDlt:v", options, &argslot)) != -1) 191 switch (arg) { 192 case 'a': 193 operation = doAssess; 194 break; 195 case 'D': 196 assessmentFlags |= kSecAssessmentFlagDirect; 197 outcomeFlags |= kSecAssessmentFlagDirect; 198 break; 199 case 'l': 200 operation = doList; 201 break; 202 case 't': 203 assessmentType = optarg; 204 break; 205 case 'v': 206 verbose++; 207 break; 208 209 case optAdd: 210 operation = doAdd; 211 break; 212 case optAnchor: 213 specification = specAnchor; 214 break; 215 case optContext: 216 if (const char *eq = strchr(optarg, '=')) { // key=value 217 CFDictionaryAddValue(context, CFTempString(string(optarg, eq - optarg)), CFTempString(eq+1)); 218 } else { // key, assume =true 219 CFDictionaryAddValue(context, CFTempString(optarg), kCFBooleanTrue); 220 } 221 break; 222 case optContinue: 223 continueOnError = true; 224 break; 225 case optEnforce: 226 assessmentFlags |= kSecAssessmentFlagEnforce; 227 outcomeFlags |= kSecAssessmentFlagEnforce; 228 break; 229 case optRuleDisable: 230 operation = doRuleDisable; 231 break; 232 case optRuleEnable: 233 operation = doRuleEnable; 234 break; 235 case optMasterDisable: 236 operation = doMasterDisable; 237 break; 238 case optMasterEnable: 239 operation = doMasterEnable; 240 break; 241 case optDevIDStatus: 242 operation = doDevIDStatus; 243 break; 244 case optDevIDDisable: 245 operation = doDevIDDisable; 246 break; 247 case optDevIDEnable: 248 operation = doDevIDEnable; 249 break; 250 case optFeatures: 251 featureCheck = optarg; 252 break; 253 case optHash: 254 specification = specHash; 255 break; 256 case optIgnoreCache: 257 assessmentFlags |= kSecAssessmentFlagIgnoreCache; 258 break; 259 case optLabel: 260 label = optarg; 261 break; 262 case optNoCache: 263 assessmentFlags |= kSecAssessmentFlagNoCache; 264 break; 265 case optPath: 266 specification = specPath; 267 break; 268 case optPriority: 269 priority = optarg; 270 break; 271 case optPurge: 272 operation = doPurge; 273 break; 274 case optRawOutput: 275 rawOutput = true; 276 break; 277 case optRemarks: 278 remarks = optarg; 279 break; 280 case optRemove: 281 operation = doRemove; 282 break; 283 case optRequirement: 284 specification = specRequirement; 285 break; 286 case optRule: 287 specification = specRule; 288 break; 289 case optStatus: 290 operation = doStatus; 291 break; 292 293 case '?': 294 usage(); 295 } 296 297 if (featureCheck) { 298 checkFeatures(featureCheck); 299 if (operation == doNothing) 300 exit(0); 301 } 302 303 // dispatch operations with no arguments 304 switch (operation) { 305 case doNothing: 306 usage(); 307 case doStatus: 308 case doMasterEnable: 309 case doMasterDisable: 310 case doDevIDStatus: 311 case doDevIDEnable: 312 case doDevIDDisable: 313 if (optind != argc) 314 usage(); 315 status(operation); 316 exit(0); 317 case doRemove: // optional arguments 318 if (optind == argc) { 319 removeAuthority(NULL); 320 exit(0); 321 } 322 break; 323 case doRuleEnable: 324 if (optind == argc) { 325 enableAuthority(NULL); 326 exit(0); 327 } 328 break; 329 case doRuleDisable: 330 if (optind == argc) { 331 disableAuthority(NULL); 332 exit(0); 333 } 334 break; 335 case doList: 336 if (optind == argc) { 337 listAuthority(NULL); 338 exit(0); 339 } 340 break; 341 default: 342 if (optind == argc) 343 usage(); 344 break; 345 } 346 347 // operate on paths given after options 348 for ( ; optind < argc; optind++) { 349 const char *target = argv[optind]; 350 try { 351 switch (operation) { 352 case doAssess: 353 assess(target); 354 break; 355 case doAdd: 356 addAuthority(target); 357 break; 358 case doRemove: 359 removeAuthority(target); 360 break; 361 case doRuleEnable: 362 enableAuthority(target); 363 break; 364 case doRuleDisable: 365 disableAuthority(target); 366 break; 367 case doList: 368 listAuthority(target); 369 break; 370 default: 371 assert(false); 372 } 373 } catch (...) { 374 diagnose(target); 375 if (!exitcode) 376 exitcode = exitFailure; 377 if (!continueOnError) 378 exit(exitFailure); 379 } 380 } 381 382 } catch (...) { 383 diagnose(NULL, exitFailure); 384 } 385 386 exit(exitcode); 387} 388 389void usage() 390{ 391 fprintf(stderr, "Usage: spctl --assess [--type type] [-v] path ... # assessment\n" 392 " spctl --add [--type type] [--path|--requirement|--anchor|--hash] spec ... # add rule(s)\n" 393 " spctl [--enable|--disable|--remove] [--type type] [--path|--requirement|--anchor|--hash|--rule] spec # change rule(s)\n" 394 " spctl --status | --master-enable | --master-disable # system master switch\n" 395 ); 396 exit(exitUsage); 397} 398 399 400// 401// Perform an assessment operation. 402// This does not change anything (except possibly, indirectly, the object cache). 403// 404void assess(const char *target) 405{ 406 SecAssessmentFlags flags = assessmentFlags; 407 if (verbose > 1) 408 flags |= kSecAssessmentFlagRequestOrigin; 409 if (assessmentType) 410 CFDictionaryAddValue(context, kSecAssessmentContextKeyOperation, typeKey(assessmentType)); 411 412 CheckedRef<SecAssessmentRef> ass; 413 ass.check(SecAssessmentCreate(CFTempURL(target), flags, context, ass)); 414 CheckedRef<CFDictionaryRef> outcome; 415 outcome.check(SecAssessmentCopyResult(ass, outcomeFlags, outcome)); 416 417 CFDictionary result(outcome.get(), 0); 418 bool success = result.get<CFBooleanRef>(kSecAssessmentAssessmentVerdict) == kCFBooleanTrue; 419 CFDictionary authority(result.get<CFDictionaryRef>(kSecAssessmentAssessmentAuthority), 0); 420 CFStringRef source = NULL; 421 if (authority) 422 source = authority.get<CFStringRef>(kSecAssessmentAssessmentSource); 423 424 // if the result is a whitelisted weak signature, bend the polite (but not raw) output to our will 425 if (!rawOutput && authority) { 426 if (CFBooleanRef weak = authority.get<CFBooleanRef>(kSecAssessmentAssessmentWeakSignature)) 427 if (CFEqual(weak, kCFBooleanTrue)) { 428 // succeeded only because of weak-signature whitelist. Report it as failed 429 success = false; 430 if (source && CFEqual(source, CFSTR("allowed cdhash"))) 431 source = CFSTR("matched cdhash"); 432 } 433 } 434 435 if (success) { 436 note(1, "%s: accepted", target); 437 } else { 438 note(0, "%s: rejected", target); 439 if (!exitcode) 440 exitcode = exitNoverify; 441 } 442 443 if (rawOutput) { 444 if (CFRef<CFDataRef> xml = makeCFData(outcome.get())) 445 fwrite(CFDataGetBytePtr(xml), CFDataGetLength(xml), 1, stdout); 446 } else if (verbose) { 447 if (authority) { 448 if (source) 449 note(1, "source=%s", cfString(source).c_str()); 450 if (CFBooleanRef cached = authority.get<CFBooleanRef>(kSecAssessmentAssessmentFromCache)) { 451 if (cached == kCFBooleanFalse) 452 note(2, "cache=no"); 453 else if (CFNumberRef row = authority.get<CFNumberRef>(kSecAssessmentAssessmentAuthorityRow)) 454 note(2, "cache=yes,row %d", cfNumber<int>(row)); 455 else 456 note(2, "cache=yes"); 457 } 458 if (CFStringRef override = authority.get<CFStringRef>(kSecAssessmentAssessmentAuthorityOverride)) 459 note(0, "override=%s", cfString(override).c_str()); 460 } else 461 note(1, "authority=none"); 462 } 463 if (CFStringRef originator = result.get<CFStringRef>(kSecAssessmentAssessmentOriginator)) 464 note(2, "origin=%s", cfString(originator).c_str()); 465} 466 467 468// 469// Apply a change to the system-wide authority configuration. 470// These are all privileged operations, of course. 471// 472static CFDictionaryRef updateOperation(const char *target, CFMutableDictionaryRef context, 473 CFStringRef operation) 474{ 475 SecCSFlags flags = assessmentFlags; 476 CFDictionaryAddValue(context, kSecAssessmentContextKeyUpdate, operation); 477 if (assessmentType) 478 CFDictionaryAddValue(context, kSecAssessmentContextKeyOperation, typeKey(assessmentType)); 479 480 CFRef<CFTypeRef> subject; 481 if (target) 482 switch (specification) { 483 case specPath: 484 { 485 subject = makeCFURL(target); 486 // For add operations, add a bookmark so we get icons 487 if (operation == kSecAssessmentUpdateOperationAdd) { 488 CFRef<CFDataRef> bookmark = CFURLCreateBookmarkData(NULL, subject.as<CFURLRef>(), 0, NULL, NULL, NULL); 489 if (bookmark) 490 CFDictionaryAddValue(context, kSecAssessmentRuleKeyBookmark, bookmark); 491 } 492 break; 493 } 494 case specRequirement: 495 MacOSError::check(SecRequirementCreateWithString(CFTempString(target), 496 kSecCSDefaultFlags, (SecRequirementRef *)&subject.aref())); 497 break; 498 case specAnchor: 499 { 500 string reqString; 501 if (target[0] == '/') { // assume path to anchor cert on disk 502 reqString = "anchor " + fileHash(target); 503 } else { 504 reqString = "anchor " + hashArgument(target); 505 } 506 MacOSError::check(SecRequirementCreateWithString(CFTempString(reqString), 507 kSecCSDefaultFlags, (SecRequirementRef *)&subject.aref())); 508 break; 509 } 510 case specHash: 511 { 512 string reqString = "cdhash " + hashArgument(target); 513 MacOSError::check(SecRequirementCreateWithString(CFTempString(reqString), 514 kSecCSDefaultFlags, (SecRequirementRef *)&subject.aref())); 515 break; 516 } 517 case specRule: 518 { 519 if (operation == kSecAssessmentUpdateOperationAdd) 520 fail("cannot insert by rule number"); 521 char *end; 522 uint64_t rule = strtol(target, &end, 0); 523 if (*end) 524 fail("%s: invalid rule number", target); 525 subject.take(CFTempNumber(rule)); 526 break; 527 } 528 } 529 530 if (label) 531 CFDictionaryAddValue(context, kSecAssessmentUpdateKeyLabel, CFTempString(label)); 532 if (priority) { 533 char *end; 534 double pri = strtod(priority, &end); 535 if (*end) // empty or bad conversion 536 fail("%s: invalid rule priority", priority); 537 CFDictionaryAddValue(context, kSecAssessmentUpdateKeyPriority, CFTempNumber(pri)); 538 } 539 if (remarks) 540 CFDictionaryAddValue(context, kSecAssessmentUpdateKeyRemarks, CFTempString(remarks)); 541 542 ErrorCheck check; 543 CFRef<CFDictionaryRef> outcome = SecAssessmentCopyUpdate(subject.get(), flags, context, check); 544 check(outcome); 545 546 if (rawOutput) 547 if (CFRef<CFDataRef> xml = makeCFData(outcome.get())) 548 fwrite(CFDataGetBytePtr(xml), CFDataGetLength(xml), 1, stdout); 549 550 return outcome.yield(); 551} 552 553void addAuthority(const char *target) 554{ 555 CFDictionary result(updateOperation(target, context, kSecAssessmentUpdateOperationAdd), noErr); 556 if (verbose && !rawOutput) 557 printf("Created rule %lld\n", cfNumber<long long>(result.get<CFNumberRef>(kSecAssessmentUpdateKeyRow))); 558} 559 560 561void removeAuthority(const char *target) 562{ 563 CFDictionary result(updateOperation(target, context, kSecAssessmentUpdateOperationRemove), noErr); 564 if (verbose && !rawOutput) 565 printf("Removed %lld rule(s)\n", cfNumber<long long>(result.get<CFNumberRef>(kSecAssessmentUpdateKeyCount))); 566} 567 568void enableAuthority(const char *target) 569{ 570 CFDictionary result(updateOperation(target, context, kSecAssessmentUpdateOperationEnable), noErr); 571 if (verbose && !rawOutput) 572 printf("Enabled %lld rule(s)\n", cfNumber<long long>(result.get<CFNumberRef>(kSecAssessmentUpdateKeyCount))); 573} 574 575void disableAuthority(const char *target) 576{ 577 CFDictionary result(updateOperation(target, context, kSecAssessmentUpdateOperationDisable), noErr); 578 if (verbose && !rawOutput) 579 printf("Disabled %lld rule(s)\n", cfNumber<long long>(result.get<CFNumberRef>(kSecAssessmentUpdateKeyCount))); 580} 581 582void listAuthority(const char *target) 583{ 584 CFDictionary result(updateOperation(target, context, kSecAssessmentUpdateOperationFind), noErr); 585 if (rawOutput) 586 return; 587 CFArrayRef rules = result.get<CFArrayRef>(kSecAssessmentUpdateKeyFound); 588 CFIndex count = CFArrayGetCount(rules); 589 for (CFIndex n = 0; n < count; n++) { 590 CFDictionary rule(CFArrayGetValueAtIndex(rules, n), noErr); 591 string typeString = "?"; 592 if (CFStringRef type = rule.get<CFStringRef>(kSecAssessmentRuleKeyType)) { 593 typeString = cfString(type); 594 string::size_type colon = typeString.find(':'); 595 if (colon != string::npos) 596 typeString = typeString.substr(colon+1); 597 } 598 string label = "UNLABELED"; 599 if (CFStringRef lab = rule.get<CFStringRef>(kSecAssessmentRuleKeyLabel)) 600 label = cfString(lab); 601 printf("%lld[%s] P%g %s %s", 602 cfNumber<long long>(rule.get<CFNumberRef>(kSecAssessmentRuleKeyID)), 603 label.c_str(), 604 cfNumber<double>(rule.get<CFNumberRef>(kSecAssessmentRuleKeyPriority)), 605 (rule.get<CFBooleanRef>(kSecAssessmentRuleKeyAllow) == kCFBooleanTrue) ? "allow" : "deny", 606 typeString.c_str() 607 ); 608 if (CFStringRef remarks = rule.get<CFStringRef>(kSecAssessmentRuleKeyRemarks)) 609 printf(" [%s]", cfString(remarks).c_str()); 610 printf("\n"); 611 printf("\t%s\n", 612 cfString(rule.get<CFStringRef>(kSecAssessmentRuleKeyRequirement)).c_str() 613 ); 614 } 615} 616 617 618// 619// Manipulate the master status. 620// This reports on, or changes, the master enable status. 621// It does not actually affect the authority database, though 622// it may tell the system to bypass it altogether. 623// 624void status(Operation op) 625{ 626 ErrorCheck check; 627 CFBooleanRef state; 628 switch (op) { 629 case doStatus: 630 check(SecAssessmentControl(CFSTR("ui-status"), &state, check)); 631 if (state == kCFBooleanTrue) { 632 printf("assessments enabled\n"); 633 if (verbose > 0) { 634 check(SecAssessmentControl(CFSTR("ui-get-devid"), &state, check)); 635 if (state == kCFBooleanTrue) 636 printf("developer id enabled\n"); 637 else 638 printf("developer id disabled\n"); 639 } 640 exit(0); 641 } else { 642 printf("assessments disabled\n"); 643 exit(1); 644 } 645 case doDevIDStatus: 646 check(SecAssessmentControl(CFSTR("ui-get-devid"), &state, check)); 647 if (state == kCFBooleanTrue) { 648 printf("devid enabled\n"); 649 exit(0); 650 } else { 651 printf("devid disabled\n"); 652 exit(1); 653 } 654 case doMasterEnable: 655 check(SecAssessmentControl(CFSTR("ui-enable"), NULL, check)); 656 exit(0); 657 case doMasterDisable: 658 check(SecAssessmentControl(CFSTR("ui-disable"), NULL, check)); 659 exit(0); 660 case doDevIDEnable: 661 check(SecAssessmentControl(CFSTR("ui-enable-devid"), NULL, check)); 662 exit(0); 663 case doDevIDDisable: 664 check(SecAssessmentControl(CFSTR("ui-disable-devid"), NULL, check)); 665 exit(0); 666 default: 667 assert(false); 668 } 669} 670 671 672// 673// Support helper functions 674// 675static CFTypeRef typeKey(const char *type) 676{ 677 if (!strncmp(type, "execute", strlen(type))) 678 return kSecAssessmentOperationTypeExecute; 679 else if (!strncmp(type, "install", strlen(type))) 680 return kSecAssessmentOperationTypeInstall; 681 else if (!strncmp(type, "open", strlen(type))) 682 return kSecAssessmentOperationTypeOpenDocument; 683 else 684 fail("%s: unrecognized assessment type", type); 685} 686 687static string hashArgument(const char *s) 688{ 689 for (const char *p = s; *p; p++) 690 if (!isxdigit(*p)) 691 fail("%s: invalid hash specification", s); 692 return string("H\"") + s + "\""; 693} 694 695static string fileHash(const char *path) 696{ 697 CFRef<CFDataRef> certData = cfLoadFile(path); 698 SHA1 hash; 699 hash.update(CFDataGetBytePtr(certData), CFDataGetLength(certData)); 700 SHA1::Digest digest; 701 hash.finish(digest); 702 string s; 703 for (const SHA1::Byte *p = digest; p < digest + sizeof(digest); p++) { 704 char buf[3]; 705 snprintf(buf, sizeof(buf), "%02.2x", *p); 706 s += buf; 707 } 708 return hashArgument(s.c_str()); 709} 710 711 712// 713// Exit unless each of the comma-separated feature names is supported 714// by this version of spctl(8). 715// 716void checkFeatures(const char *arg) 717{ 718 while (true) { 719 const char *comma = strchr(arg, ','); 720 string feature = comma ? string(arg, comma-arg) : arg; 721 if (feature.empty()) 722 fail("Invalid feature name"); 723 const char **p; 724 for (p = features; *p && feature != *p; p++) ; 725 if (!*p) 726 fail("%s: not supported in this version", feature.c_str()); 727 if (comma) { 728 arg = comma + 1; 729 if (!*arg) // tolerate trailing comma 730 break; 731 } else { 732 break; 733 } 734 } 735} 736