protocols/grpc: implement function to copy files#433
Conversation
|
cc @egernst |
ca1cc2c to
9ac03c0
Compare
|
/test |
egernst
left a comment
There was a problem hiding this comment.
lgtm, any tests we should be adding here?
|
|
||
| message UploadFileRequest { | ||
| string path = 1; | ||
| string content = 2; |
There was a problem hiding this comment.
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.
| return emptyResp, err | ||
| } | ||
|
|
||
| if err := ioutil.WriteFile(req.Path, []byte(req.Content), os.FileMode(req.FileMode)); err != nil { |
There was a problem hiding this comment.
Having the content as an array of bytes would prevent from the cast here, and would be more appropriate.
1eb59e3 to
18e5dd2
Compare
|
changes applied |
|
/test |
9f0704d to
73e1956
Compare
|
/test |
| } | ||
|
|
||
| message CopyFileRequest { | ||
| string path = 1; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@bergwolf
I think length is not needed, since we can get it with len(content), is that correct?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
- What about adding a
DoNotOverwrite boolso the client can change the default behaviour of overwriting an existing file? It could be calledNoClobber boolI 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?
There was a problem hiding this comment.
🤓 according with g, clobber means hit (someone) hard. "if he does that I'll clobber him!"
There was a problem hiding this comment.
Yeah, probably best not to use that term! No NoClobber I say! 😄
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Are we still waiting for more changes to this proto? Or can we resolve this item?
73e1956 to
a5fff83
Compare
|
/test |
1 similar comment
|
/test |
| return emptyResp, err | ||
| } | ||
|
|
||
| if _, err := file.Write(req.Content); err != nil { |
There was a problem hiding this comment.
You can simplify the two with file.WriteAt().
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
I have changed the algorithm to copy files, please take a look
There was a problem hiding this comment.
Is this comment properly addressed? Can we resolve it?
jodh-intel
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
| return emptyResp, err | ||
| } | ||
|
|
||
| file, err := os.OpenFile(req.Path, os.O_WRONLY|os.O_CREATE, os.FileMode(req.FileMode)) |
There was a problem hiding this comment.
-
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; |
There was a problem hiding this comment.
- What about adding a
DoNotOverwrite boolso the client can change the default behaviour of overwriting an existing file? It could be calledNoClobber boolI 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?
| return &gpb.Empty{}, nil | ||
| } | ||
|
|
||
| func (a *agentGRPC) CopyFile(ctx context.Context, req *pb.CopyFileRequest) (*gpb.Empty, error) { |
There was a problem hiding this comment.
We need unit tests for this - think security :)
it's designed to copy files into the workloads rootfs directory, I think we can validate that |
b31d58f to
a0ed28e
Compare
|
How to evaluate a file is small enough? Will this used for only /etc stuffs? |
|
@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 |
a0ed28e to
de48ce2
Compare
|
Along with the |
| }).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 { |
There was a problem hiding this comment.
Why is this needed? If WriteAt is not buggy, I don't see why we need to re-check the file size.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Oh, I see your point. Sorry for the noise.
|
@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). |
|
@egernst I see, let's move forward with copyfile then. |
de48ce2 to
7f1bab0
Compare
|
/test |
|
@jodh-intel @bergwolf 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. 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 @devimc |
|
|
||
| // create a temporary file and write the content. | ||
| tmpPath := req.Path + ".tmp" | ||
| tmpFile, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE, 0755) |
There was a problem hiding this comment.
Could we make this 0600 as we set the perms to the requested values later?
| return emptyResp, err | ||
| } | ||
|
|
||
| return emptyResp, os.Chmod(req.Path, os.FileMode(req.FileMode)) |
There was a problem hiding this comment.
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.
84cb5bd to
ef7f3f8
Compare
|
@sboeuf @jodh-intel changes applied |
|
/test |
ef7f3f8 to
edcd564
Compare
|
/test |
sboeuf
left a comment
There was a problem hiding this comment.
@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.
edcd564 to
a66b3f5
Compare
|
@sboeuf changes applied |
|
/test |
|
LGTM! Thanks @devimc ! |
| func (m *mockServer) CopyFile(ctx context.Context, req *pb.CopyFileRequest) (*types.Empty, error) { | ||
| mockLock.RLock() | ||
| defer mockLock.RUnlock() | ||
| if err := m.podExist(); err != nil { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
yes files are created as root, I'll add uid and gid, the other options can be added later on, what do you think?
| } | ||
|
|
||
| message CopyFileRequest { | ||
| // Path is the destination path in the guest. |
There was a problem hiding this comment.
Thanks for adding the comment. I think it could be made clearer though that the path must be:
- absolute and canonical.
- below
/run.
| 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) { |
There was a problem hiding this comment.
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 "../" ?
There was a problem hiding this comment.
good catch!, I think realpath can help us here
a66b3f5 to
795d9f9
Compare
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>
795d9f9 to
169d755
Compare
|
@jodh-intel changes applied, thanks |
|
/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; |
There was a problem hiding this comment.
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... 😄
egernst
left a comment
There was a problem hiding this comment.
LGTM! Thanks for the great work, @devimc and @jodh-intel !
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