Skip to content

Commit aa9e15c

Browse files
authored
Fix multiple bugs in IO::Buffer.map and update its documentation. (ruby#15264)
- Buffer's size did not account for offset when mapping the file, leading to possible crashes. - Size and offset were not checked properly, leading to many situations raising EINVAL errors with generic messages. - Documentation was wrong.
1 parent 29d8a50 commit aa9e15c

File tree

2 files changed

+97
-21
lines changed

2 files changed

+97
-21
lines changed

io_buffer.c

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -667,26 +667,33 @@ rb_io_buffer_map(VALUE io, size_t size, rb_off_t offset, enum rb_io_buffer_flags
667667
* call-seq: IO::Buffer.map(file, [size, [offset, [flags]]]) -> io_buffer
668668
*
669669
* Create an IO::Buffer for reading from +file+ by memory-mapping the file.
670-
* +file_io+ should be a +File+ instance, opened for reading.
670+
* +file+ should be a +File+ instance, opened for reading or reading and writing.
671671
*
672672
* Optional +size+ and +offset+ of mapping can be specified.
673+
* Trying to map an empty file or specify +size+ of 0 will raise an error.
674+
* Valid values for +offset+ are system-dependent.
673675
*
674-
* By default, the buffer would be immutable (read only); to create a writable
675-
* mapping, you need to open a file in read-write mode, and explicitly pass
676-
* +flags+ argument without IO::Buffer::IMMUTABLE.
676+
* By default, the buffer is writable and expects the file to be writable.
677+
* It is also shared, so several processes can use the same mapping.
678+
*
679+
* You can pass IO::Buffer::READONLY in +flags+ argument to make a read-only buffer;
680+
* this allows to work with files opened only for reading.
681+
* Specifying IO::Buffer::PRIVATE in +flags+ creates a private mapping,
682+
* which will not impact other processes or the underlying file.
683+
* It also allows updating a buffer created from a read-only file.
677684
*
678685
* File.write('test.txt', 'test')
679686
*
680687
* buffer = IO::Buffer.map(File.open('test.txt'), nil, 0, IO::Buffer::READONLY)
681-
* # => #<IO::Buffer 0x00000001014a0000+4 MAPPED READONLY>
688+
* # => #<IO::Buffer 0x00000001014a0000+4 EXTERNAL MAPPED FILE SHARED READONLY>
682689
*
683690
* buffer.readonly? # => true
684691
*
685692
* buffer.get_string
686693
* # => "test"
687694
*
688695
* buffer.set_string('b', 0)
689-
* # `set_string': Buffer is not writable! (IO::Buffer::AccessError)
696+
* # 'IO::Buffer#set_string': Buffer is not writable! (IO::Buffer::AccessError)
690697
*
691698
* # create read/write mapping: length 4 bytes, offset 0, flags 0
692699
* buffer = IO::Buffer.map(File.open('test.txt', 'r+'), 4, 0)
@@ -708,31 +715,48 @@ io_buffer_map(int argc, VALUE *argv, VALUE klass)
708715
// We might like to handle a string path?
709716
VALUE io = argv[0];
710717

718+
rb_off_t file_size = rb_file_size(io);
719+
// Compiler can confirm that we handled file_size <= 0 case:
720+
if (UNLIKELY(file_size <= 0)) {
721+
rb_raise(rb_eArgError, "Invalid negative or zero file size!");
722+
}
723+
// Here, we assume that file_size is positive:
724+
else if (UNLIKELY((uintmax_t)file_size > SIZE_MAX)) {
725+
rb_raise(rb_eArgError, "File larger than address space!");
726+
}
727+
711728
size_t size;
712729
if (argc >= 2 && !RB_NIL_P(argv[1])) {
713730
size = io_buffer_extract_size(argv[1]);
714-
}
715-
else {
716-
rb_off_t file_size = rb_file_size(io);
717-
718-
// Compiler can confirm that we handled file_size < 0 case:
719-
if (file_size < 0) {
720-
rb_raise(rb_eArgError, "Invalid negative file size!");
731+
if (UNLIKELY(size == 0)) {
732+
rb_raise(rb_eArgError, "Size can't be zero!");
721733
}
722-
// Here, we assume that file_size is positive:
723-
else if ((uintmax_t)file_size > SIZE_MAX) {
724-
rb_raise(rb_eArgError, "File larger than address space!");
725-
}
726-
else {
727-
// This conversion should be safe:
728-
size = (size_t)file_size;
734+
if (UNLIKELY(size > (size_t)file_size)) {
735+
rb_raise(rb_eArgError, "Size can't be larger than file size!");
729736
}
730737
}
738+
else {
739+
// This conversion should be safe:
740+
size = (size_t)file_size;
741+
}
731742

732743
// This is the file offset, not the buffer offset:
733744
rb_off_t offset = 0;
734745
if (argc >= 3) {
735746
offset = NUM2OFFT(argv[2]);
747+
if (UNLIKELY(offset < 0)) {
748+
rb_raise(rb_eArgError, "Offset can't be negative!");
749+
}
750+
if (UNLIKELY(offset >= file_size)) {
751+
rb_raise(rb_eArgError, "Offset too large!");
752+
}
753+
if (RB_NIL_P(argv[1])) {
754+
// Decrease size if it's set from the actual file size:
755+
size = (size_t)(file_size - offset);
756+
}
757+
else if (UNLIKELY((size_t)(file_size - offset) < size)) {
758+
rb_raise(rb_eArgError, "Offset too large!");
759+
}
736760
}
737761

738762
enum rb_io_buffer_flags flags = 0;

test/ruby/test_io_buffer.rb

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,64 @@ def test_new_readonly
7373

7474
def test_file_mapped
7575
buffer = File.open(__FILE__) {|file| IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY)}
76-
contents = buffer.get_string
76+
assert_equal File.size(__FILE__), buffer.size
7777

78+
contents = buffer.get_string
7879
assert_include contents, "Hello World"
7980
assert_equal Encoding::BINARY, contents.encoding
8081
end
8182

83+
def test_file_mapped_with_size
84+
buffer = File.open(__FILE__) {|file| IO::Buffer.map(file, 30, 0, IO::Buffer::READONLY)}
85+
assert_equal 30, buffer.size
86+
87+
contents = buffer.get_string
88+
assert_equal "# frozen_string_literal: false", contents
89+
assert_equal Encoding::BINARY, contents.encoding
90+
end
91+
92+
def test_file_mapped_size_too_large
93+
assert_raise ArgumentError do
94+
File.open(__FILE__) {|file| IO::Buffer.map(file, 200_000, 0, IO::Buffer::READONLY)}
95+
end
96+
assert_raise ArgumentError do
97+
File.open(__FILE__) {|file| IO::Buffer.map(file, File.size(__FILE__) + 1, 0, IO::Buffer::READONLY)}
98+
end
99+
end
100+
101+
def test_file_mapped_size_just_enough
102+
File.open(__FILE__) {|file|
103+
assert_equal File.size(__FILE__), IO::Buffer.map(file, File.size(__FILE__), 0, IO::Buffer::READONLY).size
104+
}
105+
end
106+
107+
def test_file_mapped_offset_too_large
108+
assert_raise ArgumentError do
109+
File.open(__FILE__) {|file| IO::Buffer.map(file, nil, IO::Buffer::PAGE_SIZE * 100, IO::Buffer::READONLY)}
110+
end
111+
assert_raise ArgumentError do
112+
File.open(__FILE__) {|file| IO::Buffer.map(file, 20, IO::Buffer::PAGE_SIZE * 100, IO::Buffer::READONLY)}
113+
end
114+
end
115+
116+
def test_file_mapped_zero_size
117+
assert_raise ArgumentError do
118+
File.open(__FILE__) {|file| IO::Buffer.map(file, 0, 0, IO::Buffer::READONLY)}
119+
end
120+
end
121+
122+
def test_file_mapped_negative_size
123+
assert_raise ArgumentError do
124+
File.open(__FILE__) {|file| IO::Buffer.map(file, -10, 0, IO::Buffer::READONLY)}
125+
end
126+
end
127+
128+
def test_file_mapped_negative_offset
129+
assert_raise ArgumentError do
130+
File.open(__FILE__) {|file| IO::Buffer.map(file, 20, -1, IO::Buffer::READONLY)}
131+
end
132+
end
133+
82134
def test_file_mapped_invalid
83135
assert_raise TypeError do
84136
IO::Buffer.map("foobar")

0 commit comments

Comments
 (0)