This patch provides all necessary changes to move OSv kernel by 1 GiB higher in virtual memory space to start at 0x40200000. Most changes involve adding or subtracting 0x40000000 (OSV_KERNEL_VM_SHIFT) in all relevant places. Please note that the kernel is still loaded at 2MiB in physical memory.
The motivation for this patch is to make as much space as possible (or just enough) in virtual memory to allow running unmodified Linux non-PIE executables (issue #190). Even though due to the advancement of ASLR more and more applications are PIEs (Position Independent Executables) which are pretty well supported by OSv, there are still many non-PIEs (Position Dependent Executables) that are out. The most prominent one is actualy JVM whose most distributions come with tiny (~20K) bootstrap java non-PIE executable. There are many other examples where small non-PIE executable loads other shared libraries. As issue #1043 explains there are at least 3 possible solutions and this patch implements the 3rd last one described there. Please note that in future with little effort we could provide slightly beter scheme for OSV_KERNEL_VM_SHIFT that would allow us to place the kernel even higher at the end of the 2GiB limit (small memory model) and thus support virtually any non-PIE built using small memory model. Due to its impact this patch has been tested on following hypervisors: - QEMU without KVM - QEMU with KVM - Firecracker - VirtualBox 6 - VMware Player - XEN on EC2 - XEN locally in HVM mode Fixes #1043 Signed-off-by: Waldemar Kozaczuk <jwkozac...@gmail.com> --- Makefile | 7 +++++- arch/x64/arch-setup.cc | 39 ++++++++++++++++++------------ arch/x64/boot.S | 41 +++++++++++++++++++++++++------- arch/x64/entry-xen.S | 3 ++- arch/x64/loader.ld | 50 ++++++++++++++++++++------------------- arch/x64/vmlinux-boot64.S | 6 +++-- arch/x64/xen.cc | 4 ++-- core/elf.cc | 2 +- core/mmu.cc | 9 ++++--- loader.cc | 3 ++- 10 files changed, 103 insertions(+), 61 deletions(-) diff --git a/Makefile b/Makefile index 314e74f4..3c5a2077 100644 --- a/Makefile +++ b/Makefile @@ -312,7 +312,7 @@ gcc-sysroot = $(if $(CROSS_PREFIX), --sysroot external/$(arch)/gcc.bin) \ # To add something that will *not* be part of the main kernel, you can do: # # mydir/*.o EXTRA_FLAGS = <MY_STUFF> -EXTRA_FLAGS = -D__OSV_CORE__ -DOSV_KERNEL_BASE=$(kernel_base) -DOSV_LZKERNEL_BASE=$(lzkernel_base) +EXTRA_FLAGS = -D__OSV_CORE__ -DOSV_KERNEL_BASE=$(kernel_base) -DOSV_KERNEL_VM_SHIFT=$(kernel_vm_shift) -DOSV_LZKERNEL_BASE=$(lzkernel_base) EXTRA_LIBS = COMMON = $(autodepend) -g -Wall -Wno-pointer-arith $(CFLAGS_WERROR) -Wformat=0 -Wno-format-security \ -D __BSD_VISIBLE=1 -U _FORTIFY_SOURCE -fno-stack-protector $(INCLUDES) \ @@ -421,6 +421,7 @@ ifeq ($(arch),x64) # lzkernel_base is where the compressed kernel is loaded from disk. kernel_base := 0x200000 lzkernel_base := 0x100000 +kernel_vm_base := 0x40200000 $(out)/arch/x64/boot16.o: $(out)/lzloader.elf $(out)/boot.bin: arch/x64/boot16.ld $(out)/arch/x64/boot16.o @@ -480,6 +481,7 @@ endif # x64 ifeq ($(arch),aarch64) kernel_base := 0x40080000 +kernel_vm_base := 0x40080000 include $(libfdt_base)/Makefile.libfdt libfdt-source := $(patsubst %.c, $(libfdt_base)/%.c, $(LIBFDT_SRCS)) @@ -500,6 +502,8 @@ $(out)/loader.img: $(out)/preboot.bin $(out)/loader-stripped.elf endif # aarch64 +kernel_vm_shift := $(shell printf "0x%X" $(shell expr $$(( $(kernel_vm_base) - $(kernel_base) )) )) + $(out)/bsd/sys/crypto/rijndael/rijndael-api-fst.o: COMMON+=-fno-strict-aliasing $(out)/bsd/sys/crypto/sha2/sha2.o: COMMON+=-fno-strict-aliasing $(out)/bsd/sys/net/route.o: COMMON+=-fno-strict-aliasing @@ -1873,6 +1877,7 @@ stage1: $(stage1_targets) links $(out)/loader.elf: $(stage1_targets) arch/$(arch)/loader.ld $(out)/bootfs.o $(call quiet, $(LD) -o $@ --defsym=OSV_KERNEL_BASE=$(kernel_base) \ + --defsym=OSV_KERNEL_VM_BASE=$(kernel_vm_base) --defsym=OSV_KERNEL_VM_SHIFT=$(kernel_vm_shift) \ -Bdynamic --export-dynamic --eh-frame-hdr --enable-new-dtags \ $(^:%.ld=-T %.ld) \ --whole-archive \ diff --git a/arch/x64/arch-setup.cc b/arch/x64/arch-setup.cc index 62236486..210f2d7e 100644 --- a/arch/x64/arch-setup.cc +++ b/arch/x64/arch-setup.cc @@ -85,12 +85,15 @@ extern boot_time_chart boot_time; // it by placing address of start32 at the known offset at memory // as defined by section .start32_address in loader.ld extern "C" void start32(); -void * __attribute__((section (".start32_address"))) start32_address = reinterpret_cast<void*>(&start32); +void * __attribute__((section (".start32_address"))) start32_address = + reinterpret_cast<void*>((long)&start32 - OSV_KERNEL_VM_SHIFT); void arch_setup_free_memory() { - static ulong edata; + static ulong edata, edata_phys; asm ("movl $.edata, %0" : "=rm"(edata)); + edata_phys = edata - OSV_KERNEL_VM_SHIFT; + // copy to stack so we don't free it now auto omb = *osv_multiboot_info; auto mb = omb.mb; @@ -129,13 +132,13 @@ void arch_setup_free_memory() // page tables have been set up, so we can't reference the memory being // freed. for_each_e820_entry(e820_buffer, e820_size, [] (e820ent ent) { - // can't free anything below edata, it's core code. + // can't free anything below edata_phys, it's core code. // can't free anything below kernel at this moment - if (ent.addr + ent.size <= edata) { + if (ent.addr + ent.size <= edata_phys) { return; } - if (intersects(ent, edata)) { - ent = truncate_below(ent, edata); + if (intersects(ent, edata_phys)) { + ent = truncate_below(ent, edata_phys); } // ignore anything above 1GB, we haven't mapped it yet if (intersects(ent, initial_map)) { @@ -149,21 +152,27 @@ void arch_setup_free_memory() auto base = reinterpret_cast<void*>(get_mem_area_base(area)); mmu::linear_map(base, 0, initial_map, initial_map); } - // map the core, loaded 1:1 by the boot loader - mmu::phys elf_phys = reinterpret_cast<mmu::phys>(elf_header); - elf_start = reinterpret_cast<void*>(elf_header); - elf_size = edata - elf_phys; - mmu::linear_map(elf_start, elf_phys, elf_size, OSV_KERNEL_BASE); + // Map the core, loaded by the boot loader + // In order to properly setup mapping between virtual + // and physical we need to take into account where kernel + // is loaded in physical memory - elf_phys_start - and + // where it is linked to start in virtual memory - elf_start + static mmu::phys elf_phys_start = reinterpret_cast<mmu::phys>(elf_header); + // There is simple invariant between elf_phys_start and elf_start + // as expressed by the assignment below + elf_start = reinterpret_cast<void*>(elf_phys_start + OSV_KERNEL_VM_SHIFT); + elf_size = edata_phys - elf_phys_start; + mmu::linear_map(elf_start, elf_phys_start, elf_size, OSV_KERNEL_BASE); // get rid of the command line, before low memory is unmapped parse_cmdline(mb); // now that we have some free memory, we can start mapping the rest mmu::switch_to_runtime_page_tables(); for_each_e820_entry(e820_buffer, e820_size, [] (e820ent ent) { // - // Free the memory below elf_start which we could not before - if (ent.addr < (u64)elf_start) { - if (ent.addr + ent.size >= (u64)elf_start) { - ent = truncate_above(ent, (u64) elf_start); + // Free the memory below elf_phys_start which we could not before + if (ent.addr < (u64)elf_phys_start) { + if (ent.addr + ent.size >= (u64)elf_phys_start) { + ent = truncate_above(ent, (u64) elf_phys_start); } mmu::free_initial_memory_range(ent.addr, ent.size); return; diff --git a/arch/x64/boot.S b/arch/x64/boot.S index 1402e5d0..bb157bff 100644 --- a/arch/x64/boot.S +++ b/arch/x64/boot.S @@ -24,13 +24,28 @@ .align 4096 .global ident_pt_l4 ident_pt_l4: - .quad ident_pt_l3 + 0x67 + # The addresses of the paging tables have to be the physical ones, so we have to + # manually subtract OSV_KERNEL_VM_SHIFT in all relevant places + .quad ident_pt_l3 + 0x67 - OSV_KERNEL_VM_SHIFT .rept 511 .quad 0 .endr +#if OSV_KERNEL_VM_SHIFT != 0x40000000 && OSV_KERNEL_VM_SHIFT != 0 +#error This code only works correctly for OSV_KERNEL_VM_SHIFT = 0x40000000 or 0 +#endif ident_pt_l3: - .quad ident_pt_l2 + 0x67 - .rept 511 + # Each of the 512 entries in this table maps the very 1st 512 GiB of + # virtual address space 1 GiB at a time + # The very 1st entry maps 1st GiB 1:1 by pointing to ident_pt_l2 table + # that specifies addresses of every one of 512 2MiB slots of physical memory + .quad ident_pt_l2 + 0x67 - OSV_KERNEL_VM_SHIFT + # The 2nd entry maps 2nd GiB to the same 1st GiB of physical memory by pointing + # to the same ident_pt_l2 table as the 1st entry above + # This way we effectively provide correct mapping for the kernel linked + # to start at 1 GiB + 2 MiB (0x40200000) in virtual memory and point to + # 2 MiB address (0x200000) where it starts in physical memory + .quad ident_pt_l2 + 0x67 - OSV_KERNEL_VM_SHIFT + .rept 510 .quad 0 .endr ident_pt_l2: @@ -42,7 +57,8 @@ ident_pt_l2: gdt_desc: .short gdt_end - gdt - 1 - .long gdt + # subtract OSV_KERNEL_VM_SHIFT because when gdt_desc is referenced, the memory is mapped 1:1 + .long gdt - OSV_KERNEL_VM_SHIFT # Set up the 64-bit compatible version of GDT description structure # that points to the same GDT (Global segments Descriptors Table) and @@ -53,7 +69,8 @@ gdt_desc: .align 8 gdt64_desc: .short gdt_end - gdt - 1 - .quad gdt + # subtract OSV_KERNEL_VM_SHIFT because when gdt64_desc is referenced, the memory is mapped 1:1 + .quad gdt - OSV_KERNEL_VM_SHIFT .align 8 gdt = . - 8 @@ -77,10 +94,12 @@ init_stack_top = . .globl start32 .globl start32_from_64 start32: + # Because the memory is mapped 1:1 at this point, we have to manualy + # subtract OSV_KERNEL_VM_SHIFT from virtual addresses in all relevant places # boot16.S set %eax to ELF start address, we'll use it later mov %eax, %ebp mov $0x0, %edi - lgdt gdt_desc + lgdt gdt_desc-OSV_KERNEL_VM_SHIFT # Add an address the vmlinux_entry64 will jump to when # switching from 64-bit to 32-bit mode @@ -91,7 +110,7 @@ start32_from_64: mov %eax, %fs mov %eax, %gs mov %eax, %ss - ljmp $0x18, $1f + ljmp $0x18, $1f-OSV_KERNEL_VM_SHIFT 1: and $~7, %esp # Enable PAE (Physical Address Extension) - ability to address 64GB @@ -101,6 +120,9 @@ start32_from_64: # Set root of a page table in cr3 lea ident_pt_l4, %eax + # The address of the root paging table has to be physical + # so substract OSV_KERNEL_VM_SHIFT from ident_pt_l4 + sub $OSV_KERNEL_VM_SHIFT, %eax mov %eax, %cr3 # Set long mode @@ -128,7 +150,7 @@ start64: jz start64_continue call extract_linux_boot_params mov $0x1000, %rbx - mov $0x200000, %rbp + mov $OSV_KERNEL_BASE, %rbp start64_continue: lea .bss, %rdi @@ -168,6 +190,7 @@ smpboot: mov smpboot_cr4-smpboot, %eax mov %eax, %cr4 lea ident_pt_l4, %eax + sub $OSV_KERNEL_VM_SHIFT, %eax mov %eax, %cr3 mov smpboot_efer-smpboot, %eax mov smpboot_efer+4-smpboot, %edx @@ -181,7 +204,7 @@ smpboot: smpboot_gdt_desc: .short gdt_end - gdt - 1 - .long gdt + .long gdt - OSV_KERNEL_VM_SHIFT .global smpboot_cr0 smpboot_cr0: .long 0 diff --git a/arch/x64/entry-xen.S b/arch/x64/entry-xen.S index 11f72da4..81342284 100644 --- a/arch/x64/entry-xen.S +++ b/arch/x64/entry-xen.S @@ -23,7 +23,7 @@ elfnote_val(XEN_ELFNOTE_ENTRY, xen_start) elfnote_val(XEN_ELFNOTE_HYPERCALL_PAGE, hypercall_page) -elfnote_val(XEN_ELFNOTE_VIRT_BASE, 0) +elfnote_val(XEN_ELFNOTE_VIRT_BASE, OSV_KERNEL_VM_SHIFT) elfnote_str(XEN_ELFNOTE_XEN_VERSION, "xen-3.0") elfnote_str(XEN_ELFNOTE_GUEST_OS, "osv") elfnote_str(XEN_ELFNOTE_GUEST_VERSION, "?.?") @@ -50,4 +50,5 @@ xen_start: mov %rsp, xen_bootstrap_end mov %rsi, %rdi call xen_init + mov $0x0, %rdi jmp start64 diff --git a/arch/x64/loader.ld b/arch/x64/loader.ld index caae1f68..8b82b1bb 100644 --- a/arch/x64/loader.ld +++ b/arch/x64/loader.ld @@ -14,16 +14,17 @@ SECTIONS * * We can't export the ELF header base as a symbol, because ld * insists on moving stuff around if we do. - * + */ + . = OSV_KERNEL_VM_BASE + 0x800; + /* * Place address of start32 routine at predefined offset in memory */ - . = OSV_KERNEL_BASE + 0x800; - .start32_address : { + .start32_address : AT(ADDR(.start32_address) - OSV_KERNEL_VM_SHIFT) { *(.start32_address) } - . = OSV_KERNEL_BASE + 0x1000; - .dynamic : { *(.dynamic) } :dynamic :text - .text : { + . = OSV_KERNEL_VM_BASE + 0x1000; + .dynamic : AT(ADDR(.dynamic) - OSV_KERNEL_VM_SHIFT) { *(.dynamic) } :dynamic :text + .text : AT(ADDR(.text) - OSV_KERNEL_VM_SHIFT) { text_start = .; *(.text.hot .text.hot.*) *(.text.unlikely .text.*_unlikely) @@ -31,60 +32,61 @@ SECTIONS *(.text.startup .text.startup.*) *(.text .text.*) text_end = .; + PROVIDE(low_vmlinux_entry64 = vmlinux_entry64 - OSV_KERNEL_VM_SHIFT); } :text . = ALIGN(8); - .fixup : { + .fixup : AT(ADDR(.fixup) - OSV_KERNEL_VM_SHIFT) { fault_fixup_start = .; *(.fixup) fault_fixup_end = .; } :text . = ALIGN(8); - .memcpy_decode : { + .memcpy_decode : AT(ADDR(.memcpy_decode) - OSV_KERNEL_VM_SHIFT) { memcpy_decode_start = .; *(.memcpy_decode) memcpy_decode_end = .; } :text - .eh_frame : { *(.eh_frame) } : text - .rodata : { *(.rodata*) } :text - .eh_frame : { *(.eh_frame) } :text - .eh_frame_hdr : { *(.eh_frame_hdr) } :text :eh_frame - .note : { *(.note*) } :text :note - .gcc_except_table : { *(.gcc_except_table) *(.gcc_except_table.*) } : text - .tracepoint_patch_sites ALIGN(8) : { + .eh_frame : AT(ADDR(.eh_frame) - OSV_KERNEL_VM_SHIFT) { *(.eh_frame) } : text + .rodata : AT(ADDR(.rodata) - OSV_KERNEL_VM_SHIFT) { *(.rodata*) } :text + .eh_frame : AT(ADDR(.eh_frame) - OSV_KERNEL_VM_SHIFT) { *(.eh_frame) } :text + .eh_frame_hdr : AT(ADDR(.eh_frame_hdr) - OSV_KERNEL_VM_SHIFT) { *(.eh_frame_hdr) } :text :eh_frame + .note : AT(ADDR(.note) - OSV_KERNEL_VM_SHIFT) { *(.note*) } :text :note + .gcc_except_table : AT(ADDR(.gcc_except_table) - OSV_KERNEL_VM_SHIFT) { *(.gcc_except_table) *(.gcc_except_table.*) } : text + .tracepoint_patch_sites ALIGN(8) : AT(ADDR(.tracepoint_patch_sites) - OSV_KERNEL_VM_SHIFT) { __tracepoint_patch_sites_start = .; *(.tracepoint_patch_sites) __tracepoint_patch_sites_end = .; } : text - .data.rel.ro : { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro .data.rel.ro.* .gnu.linkonce.d.rel.ro.*) } : text - .data : { *(.data) } :text + .data.rel.ro : AT(ADDR(.data.rel.ro) - OSV_KERNEL_VM_SHIFT) { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro .data.rel.ro.* .gnu.linkonce.d.rel.ro.*) } : text + .data : AT(ADDR(.data) - OSV_KERNEL_VM_SHIFT) { *(.data) } :text _init_array_start = .; - .init_array : { + .init_array : AT(ADDR(.init_array) - OSV_KERNEL_VM_SHIFT) { *(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)) *(.init_array .ctors) } : text _init_array_end = .; . = ALIGN(4096); - .percpu : { + .percpu : AT(ADDR(.percpu) - OSV_KERNEL_VM_SHIFT) { _percpu_start = .; *(.percpu) . = ALIGN(4096); _percpu_end = .; } - .percpu_workers : { + .percpu_workers : AT(ADDR(.percpu_workers) - OSV_KERNEL_VM_SHIFT) { _percpu_workers_start = .; *(.percpu_workers) _percpu_workers_end = .; } . = ALIGN(64); - .tdata : { *(.tdata .tdata.* .gnu.linkonce.td.*) } :tls :text - .tbss : { + .tdata : AT(ADDR(.tdata) - OSV_KERNEL_VM_SHIFT) { *(.tdata .tdata.* .gnu.linkonce.td.*) } :tls :text + .tbss : AT(ADDR(.tbss) - OSV_KERNEL_VM_SHIFT) { *(.tbss .tbss.* .gnu.linkonce.tb.*) . = ALIGN(64); } :tls :text .tls_template_size = SIZEOF(.tdata) + SIZEOF(.tbss); - .bss : { *(.bss .bss.*) } :text + .bss : AT(ADDR(.bss) - OSV_KERNEL_VM_SHIFT) { *(.bss .bss.*) } :text . = ALIGN(64); tcb0 = .; . = . + .tls_template_size + 256; @@ -114,4 +116,4 @@ PHDRS { eh_frame PT_GNU_EH_FRAME; note PT_NOTE; } -ENTRY(vmlinux_entry64); +ENTRY(low_vmlinux_entry64); diff --git a/arch/x64/vmlinux-boot64.S b/arch/x64/vmlinux-boot64.S index 230afd3c..12047513 100644 --- a/arch/x64/vmlinux-boot64.S +++ b/arch/x64/vmlinux-boot64.S @@ -13,7 +13,9 @@ vmlinux_entry64: mov %rsi, %rdi # Load the 64-bit version of the GDT - lgdt gdt64_desc + # Because the memory is mapped 1:1 at this point, we have to manualy + # subtract OSV_KERNEL_VM_SHIFT from the gdt address + lgdt gdt64_desc-OSV_KERNEL_VM_SHIFT # Setup the stack to switch back to 32-bit mode in order # to converge with the code that sets up transiton to 64-bit mode later. @@ -32,6 +34,6 @@ vmlinux_entry64: # to start32_from_64 which is where the boot process converges. subq $8, %rsp movl $0x18, 4(%rsp) - movl $start32_from_64, %eax + movl $start32_from_64-OSV_KERNEL_VM_SHIFT, %eax # Because memory is mapped 1:1 subtract OSV_KERNEL_VM_SHIFT movl %eax, (%rsp) lret diff --git a/arch/x64/xen.cc b/arch/x64/xen.cc index 462c266c..c02bf62c 100644 --- a/arch/x64/xen.cc +++ b/arch/x64/xen.cc @@ -172,7 +172,7 @@ void xen_init(processor::features_type &features, unsigned base) // Base + 1 would have given us the version number, it is mostly // uninteresting for us now auto x = processor::cpuid(base + 2); - processor::wrmsr(x.b, cast_pointer(&hypercall_page)); + processor::wrmsr(x.b, cast_pointer(&hypercall_page) - OSV_KERNEL_VM_SHIFT); struct xen_feature_info info; // To fill up the array used by C code @@ -192,7 +192,7 @@ void xen_init(processor::features_type &features, unsigned base) map.domid = DOMID_SELF; map.idx = 0; map.space = 0; - map.gpfn = cast_pointer(&xen_shared_info) >> 12; + map.gpfn = (cast_pointer(&xen_shared_info) - OSV_KERNEL_VM_SHIFT) >> 12; // 7 => add to physmap if (memory_hypercall(XENMEM_add_to_physmap, &map)) diff --git a/core/elf.cc b/core/elf.cc index fc2ee0c3..477a0177 100644 --- a/core/elf.cc +++ b/core/elf.cc @@ -1099,7 +1099,7 @@ void create_main_program() program::program(void* addr) : _next_alloc(addr) { - _core = std::make_shared<memory_image>(*this, (void*)ELF_IMAGE_START); + _core = std::make_shared<memory_image>(*this, (void*)(ELF_IMAGE_START + OSV_KERNEL_VM_SHIFT)); assert(_core->module_index() == core_module_index); _core->load_segments(); set_search_path({"/", "/usr/lib"}); diff --git a/core/mmu.cc b/core/mmu.cc index f9294125..575834ca 100644 --- a/core/mmu.cc +++ b/core/mmu.cc @@ -91,12 +91,12 @@ phys pte_level_mask(unsigned level) return ~((phys(1) << shift) - 1); } +static void *elf_phys_start = (void*)OSV_KERNEL_BASE; void* phys_to_virt(phys pa) { - // The ELF is mapped 1:1 void* phys_addr = reinterpret_cast<void*>(pa); - if ((phys_addr >= elf_start) && (phys_addr < elf_start + elf_size)) { - return phys_addr; + if ((phys_addr >= elf_phys_start) && (phys_addr < elf_phys_start + elf_size)) { + return (void*)(phys_addr + OSV_KERNEL_VM_SHIFT); } return phys_mem + pa; @@ -106,9 +106,8 @@ phys virt_to_phys_pt(void* virt); phys virt_to_phys(void *virt) { - // The ELF is mapped 1:1 if ((virt >= elf_start) && (virt < elf_start + elf_size)) { - return reinterpret_cast<phys>(virt); + return reinterpret_cast<phys>((void*)(virt - OSV_KERNEL_VM_SHIFT)); } #if CONF_debug_memory diff --git a/loader.cc b/loader.cc index 7d88e609..7ac99ef5 100644 --- a/loader.cc +++ b/loader.cc @@ -102,7 +102,8 @@ void premain() arch_init_premain(); - auto inittab = elf::get_init(elf_header); + auto inittab = elf::get_init(reinterpret_cast<elf::Elf64_Ehdr*>( + (void*)elf_header + OSV_KERNEL_VM_SHIFT)); if (inittab.tls.start == nullptr) { debug_early("premain: failed to get TLS data from ELF\n"); -- 2.20.1 -- You received this message because you are subscribed to the Google Groups "OSv Development" group. To unsubscribe from this group and stop receiving emails from it, send an email to osv-dev+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/osv-dev/20190620040707.23249-1-jwkozaczuk%40gmail.com. For more options, visit https://groups.google.com/d/optout.