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

fix readlink/readlinkat to return too long only when it is long #1109

Merged
merged 8 commits into from
Aug 22, 2019

Conversation

sendilkumarn
Copy link
Contributor

Currently readlink returns ENAMETOOLONG when the bufferSize is equal to the result size.

Consider the below case

int main(int argc, const char * argv[]) {
    
    std::string filename = "/tmp/test-dir/target";
    
    size_t bufferSize = 9;
    char* buffer = new char[bufferSize];
    size_t rc = readlink (filename.c_str(), buffer, bufferSize); 
   // Since `readlink's` output depends on the `bufferSize`. If the`bufferSize` is 9 or greater than 
   // 9 then it will return 9 or else it will return `bufferSize` as the value of `rc`. 

    return 0;
}

@asomers
Copy link
Member

asomers commented Aug 22, 2019

I'm afraid I don't understand the problem. Could you please be more clear? Give a complete example demonstrating the wrong behavior, and what you think the correct behavior should be.

@sendilkumarn
Copy link
Contributor Author

sendilkumarn commented Aug 22, 2019

I'm afraid I don't understand the problem.

Oops sorry! Let me try to clarify.

Consider the following folder structure

/tmp 
  |____ target
  |____ test-dir
       |____ target (symlinked folder)

With nix when we try to readLink this scenario.

let src = "/tmp/target"; 
let dest = "tmp/target/test-dir";

It works correctly when we pass in a buffer that is bigger than the src length

let mut buf = vec![0; src.len() + 1]; 
println!("{}", str::from_utf8(&buf).unwrap()); // prints "/tmp/target"

Actual: But when we pass in a buffer that is smaller than the src length, it returns ENAMETOOLONG exception.

let mut buf = vec![0; src.len()];  // returns `ENAMETOOLONG` 

Expected: Instead it should truncate the contents (to a length of buffer size), in case the buffer is too small to hold all of the contents. - from here

@asomers
Copy link
Member

asomers commented Aug 22, 2019

Truncating the result is what readlink(2) does. But Nix is supposed to be safe and easy to use; we don't want to truncate any results. Perhaps ENAMETOOLONG isn't the best error to return, but a truncated result would be even worse.

@sendilkumarn
Copy link
Contributor Author

but a truncated result would be even worse.

That makes sense. I agree but this means we should always send in a buffer that is greater than the required size or if not it fails.

We should at least allow the buffer to have the same size as that of the required result. What do you think?

Perhaps ENAMETOOLONG isn't the best error to return

Can we return EINVAL instead of ENAMETOOLONG?

@asomers
Copy link
Member

asomers commented Aug 22, 2019

but a truncated result would be even worse.

That makes sense. I agree but this means we should always send in a buffer that is greater than the required size or if not it fails.

We should at least allow the buffer to have the same size as that of the required result. What do you think?

Perhaps ENAMETOOLONG isn't the best error to return

Can we return EINVAL instead of ENAMETOOLONG?

No, that would have the same problem. The problem with both of those error codes is that they overload errors used by the OS. Better to use our own error type. Better yet would be to never return an error. How about changing that function to allocate its storage internally and return an OsString, a little like this?

fn readlink<P: ...>(path: P) -> Result<OsString> {
    let v = Vec::with_capacity(libc::PATH_MAX);
    let r = libc::readlink(..., v.as_mut_ptr() as *mut c_char, v.capacity());
    if let Ok(len) = Errno::result(r) {
        unsafe { v.set_len(len) };
        OsString::from_vec(v)
    } else {
        /* error handling */
    }
}

@sendilkumarn
Copy link
Contributor Author

@asomers That is a good idea. I updated the code to reflect your suggested changes.

CHANGELOG.md Outdated
@@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] - ReleaseDate
### Added
### Changed
- Changed `readlink` and `readlinkat` to return `osString`
Copy link
Member

Choose a reason for hiding this comment

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

The first letter of OsString should be capitalized. Also, since this change is backwards incompatible it would be courteous to our users to explain how to convert their existing code.

src/fcntl.rs Outdated
}
}
}

pub fn readlink<'a, P: ?Sized + NixPath>(path: &P, buffer: &'a mut [u8]) -> Result<&'a OsStr> {
pub fn readlink<'a, P: ?Sized + NixPath>(path: &P) -> Result<OsString> {
let mut v = vec![0u8; libc::PATH_MAX as usize];
Copy link
Member

Choose a reason for hiding this comment

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

No need to zero initialize. You can use an uninitialized vec instead.

src/fcntl.rs Outdated
})?;

wrap_readlink_result(buffer, res)

Copy link
Member

Choose a reason for hiding this comment

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

Trailing space here.

([#1109](https://github.com/nix-rust/nix/pull/1109))

Copy link
Member

Choose a reason for hiding this comment

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

This example isn't going to mean much to a reader. I'm thinking something in English, like "the buffer argument of readlink and readlinkat has been removed, and the return value is now an owned type. Existing code can be updated by removing the buffer argument and removing any clone or similar operation on the output."

src/fcntl.rs Outdated
let res = path.with_nix_path(|cstr| {
unsafe { libc::readlink(cstr.as_ptr(), v.as_mut_ptr() as *mut c_char, v.len() as size_t) }
unsafe { libc::readlink(cstr.as_ptr(), v.as_mut_ptr() as *mut c_char, len as size_t) }
Copy link
Member

Choose a reason for hiding this comment

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

You seem to be unaware of the capacity method. You should write it like this:

let mut v = Vec::with_capacity(libc::PATH_MAX as usize);
let res = path.with_nix_path(|cstr| {
    unsafe { libc::readlink(cstr.as_ptr(), v.as_mut_ptr() as *mut c_char, v.capacity() as size_t) }

CHANGELOG.md Outdated
@@ -12,8 +12,16 @@ This project adheres to [Semantic Versioning](http://semver.org/).
```rust
use nix::fcntl::{readlink, readlinkat};
Copy link
Member

Choose a reason for hiding this comment

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

This line isn't important to readers. You can suppress it with a leading #, and delete the following blank line.

CHANGELOG.md Outdated
// and removing any clone or similar operation on the output

// old code `readlink(&path, &mut buf)` can be replaced with the following
readlink(&path); // this returns OsString
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 would be more self-documenting like this:

let _: OsString = readlink(&path);

Copy link
Member

@asomers asomers 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 your submission!

bors r+

bors bot added a commit that referenced this pull request Aug 22, 2019
1109: fix readlink/readlinkat to return too long only when it is long r=asomers a=sendilkumarn

Currently readlink returns `ENAMETOOLONG` when the bufferSize is equal to the result size.  

Consider the below case

```c++
int main(int argc, const char * argv[]) {
    
    std::string filename = "/tmp/test-dir/target";
    
    size_t bufferSize = 9;
    char* buffer = new char[bufferSize];
    size_t rc = readlink (filename.c_str(), buffer, bufferSize); 
   // Since `readlink's` output depends on the `bufferSize`. If the`bufferSize` is 9 or greater than 
   // 9 then it will return 9 or else it will return `bufferSize` as the value of `rc`. 

    return 0;
}
```

Co-authored-by: Sendil Kumar <sendilkumarn@live.com>
Co-authored-by: Sendil Kumar N <sendilkumarn@live.com>
@bors
Copy link
Contributor

bors bot commented Aug 22, 2019

Build succeeded

@bors bors bot merged commit fa50f63 into nix-rust:master Aug 22, 2019
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.

3 participants