Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement an rexec_client; move previous rexec code to rexec_server #36

Closed
wants to merge 8 commits into from

Conversation

yomimono
Copy link
Contributor

Here's another attempt at #35, which tries to implement a more sensible API.

@yomimono
Copy link
Contributor Author

/cc @linse

Copy link
Collaborator

@talex5 talex5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this! It looks good to me.

There's one bit I'm not sure about though: I see that the client and server both end up running a vchan service on the same port and waiting for dom0 to connect. Does that mean that a unikernel can either be a client or be a server, but not both? If outgoing client and incoming service requests go over the same vchan (I hadn't thought of that before), maybe there should be a single vchan server with a single listen loop, and RExec_client.connect ... should just be RExec.request_service t ..., returning an RExec_client.Flow.t? Apart from the that, the new API looks perfect to me :-)

Copy link
Contributor

@cfcs cfcs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be super nice to get this merged, thanks for writing this!
I left some comments, but none that I feel should block the merging of this.

@yomimono do you have an opinion about @talex5's question?


val write : t -> Cstruct.t -> [ `Ok of unit | `Eof ] Lwt.t

val writef : t -> ('a, unit, string, unit S.or_eof Lwt.t) format4 -> 'a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like documenting the trailing \n being added here would make sense, if we want to keep it.

It seems confusing to me that write would not add a newline, while writef does.
This behavior is in line with the [current implementation of RExec.writef]https://github.com/mirage/mirage-qubes/blob/master/lib/rExec.ml#L66-L83) which is documented in the mli interface (which I also find confusing). :)


let rec send t ~ty data =
let data, data' = Cstruct.split data (min max_data_chunk (Cstruct.len data)) in
let hdr = Cstruct.create sizeof_msg_header in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we wrapped the body in a recursive function we could avoid an allocation of Cstruct.create sizeof_msg_header by reusing it (necessitating only the set_msg_header_len hdr (Cstruct.len data |> Int32.of_int); in the loop).
Since max_data_chunk = 4096 this might make sense for large sends like file transfers?

Log.debug (fun f -> f "connection with dom0 established (version %ld)" version);
RExec_common.send server ~ty:`Trigger_service tsp >>= function
| `Eof -> Lwt.return @@ Error `Closed
| `Ok () ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XXX Not related to this PR, but I went looking for the receiving side of this, and fell over qubes-core-agent-linux/qrexec/qrexec-agent.c:handle_server_exec_request which seems to assume that buf is 0-terminated:

    if ((hdr->type == MSG_EXEC_CMDLINE || hdr->type == MSG_JUST_EXEC) &&
            !strstr(buf, ":nogui:")) {
   /* seems to me this strstr() should probably be a memmem() bounded to buf_len */

in
write_string service ~dst_offset:0 ~max_len:64;
write_string vm ~dst_offset:64 ~max_len:32;
write_string identifier ~dst_offset:(64+32) ~max_len:32;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason this doesn't use the [%struct] accessors functions like set_trigger_service_parameters_service_name? (Well that is a ridiculously long function name, but it does spare us the trouble of having to be careful to get the offsets and lengths right)

In the definition they seem to suggest these should be 0-terminated, so maybe we should pass max_len:whatever-1, or validate + fail if the application decides to pass us garbage input?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in QubesOS does add a zero byte at the very end of the char array in any case. But I agree that we should ensure it's also zero-terminated on our end because otherwise we potentially lose one byte of the identifier. https://github.com/QubesOS/qubes-core-qrexec/blob/master/daemon/qrexec-daemon.c#L817

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed in qrexec-client-vm that they just pass SOCKET every time as the identifier. Maybe we should do the same and remove identifier from the arguments making it simpler.


let rec read_line t =
let stdout = Cstruct.to_string t.stdout_buf
and stderr = Cstruct.to_string t.stderr_buf
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was about to pose the cheeky question of whether converting both to string on each call was necessary, but to my surprise I found that Cstruct doesn't seem to have a index function. That sucks.

Perhaps we can do something like storing the length of the previously examined stdout_buf and stderr_buf so we can avoid checking the same bytes on long lines over and over?

I'd still prefer if we could avoid converting both buffers if we know we are always going to go with the first if there's a match; would it be OK with you to serialize these so the second conversion only happens when newline stdout is None?

let retval = String.sub stdout 0 i in
t.stdout_buf <- Cstruct.shift t.stdout_buf (i + 1);
Lwt.return (`Stdout retval)
| _, Some i ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is a problem, but as long as there's stdout_buf newlines we will never return anything from `Stderr.
It doesn't seem entirely fair, but I guess not an issue in real life.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is similar to how read works FWIW. Stdout has priority over stderr.

service_name : uint8_t [@len 64];
target_domain : uint8_t [@len 32];
request_id : uint8_t [@len 32]
} [@@little_endian]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would make sense to preserve the these should be \0-terminated comments from the original header file?

let i = String.index s chr in
Some (String.sub s 0 i, String.sub s (i + 1) (String.length s - i - 1))
with Not_found ->
None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could avoid exceptions with something like:

let split chr s =
  match String.index_opt s chr with
  | None -> None
  | Some i -> Some (String.sub s 0 i, String.sub s (i + 1) (String.length s - i - 1))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String.index_opt was added in 4.05.0 and that's our new minimum, so I concur.

@reynir
Copy link
Member

reynir commented Aug 19, 2019

I think this doesn't take account for qrexec calls can be to and from the VM during the same session - so the server part will eat client messages, and/or the client part will eat server messages. I may be wrong, but I remember thinking of how to solve this when I wanted to implement this myself. I will dig some further...

let i = String.index s chr in
Some (String.sub s 0 i, String.sub s (i + 1) (String.length s - i - 1))
with Not_found ->
None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String.index_opt was added in 4.05.0 and that's our new minimum, so I concur.

let disconnect = QV.disconnect

let vchan_base_port =
match Vchan.Port.of_string "512" with
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this constant come from?

if Cstruct.len data' = 0
then QV.send t [hdr; data]
else QV.send t [hdr; data] >>= function
| `Eof -> return `Eof
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to return `Eof when we haven't sent all data?

let retval = String.sub stdout 0 i in
t.stdout_buf <- Cstruct.shift t.stdout_buf (i + 1);
Lwt.return (`Stdout retval)
| _, Some i ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is similar to how read works FWIW. Stdout has priority over stderr.

in
write_string service ~dst_offset:0 ~max_len:64;
write_string vm ~dst_offset:64 ~max_len:32;
write_string identifier ~dst_offset:(64+32) ~max_len:32;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in QubesOS does add a zero byte at the very end of the char array in any case. But I agree that we should ensure it's also zero-terminated on our end because otherwise we potentially lose one byte of the identifier. https://github.com/QubesOS/qubes-core-qrexec/blob/master/daemon/qrexec-daemon.c#L817

in
write_string service ~dst_offset:0 ~max_len:64;
write_string vm ~dst_offset:64 ~max_len:32;
write_string identifier ~dst_offset:(64+32) ~max_len:32;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed in qrexec-client-vm that they just pass SOCKET every time as the identifier. Maybe we should do the same and remove identifier from the arguments making it simpler.

@reynir
Copy link
Member

reynir commented Sep 30, 2019

Oh, sorry. I meant to submit pending review in #39 and not these old comments

@reynir reynir mentioned this pull request Sep 30, 2019
@talex5
Copy link
Collaborator

talex5 commented Oct 11, 2019

Closing this as it appears to be superseded by #39.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants