Skip to content
This repository was archived by the owner on May 12, 2021. It is now read-only.

protocols/grpc: implement function to copy files#433

Merged
sboeuf merged 1 commit into
kata-containers:masterfrom
devimc:topic/uploadFile
Dec 18, 2018
Merged

protocols/grpc: implement function to copy files#433
sboeuf merged 1 commit into
kata-containers:masterfrom
devimc:topic/uploadFile

Conversation

@devimc
Copy link
Copy Markdown

@devimc devimc commented Dec 10, 2018

CopyFileRequest can be used to copy files from the host to the workload
rootfs (guest), this request is intended to copy "static" and small
files like resolv.conf and hosts, but any other file can be copied.

fixes #432

Signed-off-by: Julio Montes julio.montes@intel.com

@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 10, 2018

cc @egernst

@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 10, 2018

/test

Copy link
Copy Markdown
Member

@egernst egernst left a comment

Choose a reason for hiding this comment

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

lgtm, any tests we should be adding here?

Comment thread protocols/grpc/agent.proto Outdated

message UploadFileRequest {
string path = 1;
string content = 2;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think it'd make more sense to have this as an array of bytes instead of a string since it represents the content of the file.
And I'd use an extra field holding the size of this byte array.

Comment thread grpc.go Outdated
return emptyResp, err
}

if err := ioutil.WriteFile(req.Path, []byte(req.Content), os.FileMode(req.FileMode)); err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Having the content as an array of bytes would prevent from the cast here, and would be more appropriate.

Comment thread protocols/grpc/agent.proto Outdated
@devimc devimc force-pushed the topic/uploadFile branch 2 times, most recently from 1eb59e3 to 18e5dd2 Compare December 11, 2018 19:08
@devimc devimc changed the title protocols/grpc: implement function to upload files protocols/grpc: implement function to copy files Dec 11, 2018
@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 11, 2018

changes applied

@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 11, 2018

/test

@devimc devimc force-pushed the topic/uploadFile branch 2 times, most recently from 9f0704d to 73e1956 Compare December 11, 2018 21:35
@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 11, 2018

/test

Copy link
Copy Markdown

@sboeuf sboeuf left a comment

Choose a reason for hiding this comment

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

Thanks @devimc !

}

message CopyFileRequest {
string path = 1;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add offset and length here so that it is possible to send just part of a file. Also please put content at last of the struct.

What happens when the file size exceeds the grpc payload limit? If we support offset and length, client can send a file by parts.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@bergwolf
I think length is not needed, since we can get it with len(content), is that correct?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@devimc good point!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this the absolute guest root context path? In other words, is this API allowing changes to, say, /sbin/init? Or is it designed purely to copy files into the workloads rootfs directory?

If it's possible to overwrite the init binary, we'd better disallow that for now until the agent is able to serialise it's state and re-exec itself.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  • What about adding a DoNotOverwrite bool so the client can change the default behaviour of overwriting an existing file? It could be called NoClobber bool I guess, but "clobber" is not a common word even if English is your native tongue ;)

But maybe the default should be to not overwrite? What is the intent of this change?

Copy link
Copy Markdown
Author

@devimc devimc Dec 13, 2018

Choose a reason for hiding this comment

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

🤓 according with g, clobber means hit (someone) hard. "if he does that I'll clobber him!"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yeah, probably best not to use that term! No NoClobber I say! 😄

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm even okay with the overwrite just happening, if it keeps it simple. You can do similar things today with docker CLI, afaiu, right? Either way - adding overwrite option seems reasonable to me.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Are we still waiting for more changes to this proto? Or can we resolve this item?

@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 12, 2018

/test

1 similar comment
@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 12, 2018

/test

Comment thread grpc.go Outdated
return emptyResp, err
}

if _, err := file.Write(req.Content); err != nil {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can simplify the two with file.WriteAt().

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It would be safer to write the copied file to a temporary one, then atomically move it over the top of the existing one, overwriting the original. The downside being we'll temporarily need more disk space to retain two copies. We'll also have to take care to ensure the temp copy is written in the same directory as the original to avoid any FS-crossing performance issues.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

sounds good, but then we'll need two commands CopyFile and MoveFile because CopyFile doesn't know when the temporarily file should be renamed/moved

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I just mean that the function could do something like the following (untested):

dir := filepath.Dir(req.Path)
file := filepath.Base(req.Path)

// create hidden temp file in same directory as original file with a random suffix
tmpfile, err := ioutil.TempFile(dir, "."+file+"*")

name := tmpfile.Name()
tmpfile.WriteAt(...)
tmpfile.Close()

// move the temp file over the top of the destination path atomically
err := os.Rename(name, req.Path)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@jodh-intel ohh I see a problem with this design, if offset is != 0 and dest exist, it must be copied to the temporary file, the problem is when dest is a humongous file, copying dest to the temporarily file will be expensive in both time and disk space.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

yep - slow, but safe. We need to capture these sorts of scenarios in a doc and decide on the best approach in each case I think.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@sboeuf writes:
well you need to think about this CopyFile() command as a workaround for 9p, so we're not planning on having huge files being passed through here. Thus, don't know if we should worry about performance here. I value more the security check personally.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I have changed the algorithm to copy files, please take a look

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this comment properly addressed? Can we resolve it?

Copy link
Copy Markdown

@jodh-intel jodh-intel left a comment

Choose a reason for hiding this comment

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

I've got lots of questions about this. Ultimately, I think we need a design doc for this change to understand the intention, scope, potential security impact and so on as we're essentially adding a one-way ftp server here.

}

message CopyFileRequest {
string path = 1;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this the absolute guest root context path? In other words, is this API allowing changes to, say, /sbin/init? Or is it designed purely to copy files into the workloads rootfs directory?

If it's possible to overwrite the init binary, we'd better disallow that for now until the agent is able to serialise it's state and re-exec itself.

Comment thread grpc.go Outdated
return emptyResp, err
}

file, err := os.OpenFile(req.Path, os.O_WRONLY|os.O_CREATE, os.FileMode(req.FileMode))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  • What happens if the file exists as, say, a socket? Won't this overwrite it as a regular file? Maybe we should check to ensure the types are the same?

  • Maybe we should enhance the getAgentDetails()gRPC API to include the architecture "just in case" to handle the scenario where the caller wants to send a binary file?

  • I don't think we should just blindly create a file with the mode specified. What if this is a long-running container and the CopyFile() ends up opening ports somehow or overwriting critical files, or making critical file world-writable...

}

message CopyFileRequest {
string path = 1;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  • What about adding a DoNotOverwrite bool so the client can change the default behaviour of overwriting an existing file? It could be called NoClobber bool I guess, but "clobber" is not a common word even if English is your native tongue ;)

But maybe the default should be to not overwrite? What is the intent of this change?

Comment thread grpc.go
return &gpb.Empty{}, nil
}

func (a *agentGRPC) CopyFile(ctx context.Context, req *pb.CopyFileRequest) (*gpb.Empty, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We need unit tests for this - think security :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

sure thing

@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 13, 2018

@jodh-intel

Is this the absolute guest root context path? In other words, is this API allowing changes to, say, /sbin/init? Or is it designed purely to copy files into the workloads rootfs directory?

it's designed to copy files into the workloads rootfs directory, I think we can validate that Path always starts with /tmp

@devimc devimc force-pushed the topic/uploadFile branch 2 times, most recently from b31d58f to a0ed28e Compare December 13, 2018 22:06
@caoruidong
Copy link
Copy Markdown
Member

How to evaluate a file is small enough? Will this used for only /etc stuffs?

@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 14, 2018

@caoruidong , actually I need to update the commit message, since the algorithm to copy file has changed and now there is not limit in the size

@bergwolf
Copy link
Copy Markdown
Member

Along with the copyfile approach, did we evaluate the option of reading metadata from the FC guest-facing metadata service?

Comment thread grpc.go
}).Debugf("Checking temporary file size")
// if file size is not equal to the expected size means that copy file operation has not finished.
// CopyFile should be called again with new content and a different offset.
if st.Size() != req.FileSize {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this needed? If WriteAt is not buggy, I don't see why we need to re-check the file size.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

this is not to check WriteAt, client sends file's size (expected size), temporary file is renamed to destination file once its size is equal to expected size

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't see why we need stat(). In each request, we have offset and len(content), and the sum of them should just equal to file size in the request otherwise the request is illegal on its own. If we really want to validate that, we should do it upon handling the request and before really writing to the file. The current implementation of write->stat->check size, is validating the golang fs interfaces instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh, I see your point. Sorry for the noise.

@egernst
Copy link
Copy Markdown
Member

egernst commented Dec 17, 2018

@bergwolf - we haven't tested with FC's MDS, just vsock so far. While this may be interesting down the road, I want to enable this no-9p use case for more hypervisors than just FC (NEMU/QEMU, and simplify requirements for additional hypervisors).

@bergwolf
Copy link
Copy Markdown
Member

@egernst I see, let's move forward with copyfile then.

@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 17, 2018

/test

@sboeuf
Copy link
Copy Markdown

sboeuf commented Dec 17, 2018

@jodh-intel @bergwolf
I've read your comments and concerns, but I think we should really keep this method as simple as possible here for now.
First, the need for this function comes from the need to support hypervisor without a 9p or simple filesystem support. So the container rootfs is exposed as a block device, but simple files such as resolv.conf still needs to be passed inside the VM through a different way since they're not part of the rootfs.

The call needs to be kept simple as it will be the runtime's responsibility to update the files by monitoring any change. But for now that's a simple way to pass those files.

From a security perspective, if we keep it simple, and one-way, we're not opening any security holes, since we won't provide any mechanism to write back to the host.
By keeping it simple, I mean that we should not try to support copying the same file in several parts, at least for now.

I agree we should make sure we don't allow this call to override some files of our VM rootfs, but I think @devimc took care of this already by writing to /tmp.

@devimc
You should make sure to clearly document in the code the purpose of this function, explaining the need for it and the very limited scope that is targeted here.

Comment thread grpc.go Outdated

// create a temporary file and write the content.
tmpPath := req.Path + ".tmp"
tmpFile, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE, 0755)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could we make this 0600 as we set the perms to the requested values later?

Comment thread grpc.go Outdated
return emptyResp, err
}

return emptyResp, os.Chmod(req.Path, os.FileMode(req.FileMode))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's safer to do the os.Chmod() before the os.Rename() as that way there is no "window" for the destination file to have higher privs (from the temp file) than the requested privs.

@devimc devimc force-pushed the topic/uploadFile branch 2 times, most recently from 84cb5bd to ef7f3f8 Compare December 17, 2018 18:04
@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 17, 2018

@sboeuf @jodh-intel changes applied

@sboeuf
Copy link
Copy Markdown

sboeuf commented Dec 17, 2018

/test

@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 17, 2018

/test

Copy link
Copy Markdown

@sboeuf sboeuf left a comment

Choose a reason for hiding this comment

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

@devimc oh okay I thought we would go with simpler implementation, but sending the file through multiple calls is handled properly here, so I'm actually fine with it!
Thanks for adding more comments, this helps a lot understanding the purpose here.

Nit: Adding some comments to protocols/grpc/agent.proto, particularly each field of CopyFileRequest would be a good way to provide information about how CopyFileRequest is supposed to be used.

@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 17, 2018

@sboeuf changes applied

@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 17, 2018

/test

@bergwolf
Copy link
Copy Markdown
Member

bergwolf commented Dec 18, 2018

LGTM! Thanks @devimc !

Approved with PullApprove Approved with PullApprove

Comment thread protocols/mockserver/mockserver.go Outdated
func (m *mockServer) CopyFile(ctx context.Context, req *pb.CopyFileRequest) (*types.Empty, error) {
mockLock.RLock()
defer mockLock.RUnlock()
if err := m.podExist(); err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You could simplify to just:

    return nil, m.podExist()

// to the destination path.
int64 file_size = 2;
// FileMode is the file mode.
uint32 file_mode = 3;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Don't we also need to send uid+gid for both file and directory as currently they will only ever be created as the user the agent is running as (root)? Maybe less important but also what about atime, mtime, ctime?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yes files are created as root, I'll add uid and gid, the other options can be added later on, what do you think?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sounds good.

Comment thread protocols/grpc/agent.proto Outdated
}

message CopyFileRequest {
// Path is the destination path in the guest.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for adding the comment. I think it could be made clearer though that the path must be:

  • absolute and canonical.
  • below /run.

Comment thread grpc.go Outdated
func (a *agentGRPC) CopyFile(ctx context.Context, req *pb.CopyFileRequest) (*gpb.Empty, error) {
// container's rootfs is mounted at /run, in order to avoid overwrite guest's rootfs files, only
// is possible to copy files to /run
if !strings.HasPrefix(req.Path, containersRootfsPath) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This isn't strict enough - consider the following, which will be accepted:

req.Path = "/run/../sbin/init"

But since we can't guarantee the file will already exist, it can't be expanded trivially. How about just adding an extra check and denying the request if it contains "../" ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

good catch!, I think realpath can help us here

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

thanks - wfm.

CopyFileRequest can be used to copy files from the host to the workload
rootfs (guest), this request is intended to copy "static" and small
files like resolv.conf and hosts, but any other file can be copied.

fixes kata-containers#432

Signed-off-by: Julio Montes <julio.montes@intel.com>
@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 18, 2018

@jodh-intel changes applied, thanks

@devimc
Copy link
Copy Markdown
Author

devimc commented Dec 18, 2018

/test

// DirMode is the mode for the parent directories of destination path.
uint32 dir_mode = 4;
// Uid is the numeric user id.
int32 uid = 5;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There is the question of how the client is going to know the uid and gid values of course. They can prolly safely assume the standard users exist, but if they want to get elaborate, we may need to consider expanding GetGuestDetails() to return a list of uids+gids (optionally).... I know! This appears to be such a simple feature, but... 😄

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yes, I think so 😃

@jodh-intel
Copy link
Copy Markdown

Given the recent discussion and changes, @sboeuf and @egernst may want to re-ack this PR?

Copy link
Copy Markdown
Member

@egernst egernst left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for the great work, @devimc and @jodh-intel !

Copy link
Copy Markdown

@sboeuf sboeuf left a comment

Choose a reason for hiding this comment

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

Thanks guys! looks good!

@sboeuf sboeuf merged commit 8e48125 into kata-containers:master Dec 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement a function/request to upload files from host to guest

7 participants