From f139fff6ae7063c1965fa3085bb2585d7d838d72 Mon Sep 17 00:00:00 2001 From: Kimplul Date: Wed, 30 Oct 2024 13:34:18 +0200 Subject: rpc actually marks and unmarks stack regions + Processes won't be able to read previous stack frames etc --- arch/riscv64/kernel/entry.S | 4 ++ arch/riscv64/kernel/except.c | 1 + arch/riscv64/kernel/proc.c | 3 +- arch/riscv64/kernel/vmem.c | 90 +++++++++++++++++++++++++++++++-------- benchmarks/ipc-req/init.c | 3 +- docs/visionfive2.md | 3 ++ include/arch/tcb.h | 69 ++++++++++++++++++++++++++---- src/uapi/ipc.c | 38 ++++++----------- src/vmem.c | 10 ++++- tests/Makefile | 4 ++ tests/create-exhaustion/source.mk | 3 +- tests/create/source.mk | 3 +- tests/detach/source.mk | 3 +- tests/fork-exhaustion/source.mk | 3 +- tests/fork/source.mk | 3 +- tests/hello-world/source.mk | 3 +- tests/ipc-notify/source.mk | 3 +- tests/ipc-req/init.c | 28 +++++++----- tests/ipc-req/source.mk | 3 +- tests/malloc/source.mk | 3 +- tests/noop/source.mk | 3 +- tests/scripts/gen-simple | 4 +- tests/scripts/gen-tests | 29 +++++++++++++ 23 files changed, 229 insertions(+), 87 deletions(-) create mode 100755 tests/scripts/gen-tests diff --git a/arch/riscv64/kernel/entry.S b/arch/riscv64/kernel/entry.S index f894e0d..48851d4 100644 --- a/arch/riscv64/kernel/entry.S +++ b/arch/riscv64/kernel/entry.S @@ -92,6 +92,9 @@ continue_trap: save_callee save_caller + csrr t0, CSR_SEPC + sr t0, offsetof_exec(tp) + mv a0, s10 /* get actual kernel stack into sp */ mv sp, tp @@ -117,6 +120,7 @@ handle_exception: csrr a0, CSR_SEPC csrr a1, CSR_STVAL csrr a2, CSR_SCAUSE + sr a0, offsetof_exec(tp) mv sp, tp call riscv_handle_exception diff --git a/arch/riscv64/kernel/except.c b/arch/riscv64/kernel/except.c index e46b584..2525716 100644 --- a/arch/riscv64/kernel/except.c +++ b/arch/riscv64/kernel/except.c @@ -1,5 +1,6 @@ #include #include +#include #include #include diff --git a/arch/riscv64/kernel/proc.c b/arch/riscv64/kernel/proc.c index 3ae2e15..ba5c8e7 100644 --- a/arch/riscv64/kernel/proc.c +++ b/arch/riscv64/kernel/proc.c @@ -48,7 +48,8 @@ void run_init(struct tcb *t, vm_t fdt, vm_t initrd) clone_uvmem(r->proc.vmem, t->rpc.vmem); flush_tlb_all(); - vm_t stack_top = t->rpc_stack - BASE_PAGE_SIZE; + /* lore, should probably be codified somewhere */ + vm_t stack_top = get_stack(t); info("jumping to %lx with sp = %lx\n", (long)t->callback, stack_top); bkl_unlock(); diff --git a/arch/riscv64/kernel/vmem.c b/arch/riscv64/kernel/vmem.c index d66ddec..a1ca54e 100644 --- a/arch/riscv64/kernel/vmem.c +++ b/arch/riscv64/kernel/vmem.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "pages.h" #include "arch.h" @@ -598,7 +599,8 @@ stat_t setup_rpc_stack(struct tcb *t) { /* by default rpc stack is marked inaccessible to generate segfaults on * access so as to ease stack usage tracking */ - vmflags_t flags = VM_V | VM_R | VM_W | VM_U; + vmflags_t flags = VM_V | VM_R | + VM_W /*| VM_U note how this is missing */; for (size_t i = 0; i < rpc_pages; ++i) { pm_t page = alloc_page(BASE_PAGE); @@ -620,8 +622,12 @@ stat_t setup_rpc_stack(struct tcb *t) t->arch.rpc_leaf = (struct vmem *)__find_vmem(t->rpc.vmem, RPC_STACK_BASE, NULL); - /* we count downward in base pages */ - t->arch.rpc_idx = rpc_pages; + /* we count downward in base pages, the top page is *always* reserved */ + t->arch.rpc_idx = rpc_pages - 1; + + /* mark our stack region inaccessible to userspace */ + vm_t stack_top = t->rpc_stack - BASE_PAGE_SIZE; + set_stack(t, stack_top); return OK; } @@ -640,7 +646,7 @@ void destroy_rpc_stack(struct tcb *t) void reset_rpc_stack(struct tcb *t) { t->rpc_stack = RPC_STACK_BASE + (BASE_PAGE_SIZE * rpc_pages); - t->arch.rpc_idx = rpc_pages; + t->arch.rpc_idx = rpc_pages - 1; } bool rpc_stack_empty(pm_t addr) @@ -658,37 +664,85 @@ vm_t rpc_position(struct tcb *t) return RPC_STACK_BASE + (BASE_PAGE_SIZE * t->arch.rpc_idx); } -void mark_rpc_invalid(struct tcb *t, vm_t top) +/* these following stack handling routines are pretty mind-bending + * unfortunately, might there be a better way? */ + +bool in_rpc_stack(struct tcb *t, vm_t addr) +{ + return addr >= RPC_STACK_BASE + && addr < RPC_STACK_BASE + BASE_PAGE_SIZE * t->arch.rpc_idx; +} + +void close_rpc(struct tcb *t) { struct vmem *b = t->arch.rpc_leaf; int top_idx = t->arch.rpc_idx; - int bottom_idx = (top - RPC_STACK_BASE) / BASE_PAGE_SIZE; - assert(bottom_idx < top_idx); - while (top_idx != bottom_idx) { + /* absolute max iter count */ + for (; top_idx < (int)rpc_pages; --top_idx) { + /* mark inaccessible until we reach the first inaccessible + * region */ pm_t *pte = (pm_t *)&b->leaf[top_idx]; + if (!(pte_flags(*pte) & VM_U)) + break; + /* make page not accessible from userspace */ clear_bits(*pte, vp_flags(VM_U)); - top_idx--; + } + + /* make page following closed region accessible so we can avoid an + * unnecessary page fault */ + pm_t *pte = (pm_t *)&b->leaf[top_idx]; + set_bits(*pte, vp_flags(VM_U)); + t->arch.rpc_idx = top_idx; +} + +void shrink_rpc(struct tcb *t) +{ + struct vmem *b = t->arch.rpc_leaf; + int top_idx = t->arch.rpc_idx; + + for (; top_idx > 0; ++top_idx) { + pm_t *pte = (pm_t *)&b->leaf[top_idx]; + if (!(pte_flags(*pte) & VM_U)) + break; + + clear_bits(*pte, vp_flags(VM_U)); } t->arch.rpc_idx = top_idx; } -void mark_rpc_valid(struct tcb *t, vm_t bottom) +void open_rpc(struct tcb *t, vm_t top) { + assert(is_aligned(top, BASE_PAGE_SIZE)); struct vmem *b = t->arch.rpc_leaf; - int bottom_idx = t->arch.rpc_idx; - int top_idx = (bottom - RPC_STACK_BASE) / BASE_PAGE_SIZE; - assert(bottom_idx <= top_idx); + int old_idx = t->arch.rpc_idx; + int new_idx = (top - RPC_STACK_BASE) / BASE_PAGE_SIZE; + assert(new_idx >= old_idx); + + /* always stop one before the page index indicated, might be kind of + * easy to mess up */ + for (int i = old_idx; i < new_idx - 1; ++i) { + pm_t *pte = (pm_t *)&b->leaf[i]; + set_bits(*pte, vp_flags(VM_U)); + } + + t->arch.rpc_idx = new_idx - 1; +} + +void grow_rpc(struct tcb *t, vm_t top) +{ + assert(is_aligned(top, BASE_PAGE_SIZE)); + struct vmem *b = t->arch.rpc_leaf; + int top_idx = t->arch.rpc_idx; + int bottom_idx = (top - RPC_STACK_BASE) / BASE_PAGE_SIZE; + assert (top_idx >= bottom_idx); - while (top_idx != bottom_idx) { - pm_t *pte = (pm_t *)&b->leaf[bottom_idx]; + for (int i = bottom_idx; i < top_idx; ++i) { + pm_t *pte = (pm_t *)&b->leaf[i]; /* make page accessible from userspace */ set_bits(*pte, vp_flags(VM_U)); - /* clear used bits */ - clear_bits(*pte, vp_flags(VM_A | VM_D)); - bottom_idx++; } t->arch.rpc_idx = bottom_idx; diff --git a/benchmarks/ipc-req/init.c b/benchmarks/ipc-req/init.c index 02064a6..910e018 100644 --- a/benchmarks/ipc-req/init.c +++ b/benchmarks/ipc-req/init.c @@ -12,8 +12,9 @@ START(pid, tid, d0, d1, d2, d3) uint64_t timebase = sys_timebase(); uint64_t start = sys_ticks(); - for (size_t i = 0; i < 1000; ++i) + for (size_t i = 0; i < 1000; ++i) { sys_ipc_req0(1); + } uint64_t end = sys_ticks(); report(start, end, timebase); diff --git a/docs/visionfive2.md b/docs/visionfive2.md index 250a844..24677f8 100644 --- a/docs/visionfive2.md +++ b/docs/visionfive2.md @@ -13,6 +13,9 @@ point. The following is how to boot with an SD card. The board seems to support serial and ethernet booting, but I haven't looked into how they work. +The SD card should have an empty GPT table, you can create one with +`fdisk /dev/sdX/`. + 1. Build `kmi` with `GENERIC_UBOOT=1`. At the moment, SMP is broken on the VisionFive2, so you'll need to comment out `smp_bringup(d, fdt);` in `main.c`. diff --git a/include/arch/tcb.h b/include/arch/tcb.h index 18386d3..709a3db 100644 --- a/include/arch/tcb.h +++ b/include/arch/tcb.h @@ -77,20 +77,71 @@ bool rpc_stack_empty(pm_t addr); vm_t rpc_position(struct tcb *t); /** - * Mark RPC stack up to \p top accessible from userspace. + * Checks whether an address is in the rpc stack. + * Currently also serves double-duty to check that the address is currently + * inaccessible, but could be made accessible with \ref grow_rpc. Used by + * \ref handle_pagefault. * - * @param t Thread whose RPC stack is being modified. - * @param top Address up to where stack should be accessible from userspace. + * @param t tcb to check. + * @param addr Address to check. + * @return \ref true if the above holds, \ref false otherwise. */ -void mark_rpc_valid(struct tcb *t, vm_t top); +bool in_rpc_stack(struct tcb *t, vm_t addr); /** - * Mark RPC stack down to \p bottom inaccessible from userspace. + * Shrinks rpc stack down to where the previous reserved area is, so stack looks + * something like this: * - * @param t Thread whose RPC stack is being modified. - * @param bottom Address down to where stack should be inaccessible from - * userspace. + * ``` + * ctx | user | < + * v + * ctx | < + * ``` + * + * Implementation note: t->arch.rpc_idx now points to ctx. + * + * @param t tcb to shrink. + */ +void shrink_rpc(struct tcb *t); + +/** + * Opens back up a previous stack frame, so + * ``` + * ctx | user | ctx | < + * v + * ctx | user | < + * ``` + * + * @param t tcb to open up again. + * @param top Address to where previous ctx is. + */ +void open_rpc(struct tcb *t, vm_t top); + +/** + * Adds more accessible space on the stack, so + * ``` + * ctx | user | < + * v + * ctx | user | user | < + * ``` + * + * @param t tcb whose stack to grow. + * @param top Address to grow to. + */ +void grow_rpc(struct tcb *t, vm_t top); + +/** + * Marks all user accessible areas inaccessible, so + * ``` + * ctx | user | < + * ctx | ---- | < + * ``` + * + * Used by \ref do_ipc to make sure different processes can't mess with + * eachother's stacks. + * + * @param t tcb whose stack should be closed. */ -void mark_rpc_invalid(struct tcb *t, vm_t bottom); +void close_rpc(struct tcb *t); #endif /* KMI_ARCH_TCB_H */ diff --git a/src/uapi/ipc.c b/src/uapi/ipc.c index 6f6862e..3e9c04a 100644 --- a/src/uapi/ipc.c +++ b/src/uapi/ipc.c @@ -53,7 +53,8 @@ static inline void finalize_rpc(struct tcb *t, struct tcb *r, vm_t s) t->pid = r->rid; /* make sure updates are visible when swapping to the new virtual memory */ - mark_rpc_invalid(t, s); + close_rpc(t); + set_stack(t, s); } /** @@ -84,25 +85,9 @@ static inline vm_t enter_rpc(struct tcb *t, struct sys_ret a, ctx->pid = t->pid; ctx->eid = t->eid; ctx->notify = flags & IPC_NOTIFY; - ctx->rpc_stack = rpc_stack; - - /** @todo if we run out of rpc_stack space we should just stop, likely - * return a status? except it shouldn't happen after we've run - * enough_rpc_stack(). */ - vm_t new_stack = rpc_stack - BASE_PAGE_SIZE; - - /** @todo what if each stack is only some number of pages, and if a proc - * goes over the limit is is seen as programming error? Possibly user - * configurable number as well, might actually use the config subsystem - * :D - * In such a case it would probably be smarter to mark all pages - * inaccessible at first, and then mark the first page accessible. If - * the process needs more stack space it'll cause a paging exception, - * we'll handle it separately and if the process isn't going over the - * limit just give it more. - * */ - t->rpc_stack = new_stack; - return new_stack; + ctx->rpc_stack = t->rpc_stack; + t->rpc_stack = rpc_stack; + return rpc_stack - BASE_PAGE_SIZE; } /** @@ -215,9 +200,9 @@ void notify(struct tcb *t, enum notify_flag flags) */ static void leave_rpc(struct tcb *t, struct sys_ret a) { - vm_t rpc_stack = t->rpc_stack + BASE_PAGE_SIZE; + vm_t rpc_stack = t->rpc_stack; struct call_ctx *ctx = (struct call_ctx *)(rpc_stack) - 1; - t->regs = (vm_t)ctx; + t->regs = (vm_t)ctx->rpc_stack - sizeof(struct call_ctx); /* again, get rid of args as fast as possible */ if (!ctx->notify) @@ -232,9 +217,9 @@ static void leave_rpc(struct tcb *t, struct sys_ret a) break; } - rpc_stack = ctx->rpc_stack + BASE_PAGE_SIZE; + rpc_stack = ctx->rpc_stack; ctx = (struct call_ctx *)(rpc_stack) - 1; - t->regs = (vm_t)ctx; + t->regs = (vm_t)ctx->rpc_stack - sizeof(struct call_ctx); r = get_tcb(ctx->pid); /* equivalent to return_args1 but without returning so we can @@ -249,7 +234,10 @@ static void leave_rpc(struct tcb *t, struct sys_ret a) set_return(t, ctx->exec); /* if we're returning from a failed rpc, this should essentially be a * no-op */ - mark_rpc_valid(t, ctx->rpc_stack); + shrink_rpc(t); + open_rpc(t, ctx->rpc_stack); + flush_tlb_all(); + t->rpc_stack = ctx->rpc_stack; t->pid = ctx->pid; t->eid = ctx->eid; diff --git a/src/vmem.c b/src/vmem.c index 0404202..8c23d4f 100644 --- a/src/vmem.c +++ b/src/vmem.c @@ -427,9 +427,17 @@ vmflags_t sanitize_uvflags(vmflags_t flags) void handle_pagefault(vm_t addr) { + info("page fault at %lx\n", addr); struct tcb *t = cur_tcb(); - struct tcb *p = get_cproc(t); + if (in_rpc_stack(t, addr)) { + vm_t aligned = align_down(addr, BASE_PAGE_SIZE); + grow_rpc(t, aligned); + flush_tlb_all(); + return; + } + + struct tcb *p = get_cproc(t); size_t ref = __page(addr); struct mem_region *m = find_closest_used_region(&p->uvmem.region, addr); diff --git a/tests/Makefile b/tests/Makefile index 0a662f7..105b9bb 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -6,8 +6,12 @@ check: SOURCES != echo */source.mk DO != echo -n > tests.mk + +TESTS := include $(SOURCES) +DO != ./scripts/gen-tests $(TESTS) + RM ?= rm .PHONY: clean diff --git a/tests/create-exhaustion/source.mk b/tests/create-exhaustion/source.mk index d096746..5b90073 100644 --- a/tests/create-exhaustion/source.mk +++ b/tests/create-exhaustion/source.mk @@ -1,2 +1 @@ -DO != ./scripts/gen-prog -n create-exhaustion -p init init.c -DO != ./scripts/gen-simple -n create-exhaustion -p init +TESTS += create-exhaustion diff --git a/tests/create/source.mk b/tests/create/source.mk index b9f8efe..7aa5eb7 100644 --- a/tests/create/source.mk +++ b/tests/create/source.mk @@ -1,2 +1 @@ -DO != ./scripts/gen-prog -n create -p init init.c -DO != ./scripts/gen-simple -n create -p init +TESTS += create diff --git a/tests/detach/source.mk b/tests/detach/source.mk index 843562f..c248759 100644 --- a/tests/detach/source.mk +++ b/tests/detach/source.mk @@ -1,2 +1 @@ -DO != ./scripts/gen-prog -n detach -p init init.c -DO != ./scripts/gen-simple -n detach -p init +TESTS += detach diff --git a/tests/fork-exhaustion/source.mk b/tests/fork-exhaustion/source.mk index 458973d..7dc7d36 100644 --- a/tests/fork-exhaustion/source.mk +++ b/tests/fork-exhaustion/source.mk @@ -1,2 +1 @@ -DO != ./scripts/gen-prog -n fork-exhaustion -p init init.c -DO != ./scripts/gen-simple -n fork-exhaustion -p init +TESTS += fork-exhaustion diff --git a/tests/fork/source.mk b/tests/fork/source.mk index 47572e6..e78d467 100644 --- a/tests/fork/source.mk +++ b/tests/fork/source.mk @@ -1,2 +1 @@ -DO != ./scripts/gen-prog -n fork -p init init.c -DO != ./scripts/gen-simple -n fork -p init +TEST += fork diff --git a/tests/hello-world/source.mk b/tests/hello-world/source.mk index 5fd60ff..a24092c 100644 --- a/tests/hello-world/source.mk +++ b/tests/hello-world/source.mk @@ -1,2 +1 @@ -DO != ./scripts/gen-prog -n hello-world -p init init.c -DO != ./scripts/gen-simple -n hello-world -p init +TESTS += hello-world diff --git a/tests/ipc-notify/source.mk b/tests/ipc-notify/source.mk index e154996..635d335 100644 --- a/tests/ipc-notify/source.mk +++ b/tests/ipc-notify/source.mk @@ -1,2 +1 @@ -DO != ./scripts/gen-prog -n ipc-notify -p init init.c -DO != ./scripts/gen-simple -n ipc-notify -p init +TESTS += ipc-notify diff --git a/tests/ipc-req/init.c b/tests/ipc-req/init.c index 3439335..90e04a7 100644 --- a/tests/ipc-req/init.c +++ b/tests/ipc-req/init.c @@ -4,19 +4,25 @@ START(pid, tid, d0, d1, d2, d3) { check(pid == 0 || pid == 1, "illegal pid for init"); if (pid == 0) { - /* send request to ourselves */ - printf("sending ipc req to ourselves\n"); - struct sys_ret r = sys_ipc_req4(1, 1, 2, 3, 4); - printf("returned ipc req to ourselves\n"); - check(r.s == OK, "not OK return\n"); - check(r.id == 1, "not OK response ID\n"); - check(r.a0 == 1, "not OK d0 response\n"); - check(r.a1 == 2, "not OK d1 response\n"); - check(r.a2 == 3, "not OK d2 response\n"); - check(r.a3 == 4, "not OK d3 response\n"); + printf("doing nonsense response\n"); + enum sys_status s = sys_ipc_resp0(); + check(s != OK, "OK return?\n"); + + for (size_t i = 0; i < 3; ++i) { + printf("sending ipc req %zd to ourselves\n", i); + struct sys_ret r = sys_ipc_req4(1, 1, 2, 3, 4); + + printf("returned ipc req to ourselves\n"); + check(r.s == OK, "not OK return\n"); + check(r.id == 1, "not OK response ID\n"); + check(r.a0 == 1, "not OK d0 response\n"); + check(r.a1 == 2, "not OK d1 response\n"); + check(r.a2 == 3, "not OK d2 response\n"); + check(r.a3 == 4, "not OK d3 response\n"); + } /* try to request to non-existing proc */ - r = sys_ipc_req0(200); + struct sys_ret r = sys_ipc_req0(200); check(r.s != OK, "got OK return for illegal pid\n"); } else if (pid == 1) { diff --git a/tests/ipc-req/source.mk b/tests/ipc-req/source.mk index 0e65407..4b77d99 100644 --- a/tests/ipc-req/source.mk +++ b/tests/ipc-req/source.mk @@ -1,2 +1 @@ -DO != ./scripts/gen-prog -n ipc-req -p init init.c -DO != ./scripts/gen-simple -n ipc-req -p init +TESTS += ipc-req diff --git a/tests/malloc/source.mk b/tests/malloc/source.mk index 9498682..01b4d6e 100644 --- a/tests/malloc/source.mk +++ b/tests/malloc/source.mk @@ -1,2 +1 @@ -DO != ./scripts/gen-prog -n malloc -p init init.c -DO != ./scripts/gen-simple -n malloc -p init +TESTS += malloc diff --git a/tests/noop/source.mk b/tests/noop/source.mk index cbb87b7..0663967 100644 --- a/tests/noop/source.mk +++ b/tests/noop/source.mk @@ -1,2 +1 @@ -DO != ./scripts/gen-prog -n noop -p init init.c -DO != ./scripts/gen-simple -n noop -p init +TESTS += noop diff --git a/tests/scripts/gen-simple b/tests/scripts/gen-simple index 418cc02..a942fb3 100755 --- a/tests/scripts/gen-simple +++ b/tests/scripts/gen-simple @@ -28,8 +28,10 @@ echo " timeout --foreground 30s \$(QEMU) \ echo "TESTS += $NAME" >> tests.mk echo ".PHONY: $NAME" >> tests.mk -echo "$NAME: reports/$NAME/log" >> tests.mk +echo "reports/$NAME/OK: reports/$NAME/log" >> tests.mk echo " grep 'BUG' reports/$NAME/log \\" >> tests.mk echo " && echo 'BUG' > reports/$NAME/log \\" >> tests.mk echo " || tail -n1 reports/$NAME/log | tr -d '\\\\r' \\" >> tests.mk echo " > reports/$NAME/OK" >> tests.mk + +echo "$NAME: reports/$NAME/OK" >> tests.mk diff --git a/tests/scripts/gen-tests b/tests/scripts/gen-tests new file mode 100755 index 0000000..7095e30 --- /dev/null +++ b/tests/scripts/gen-tests @@ -0,0 +1,29 @@ +#!/bin/sh + +mkdir -p $(for d in "${@}"; do echo $d; done | sed "s|^|build/|") +mkdir -p $(for d in "${@}"; do echo $d; done | sed "s|^|reports/|") + +for NAME in "${@}"; do + echo "build/$NAME/init.d:" >> tests.mk + echo "-include build/$NAME/init.d" >> tests.mk + + echo "build/$NAME/init: $NAME/init.c \$(COMMON) \$(KMI)" >> tests.mk + echo " \$(COMPILE_TEST) $NAME/init.c \$(COMMON) -o build/$NAME/init" >> tests.mk + + echo "build/$NAME/initrd: build/$NAME/init" >> tests.mk + echo " echo build/$NAME/init | \$(GEN_INITRD) build/$NAME/initrd" >> tests.mk + + echo "reports/$NAME/log: build/$NAME/initrd" >> tests.mk + echo " timeout --foreground 30s \$(QEMU) \ + build/$NAME/initrd > reports/$NAME/log" >> tests.mk + + echo "reports/$NAME/OK: reports/$NAME/log" >> tests.mk + echo " grep 'BUG' reports/$NAME/log \\" >> tests.mk + echo " && echo 'BUG' > reports/$NAME/log \\" >> tests.mk + echo " || tail -n1 reports/$NAME/log | tr -d '\\\\r' \\" >> tests.mk + echo " > reports/$NAME/OK" >> tests.mk + + echo "TESTS += $NAME" >> tests.mk + echo ".PHONY: $NAME" >> tests.mk + echo "$NAME: reports/$NAME/OK" >> tests.mk +done -- cgit v1.3