compress: handle overflows in varints

This handles corrupt data explicitly.
This commit is contained in:
Motiejus Jakštys 2023-06-06 20:07:34 +03:00
parent d6150734f1
commit 277a48296a
5 changed files with 72 additions and 55 deletions

View File

@ -307,13 +307,13 @@ const GroupMembersIter = struct {
total: usize, total: usize,
arr: []const u8, arr: []const u8,
pub fn nextMust(self: *GroupMembersIter) ?u64 { pub fn next(self: *GroupMembersIter) error{Overflow}!?u64 {
return self.it.nextMust(); return self.it.next();
} }
}; };
pub fn groupMembersIter(members_slice: []const u8) GroupMembersIter { pub fn groupMembersIter(members_slice: []const u8) error{Overflow}!GroupMembersIter {
var vit = compress.varintSliceIteratorMust(members_slice); var vit = try compress.varintSliceIterator(members_slice);
var it = compress.deltaDecompressionIterator(&vit); var it = compress.deltaDecompressionIterator(&vit);
return GroupMembersIter{ return GroupMembersIter{
.arr = members_slice, .arr = members_slice,
@ -324,9 +324,12 @@ pub fn groupMembersIter(members_slice: []const u8) GroupMembersIter {
} }
// dumps PackedGroup to []u8 and returns a CGroup. // dumps PackedGroup to []u8 and returns a CGroup.
pub fn packCGroup(self: *const DB, group: *const PackedGroup, buf: []u8) error{BufferTooSmall}!CGroup { pub fn packCGroup(self: *const DB, group: *const PackedGroup, buf: []u8) error{
Overflow,
BufferTooSmall,
}!CGroup {
const members_slice = self.groupmembers[group.members_offset..]; const members_slice = self.groupmembers[group.members_offset..];
var it = groupMembersIter(members_slice); var it = try groupMembersIter(members_slice);
const num_members = it.total; const num_members = it.total;
const ptr_end = @sizeOf(?[*:0]const u8) * (num_members + 1); const ptr_end = @sizeOf(?[*:0]const u8) * (num_members + 1);
@ -338,7 +341,7 @@ pub fn packCGroup(self: *const DB, group: *const PackedGroup, buf: []u8) error{B
var buf_offset: usize = ptr_end; var buf_offset: usize = ptr_end;
var i: usize = 0; var i: usize = 0;
while (it.nextMust()) |member_offset| : (i += 1) { while (try it.next()) |member_offset| : (i += 1) {
const entry = PackedUser.fromBytes(@alignCast(8, self.users[member_offset << 3 ..])); const entry = PackedUser.fromBytes(@alignCast(8, self.users[member_offset << 3 ..]));
const start = buf_offset; const start = buf_offset;
const name = entry.user.name(); const name = entry.user.name();
@ -389,7 +392,7 @@ pub fn getgrnam(
name: []const u8, name: []const u8,
buf: []u8, buf: []u8,
omit_members: bool, omit_members: bool,
) error{BufferTooSmall}!?CGroup { ) error{ Overflow, BufferTooSmall }!?CGroup {
const group = self.getGroupByName(name) orelse return null; const group = self.getGroupByName(name) orelse return null;
if (omit_members) if (omit_members)
return try packCGroupNoMembers(&group, buf) return try packCGroupNoMembers(&group, buf)
@ -403,7 +406,7 @@ pub fn getgrgid(
gid: u32, gid: u32,
buf: []u8, buf: []u8,
omit_members: bool, omit_members: bool,
) error{BufferTooSmall}!?CGroup { ) error{ Overflow, BufferTooSmall }!?CGroup {
const group = self.getGroupByGid(gid) orelse return null; const group = self.getGroupByGid(gid) orelse return null;
if (omit_members) if (omit_members)
return try packCGroupNoMembers(&group, buf) return try packCGroupNoMembers(&group, buf)
@ -687,8 +690,8 @@ fn groupsSection(
}; };
} }
pub fn userGids(self: *const DB, offset: u64) compress.DeltaDecompressionIterator { pub fn userGids(self: *const DB, offset: u64) error{Overflow}!compress.DeltaDecompressionIterator {
var vit = compress.varintSliceIteratorMust(self.additional_gids[offset..]); var vit = try compress.varintSliceIterator(self.additional_gids[offset..]);
return compress.deltaDecompressionIterator(&vit); return compress.deltaDecompressionIterator(&vit);
} }

View File

@ -80,12 +80,6 @@ pub fn uvarint(buf: []const u8) error{Overflow}!Varint {
}; };
} }
pub fn uvarintMust(buf: []const u8) Varint {
return uvarint(buf) catch |err| switch (err) {
error.Overflow => unreachable,
};
}
// https://golang.org/pkg/encoding/binary/#PutUvarint // https://golang.org/pkg/encoding/binary/#PutUvarint
pub fn putUvarint(buf: []u8, x: u64) usize { pub fn putUvarint(buf: []u8, x: u64) usize {
var i: usize = 0; var i: usize = 0;
@ -118,12 +112,6 @@ pub const VarintSliceIterator = struct {
return value.value; return value.value;
} }
pub fn nextMust(self: *VarintSliceIterator) ?u64 {
return self.next() catch |err| switch (err) {
error.Overflow => unreachable,
};
}
// returns the number of remaining items. If called before the first // returns the number of remaining items. If called before the first
// next(), returns the length of the slice. // next(), returns the length of the slice.
pub fn remaining(self: *const VarintSliceIterator) usize { pub fn remaining(self: *const VarintSliceIterator) usize {
@ -140,12 +128,6 @@ pub fn varintSliceIterator(arr: []const u8) error{Overflow}!VarintSliceIterator
}; };
} }
pub fn varintSliceIteratorMust(arr: []const u8) VarintSliceIterator {
return varintSliceIterator(arr) catch |err| switch (err) {
error.Overflow => unreachable,
};
}
pub const DeltaDecompressionIterator = struct { pub const DeltaDecompressionIterator = struct {
vit: *VarintSliceIterator, vit: *VarintSliceIterator,
prev: u64, prev: u64,
@ -167,12 +149,6 @@ pub const DeltaDecompressionIterator = struct {
pub fn remaining(self: *const DeltaDecompressionIterator) usize { pub fn remaining(self: *const DeltaDecompressionIterator) usize {
return self.vit.remaining; return self.vit.remaining;
} }
pub fn nextMust(self: *DeltaDecompressionIterator) ?u64 {
return self.next() catch |err| switch (err) {
error.Overflow => unreachable,
};
}
}; };
pub fn deltaDecompressionIterator(vit: *VarintSliceIterator) DeltaDecompressionIterator { pub fn deltaDecompressionIterator(vit: *VarintSliceIterator) DeltaDecompressionIterator {
@ -344,8 +320,8 @@ const GroupMembersIter = struct {
total: usize, total: usize,
}; };
pub fn groupMembersIter(members_slice: []const u8) GroupMembersIter { pub fn groupMembersIter(members_slice: []const u8) error{Overflow}!GroupMembersIter {
var vit = compress.varintSliceIteratorMust(members_slice); var vit = try compress.varintSliceIterator(members_slice);
var it = compress.deltaDecompressionIterator(&vit); var it = compress.deltaDecompressionIterator(&vit);
return GroupMembersIter{ return GroupMembersIter{
.vit = vit, .vit = vit,
@ -357,11 +333,10 @@ pub fn groupMembersIter(members_slice: []const u8) GroupMembersIter {
test "compress: trying to repro pointer change of DB.groupMembersIter" { test "compress: trying to repro pointer change of DB.groupMembersIter" {
const members_slice = &[_]u8{ 4, 0, 60, 2, 2, 2, 64, 2 }; const members_slice = &[_]u8{ 4, 0, 60, 2, 2, 2, 64, 2 };
var members = groupMembersIter(members_slice); var members = try groupMembersIter(members_slice);
var i: usize = 0; var i: usize = 0;
while (members.it.nextMust()) |member_offset| : (i += 1) { while (try members.it.next()) |member_offset| : (i += 1) {
_ = member_offset; _ = member_offset;
//std.debug.print("member_offset: {d}\n", .{member_offset});
} }
} }

View File

@ -221,6 +221,7 @@ fn getgrgid_r(
var buf = buffer[0..buflen]; var buf = buffer[0..buflen];
const cgroup = db.getgrgid(gid, buf, omit_members) catch |err| switch (err) { const cgroup = db.getgrgid(gid, buf, omit_members) catch |err| switch (err) {
error.Overflow => return badFile(errnop),
error.BufferTooSmall => { error.BufferTooSmall => {
errnop.* = @enumToInt(os.E.RANGE); errnop.* = @enumToInt(os.E.RANGE);
return c.NSS_STATUS_TRYAGAIN; return c.NSS_STATUS_TRYAGAIN;
@ -263,6 +264,7 @@ fn getgrnam_r(
const nameSlice = mem.sliceTo(name, 0); const nameSlice = mem.sliceTo(name, 0);
var buf = buffer[0..buflen]; var buf = buffer[0..buflen];
const cgroup = db.getgrnam(nameSlice, buf, omit_members) catch |err| switch (err) { const cgroup = db.getgrnam(nameSlice, buf, omit_members) catch |err| switch (err) {
error.Overflow => return badFile(errnop),
error.BufferTooSmall => { error.BufferTooSmall => {
errnop.* = @enumToInt(os.E.RANGE); errnop.* = @enumToInt(os.E.RANGE);
return c.NSS_STATUS_TRYAGAIN; return c.NSS_STATUS_TRYAGAIN;
@ -379,6 +381,10 @@ fn getgrent_r(
result.* = cgroup; result.* = cgroup;
return c.NSS_STATUS_SUCCESS; return c.NSS_STATUS_SUCCESS;
} else |err| switch (err) { } else |err| switch (err) {
error.Overflow => {
it.rollback();
return badFile(errnop);
},
error.BufferTooSmall => { error.BufferTooSmall => {
it.rollback(); it.rollback();
errnop.* = @enumToInt(os.E.RANGE); errnop.* = @enumToInt(os.E.RANGE);
@ -469,14 +475,18 @@ fn initgroups_dyn(
return c.NSS_STATUS_NOTFOUND; return c.NSS_STATUS_NOTFOUND;
}; };
var gids = db.userGids(user.additional_gids_offset); var gids = db.userGids(user.additional_gids_offset) catch |err| switch (err) {
error.Overflow => return badFile(errnop),
};
const remaining = gids.vit.remaining; const remaining = gids.vit.remaining;
// the implementation below is ported from glibc's db-initgroups.c // the implementation below is ported from glibc's db-initgroups.c
// even though we know the size of the groups upfront, I found it too difficult // even though we know the size of the groups upfront, I found it too difficult
// to preallocate and juggle size, start and limit while keeping glibc happy. // to preallocate and juggle size, start and limit while keeping glibc happy.
var any: bool = false; var any: bool = false;
while (gids.nextMust()) |gid| { while (gids.next() catch |err| switch (err) {
error.Overflow => return badFile(errnop),
}) |gid| {
if (start.* == size.*) { if (start.* == size.*) {
if (limit > 0 and size.* == limit) if (limit > 0 and size.* == limit)
return c.NSS_STATUS_SUCCESS; return c.NSS_STATUS_SUCCESS;
@ -528,6 +538,11 @@ fn getDBErrno(errnop: *c_int) ?*const DB {
return &state.file.db; return &state.file.db;
} }
fn badFile(errnop: *c_int) c.enum_nss_status {
errnop.* = @enumToInt(os.E.NOENT);
return c.NSS_STATUS_NOTFOUND;
}
// isId tells if this command is "id". Reads the cmdline // isId tells if this command is "id". Reads the cmdline
// from the given fd. Returns false on any error. // from the given fd. Returns false on any error.
fn isId(fd: os.fd_t) bool { fn isId(fd: os.fd_t) bool {

View File

@ -162,7 +162,13 @@ fn execute(
while (it.next()) |packed_user| { while (it.next()) |packed_user| {
const offset = packed_user.additional_gids_offset; const offset = packed_user.additional_gids_offset;
const additional_gids = db.additional_gids[offset..]; const additional_gids = db.additional_gids[offset..];
const vit = compress.varintSliceIteratorMust(additional_gids); const vit = compress.varintSliceIterator(additional_gids) catch |err| {
stderr.print(
"ERROR {s}: file '{s}' is corrupted or cannot be read\n",
.{ @errorName(err), db_file },
) catch {};
return 1;
};
// the primary gid of the user is never in "additional gids" // the primary gid of the user is never in "additional gids"
const ngroups = vit.remaining + 1; const ngroups = vit.remaining + 1;
if (ngroups > popUser.score) { if (ngroups > popUser.score) {

View File

@ -79,7 +79,7 @@ fn execute(
} else { } else {
stderr.print("bad argument {s}: expected passwd or group\n", .{ stderr.print("bad argument {s}: expected passwd or group\n", .{
myflags.args[0], myflags.args[0],
}) catch return 3; }) catch {};
return 1; return 1;
} }
}; };
@ -89,18 +89,36 @@ fn execute(
stderr.print( stderr.print(
"ERROR {s}: file '{s}' is corrupted or cannot be read\n", "ERROR {s}: file '{s}' is corrupted or cannot be read\n",
.{ @errorName(err), db_file }, .{ @errorName(err), db_file },
) catch return 3; ) catch {};
return 1; return 1;
}; };
defer file.close(); defer file.close();
// TODO: how can I handle error.Overflow in a single place? Can I catch on
// a block?
return switch (mode) { return switch (mode) {
.passwd => passwd(stdout, &file.db, myflags.args[1..]), .passwd => passwd(stdout, &file.db, myflags.args[1..]) catch |err| switch (err) {
.group => group(stdout, &file.db, myflags.args[1..]), error.Overflow => {
stderr.print(
"ERROR {s}: file '{s}' is corrupted or cannot be read\n",
.{ @errorName(err), db_file },
) catch {};
return 1;
},
},
.group => group(stdout, &file.db, myflags.args[1..]) catch |err| switch (err) {
error.Overflow => {
stderr.print(
"ERROR {s}: file '{s}' is corrupted or cannot be read\n",
.{ @errorName(err), db_file },
) catch {};
return 1;
},
},
}; };
} }
fn passwd(stdout: anytype, db: *const DB, keys: []const [*:0]const u8) u8 { fn passwd(stdout: anytype, db: *const DB, keys: []const [*:0]const u8) error{Overflow}!u8 {
if (keys.len == 0) if (keys.len == 0)
return passwdAll(stdout, db); return passwdAll(stdout, db);
var some_notfound = false; var some_notfound = false;
@ -134,7 +152,7 @@ fn passwdAll(stdout: anytype, db: *const DB) u8 {
return 0; return 0;
} }
fn group(stdout: anytype, db: *const DB, keys: []const [*:0]const u8) u8 { fn group(stdout: anytype, db: *const DB, keys: []const [*:0]const u8) error{Overflow}!u8 {
if (keys.len == 0) if (keys.len == 0)
return groupAll(stdout, db); return groupAll(stdout, db);
var some_notfound = false; var some_notfound = false;
@ -151,31 +169,31 @@ fn group(stdout: anytype, db: *const DB, keys: []const [*:0]const u8) u8 {
continue; continue;
}; };
if (printGroup(stdout, db, &g)) |exit_code| if (try printGroup(stdout, db, &g)) |exit_code|
return exit_code; return exit_code;
} }
return if (some_notfound) 2 else 0; return if (some_notfound) 2 else 0;
} }
fn groupAll(stdout: anytype, db: *const DB) u8 { fn groupAll(stdout: anytype, db: *const DB) error{Overflow}!u8 {
var it = PackedGroup.iterator(db.groups, db.header.num_groups); var it = PackedGroup.iterator(db.groups, db.header.num_groups);
while (it.next()) |g| while (it.next()) |g|
if (printGroup(stdout, db, &g)) |exit_code| if (try printGroup(stdout, db, &g)) |exit_code|
return exit_code; return exit_code;
return 0; return 0;
} }
fn printGroup(stdout: anytype, db: *const DB, g: *const PackedGroup) ?u8 { fn printGroup(stdout: anytype, db: *const DB, g: *const PackedGroup) error{Overflow}!?u8 {
// not converting to Group to save a few memory allocations. // not converting to Group to save a few memory allocations.
stdout.print("{s}:x:{d}:", .{ g.name(), g.gid() }) catch return 3; stdout.print("{s}:x:{d}:", .{ g.name(), g.gid() }) catch return 3;
var it = DB.groupMembersIter(db.groupmembers[g.members_offset..]); var it = try DB.groupMembersIter(db.groupmembers[g.members_offset..]);
// lines will be buffered, flushed on every EOL. // lines will be buffered, flushed on every EOL.
var line_writer = io.bufferedWriter(stdout); var line_writer = io.bufferedWriter(stdout);
var i: usize = 0; var i: usize = 0;
while (it.nextMust()) |member_offset| : (i += 1) { while (try it.next()) |member_offset| : (i += 1) {
const puser = PackedUser.fromBytes(@alignCast(8, db.users[member_offset << 3 ..])); const puser = PackedUser.fromBytes(@alignCast(8, db.users[member_offset << 3 ..]));
const name = puser.user.name(); const name = puser.user.name();
if (i != 0) if (i != 0)