|
9 | 9 | import pkgutil |
10 | 10 | import re |
11 | 11 | import subprocess # nosec |
12 | | -import time |
13 | 12 | import uuid |
14 | 13 | from enum import Enum |
15 | 14 | from shutil import which |
|
35 | 34 |
|
36 | 35 | from .__version__ import __version__ |
37 | 36 | from .azcopy import azcopy_sync |
38 | | -from .backend import Backend, BackendConfig, ContainerWrapper, wait |
39 | | -from .ssh import build_ssh_command, ssh_connect, temp_file |
| 37 | +from .backend import Backend, BackendConfig, ContainerWrapper |
40 | 38 |
|
41 | 39 | UUID_EXPANSION = TypeVar("UUID_EXPANSION", UUID, str) |
42 | 40 |
|
@@ -530,316 +528,6 @@ def _download_tasks( |
530 | 528 | azcopy_sync(to_download[name], outdir) |
531 | 529 |
|
532 | 530 |
|
533 | | -class Repro(Endpoint): |
534 | | - """Interact with Reproduction VMs""" |
535 | | - |
536 | | - endpoint = "repro_vms" |
537 | | - |
538 | | - def get(self, vm_id: UUID_EXPANSION) -> models.Repro: |
539 | | - """get information about a Reproduction VM""" |
540 | | - vm_id_expanded = self._disambiguate_uuid( |
541 | | - "vm_id", vm_id, lambda: [str(x.vm_id) for x in self.list()] |
542 | | - ) |
543 | | - |
544 | | - self.logger.debug("get repro vm: %s", vm_id_expanded) |
545 | | - return self._req_model( |
546 | | - "GET", models.Repro, data=requests.ReproGet(vm_id=vm_id_expanded) |
547 | | - ) |
548 | | - |
549 | | - def get_files( |
550 | | - self, |
551 | | - report_container: primitives.Container, |
552 | | - report_name: str, |
553 | | - include_setup: bool = False, |
554 | | - output_dir: primitives.Directory = primitives.Directory("."), |
555 | | - ) -> None: |
556 | | - """downloads the files necessary to locally repro the crash from a given report""" |
557 | | - report_bytes = self.onefuzz.containers.files.get(report_container, report_name) |
558 | | - report = json.loads(report_bytes) |
559 | | - |
560 | | - crash_info = { |
561 | | - "input_blob_container": primitives.Container(""), |
562 | | - "input_blob_name": "", |
563 | | - "job_id": "", |
564 | | - } |
565 | | - if "input_blob" in report: |
566 | | - crash_info["input_blob_container"] = report["input_blob"]["container"] |
567 | | - crash_info["input_blob_name"] = report["input_blob"]["name"] |
568 | | - crash_info["job_id"] = report["job_id"] |
569 | | - elif "crash_test_result" in report and "original_crash_test_result" in report: |
570 | | - if report["original_crash_test_result"]["crash_report"] is None: |
571 | | - self.logger.error( |
572 | | - "No crash report found in the original crash test result, repro files cannot be retrieved" |
573 | | - ) |
574 | | - return |
575 | | - elif report["crash_test_result"]["crash_report"] is None: |
576 | | - self.logger.info( |
577 | | - "No crash report found in the new crash test result, falling back on the original crash test result for job_id" |
578 | | - "Note: if using --include_setup, the downloaded fuzzer binaries may be out-of-date" |
579 | | - ) |
580 | | - |
581 | | - original_report = report["original_crash_test_result"]["crash_report"] |
582 | | - new_report = ( |
583 | | - report["crash_test_result"]["crash_report"] or original_report |
584 | | - ) # fallback on original_report |
585 | | - |
586 | | - crash_info["input_blob_container"] = original_report["input_blob"][ |
587 | | - "container" |
588 | | - ] |
589 | | - crash_info["input_blob_name"] = original_report["input_blob"]["name"] |
590 | | - crash_info["job_id"] = new_report["job_id"] |
591 | | - else: |
592 | | - self.logger.error( |
593 | | - "Encountered an unhandled report format, repro files cannot be retrieved" |
594 | | - ) |
595 | | - return |
596 | | - |
597 | | - self.logger.info( |
598 | | - "downloading files necessary to locally repro crash %s", |
599 | | - crash_info["input_blob_name"], |
600 | | - ) |
601 | | - self.onefuzz.containers.files.download( |
602 | | - primitives.Container(crash_info["input_blob_container"]), |
603 | | - crash_info["input_blob_name"], |
604 | | - os.path.join(output_dir, crash_info["input_blob_name"]), |
605 | | - ) |
606 | | - |
607 | | - if include_setup: |
608 | | - setup_container = list( |
609 | | - self.onefuzz.jobs.containers.list( |
610 | | - crash_info["job_id"], enums.ContainerType.setup |
611 | | - ) |
612 | | - )[0] |
613 | | - |
614 | | - self.onefuzz.containers.files.download_dir( |
615 | | - primitives.Container(setup_container), output_dir |
616 | | - ) |
617 | | - |
618 | | - def create( |
619 | | - self, container: primitives.Container, path: str, duration: int = 24 |
620 | | - ) -> models.Repro: |
621 | | - """Create a Reproduction VM from a Crash Report""" |
622 | | - self.logger.info( |
623 | | - "creating repro vm: %s %s (%d hours)", container, path, duration |
624 | | - ) |
625 | | - return self._req_model( |
626 | | - "POST", |
627 | | - models.Repro, |
628 | | - data=models.ReproConfig(container=container, path=path, duration=duration), |
629 | | - ) |
630 | | - |
631 | | - def delete(self, vm_id: UUID_EXPANSION) -> models.Repro: |
632 | | - """Delete a Reproduction VM""" |
633 | | - vm_id_expanded = self._disambiguate_uuid( |
634 | | - "vm_id", vm_id, lambda: [str(x.vm_id) for x in self.list()] |
635 | | - ) |
636 | | - |
637 | | - self.logger.debug("deleting repro vm: %s", vm_id_expanded) |
638 | | - return self._req_model( |
639 | | - "DELETE", models.Repro, data=requests.ReproGet(vm_id=vm_id_expanded) |
640 | | - ) |
641 | | - |
642 | | - def list(self) -> List[models.Repro]: |
643 | | - """List all VMs""" |
644 | | - self.logger.debug("listing repro vms") |
645 | | - return self._req_model_list("GET", models.Repro, data=requests.ReproGet()) |
646 | | - |
647 | | - def _dbg_linux( |
648 | | - self, repro: models.Repro, debug_command: Optional[str] |
649 | | - ) -> Optional[str]: |
650 | | - """Launch gdb with GDB script that includes 'target remote | ssh ...'""" |
651 | | - |
652 | | - if ( |
653 | | - repro.auth is None |
654 | | - or repro.ip is None |
655 | | - or repro.state != enums.VmState.running |
656 | | - ): |
657 | | - raise Exception("vm setup failed: %s" % repro.state) |
658 | | - |
659 | | - with build_ssh_command( |
660 | | - repro.ip, repro.auth.private_key, command="-T" |
661 | | - ) as ssh_cmd: |
662 | | - gdb_script = [ |
663 | | - "target remote | %s sudo /onefuzz/bin/repro-stdout.sh" |
664 | | - % " ".join(ssh_cmd) |
665 | | - ] |
666 | | - |
667 | | - if debug_command: |
668 | | - gdb_script += [debug_command, "quit"] |
669 | | - |
670 | | - with temp_file("gdb.script", "\n".join(gdb_script)) as gdb_script_path: |
671 | | - dbg = ["gdb", "--silent", "--command", gdb_script_path] |
672 | | - |
673 | | - if debug_command: |
674 | | - dbg += ["--batch"] |
675 | | - |
676 | | - try: |
677 | | - # security note: dbg is built from content coming from |
678 | | - # the server, which is trusted in this context. |
679 | | - return subprocess.run( # nosec |
680 | | - dbg, stdout=subprocess.PIPE, stderr=subprocess.STDOUT |
681 | | - ).stdout.decode(errors="ignore") |
682 | | - except subprocess.CalledProcessError as err: |
683 | | - self.logger.error( |
684 | | - "debug failed: %s", err.output.decode(errors="ignore") |
685 | | - ) |
686 | | - raise err |
687 | | - else: |
688 | | - # security note: dbg is built from content coming from the |
689 | | - # server, which is trusted in this context. |
690 | | - subprocess.call(dbg) # nosec |
691 | | - return None |
692 | | - |
693 | | - def _dbg_windows( |
694 | | - self, |
695 | | - repro: models.Repro, |
696 | | - debug_command: Optional[str], |
697 | | - retry_limit: Optional[int], |
698 | | - ) -> Optional[str]: |
699 | | - """Setup an SSH tunnel, then connect via CDB over SSH tunnel""" |
700 | | - |
701 | | - if ( |
702 | | - repro.auth is None |
703 | | - or repro.ip is None |
704 | | - or repro.state != enums.VmState.running |
705 | | - ): |
706 | | - raise Exception("vm setup failed: %s" % repro.state) |
707 | | - |
708 | | - retry_count = 0 |
709 | | - bind_all = which("wslpath") is not None and repro.os == enums.OS.windows |
710 | | - proxy = "*:" + REPRO_SSH_FORWARD if bind_all else REPRO_SSH_FORWARD |
711 | | - while retry_limit is None or retry_count <= retry_limit: |
712 | | - if retry_limit: |
713 | | - retry_count = retry_count + 1 |
714 | | - with ssh_connect(repro.ip, repro.auth.private_key, proxy=proxy): |
715 | | - dbg = ["cdb.exe", "-remote", "tcp:port=1337,server=localhost"] |
716 | | - if debug_command: |
717 | | - dbg_script = [debug_command, "qq"] |
718 | | - with temp_file( |
719 | | - "db.script", "\r\n".join(dbg_script) |
720 | | - ) as dbg_script_path: |
721 | | - dbg += ["-cf", _wsl_path(dbg_script_path)] |
722 | | - |
723 | | - logging.debug("launching: %s", dbg) |
724 | | - try: |
725 | | - # security note: dbg is built from content coming from the server, |
726 | | - # which is trusted in this context. |
727 | | - return subprocess.run( # nosec |
728 | | - dbg, stdout=subprocess.PIPE, stderr=subprocess.STDOUT |
729 | | - ).stdout.decode(errors="ignore") |
730 | | - except subprocess.CalledProcessError as err: |
731 | | - if err.returncode == 0x8007274D: |
732 | | - self.logger.info( |
733 | | - "failed to connect to debug-server trying again in 10 seconds..." |
734 | | - ) |
735 | | - time.sleep(10.0) |
736 | | - else: |
737 | | - self.logger.error( |
738 | | - "debug failed: %s", |
739 | | - err.output.decode(errors="ignore"), |
740 | | - ) |
741 | | - raise err |
742 | | - else: |
743 | | - logging.debug("launching: %s", dbg) |
744 | | - # security note: dbg is built from content coming from the |
745 | | - # server, which is trusted in this context. |
746 | | - try: |
747 | | - subprocess.check_call(dbg) # nosec |
748 | | - return None |
749 | | - except subprocess.CalledProcessError as err: |
750 | | - if err.returncode == 0x8007274D: |
751 | | - self.logger.info( |
752 | | - "failed to connect to debug-server trying again in 10 seconds..." |
753 | | - ) |
754 | | - time.sleep(10.0) |
755 | | - else: |
756 | | - return None |
757 | | - |
758 | | - if retry_limit is not None: |
759 | | - self.logger.info( |
760 | | - f"failed to connect to debug-server after {retry_limit} attempts. Please try again later " |
761 | | - + f"with onefuzz debug connect {repro.vm_id}" |
762 | | - ) |
763 | | - return None |
764 | | - |
765 | | - def connect( |
766 | | - self, |
767 | | - vm_id: UUID_EXPANSION, |
768 | | - delete_after_use: bool = False, |
769 | | - debug_command: Optional[str] = None, |
770 | | - retry_limit: Optional[int] = None, |
771 | | - ) -> Optional[str]: |
772 | | - """Connect to an existing Reproduction VM""" |
773 | | - |
774 | | - self.logger.info("connecting to reproduction VM: %s", vm_id) |
775 | | - |
776 | | - if which("ssh") is None: |
777 | | - raise Exception("unable to find ssh on local machine") |
778 | | - |
779 | | - def missing_os() -> Tuple[bool, str, models.Repro]: |
780 | | - repro = self.get(vm_id) |
781 | | - return ( |
782 | | - repro.os is not None, |
783 | | - "waiting for os determination", |
784 | | - repro, |
785 | | - ) |
786 | | - |
787 | | - repro = wait(missing_os) |
788 | | - |
789 | | - if repro.os == enums.OS.windows: |
790 | | - if which("cdb.exe") is None: |
791 | | - raise Exception("unable to find cdb.exe on local machine") |
792 | | - if repro.os == enums.OS.linux: |
793 | | - if which("gdb") is None: |
794 | | - raise Exception("unable to find gdb on local machine") |
795 | | - |
796 | | - def func() -> Tuple[bool, str, models.Repro]: |
797 | | - repro = self.get(vm_id) |
798 | | - state = repro.state |
799 | | - return ( |
800 | | - repro.auth is not None |
801 | | - and repro.ip is not None |
802 | | - and state not in [enums.VmState.init, enums.VmState.extensions_launch], |
803 | | - "launching reproducing vm. current state: %s" % state, |
804 | | - repro, |
805 | | - ) |
806 | | - |
807 | | - repro = wait(func) |
808 | | - # give time for debug server to initialize |
809 | | - time.sleep(30.0) |
810 | | - result: Optional[str] = None |
811 | | - if repro.os == enums.OS.windows: |
812 | | - result = self._dbg_windows(repro, debug_command, retry_limit) |
813 | | - elif repro.os == enums.OS.linux: |
814 | | - result = self._dbg_linux(repro, debug_command) |
815 | | - else: |
816 | | - raise NotImplementedError |
817 | | - |
818 | | - if delete_after_use: |
819 | | - self.logger.debug("deleting vm %s", repro.vm_id) |
820 | | - self.delete(repro.vm_id) |
821 | | - |
822 | | - return result |
823 | | - |
824 | | - def create_and_connect( |
825 | | - self, |
826 | | - container: primitives.Container, |
827 | | - path: str, |
828 | | - duration: int = 24, |
829 | | - delete_after_use: bool = False, |
830 | | - debug_command: Optional[str] = None, |
831 | | - retry_limit: Optional[int] = None, |
832 | | - ) -> Optional[str]: |
833 | | - """Create and connect to a Reproduction VM""" |
834 | | - repro = self.create(container, path, duration=duration) |
835 | | - return self.connect( |
836 | | - repro.vm_id, |
837 | | - delete_after_use=delete_after_use, |
838 | | - debug_command=debug_command, |
839 | | - retry_limit=retry_limit, |
840 | | - ) |
841 | | - |
842 | | - |
843 | 531 | class Notifications(Endpoint): |
844 | 532 | """Interact with models.Notifications""" |
845 | 533 |
|
@@ -1900,7 +1588,6 @@ def __init__( |
1900 | 1588 | client_secret=client_secret, |
1901 | 1589 | ) |
1902 | 1590 | self.containers = Containers(self) |
1903 | | - self.repro = Repro(self) |
1904 | 1591 | self.notifications = Notifications(self) |
1905 | 1592 | self.tasks = Tasks(self) |
1906 | 1593 | self.jobs = Jobs(self) |
|
0 commit comments