#include <assert.h>
#include <uv.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
uv_loop_t *loop;
uv_tty_t tty;
uv_timer_t timer;
void myread(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf);
void alloc(uv_handle_t *handle, size_t suggested, uv_buf_t *buf) {
    buf->len = 1;
    buf->base = malloc(1);
}
void mytimer(uv_timer_t *timer, int status) {
    printf("going back for a read\n");
    assert(uv_read_start((uv_stream_t*) &tty, alloc, myread) == 0);
}
void myread(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
    printf("read %d bytes\n", (int) nread);
    assert(uv_read_stop(stream) == 0);
    if (nread > 0)      
        uv_timer_start(&timer, mytimer, 1, 0);
}
int main() {
    loop = uv_default_loop();
    uv_tty_init(loop, &tty, 0, 1);
    uv_timer_init(loop, &timer);
    uv_read_start((uv_stream_t*) &tty, alloc, myread);
    return uv_run(loop, UV_RUN_DEFAULT);
} 
When I compile that program, and then run it via echo a | ./foo, the program will never exit (it never sees EOF for stdin). If the call to uv_read_stop is omitted, however, then the program will successfully exit.
I also found out that running ./foo and then hitting ctrl+D does indeed see the EOF (and the program exits). This program was initially tested on linux (where it didn't work), but it turns out that on osx this does work (both echo a | ./foo and ./foo + ctrl-d).
I also found out that the reading one byte at a time is important, If I read a larger buffer then this works as expected (at least in the small cases).