const std = @import("std"); const os = std.os; const fmt = std.fmt; const log = std.log; const mem = std.mem; const math = std.math; const heap = std.heap; const once = std.once; const Mutex = std.Thread.Mutex; const Allocator = mem.Allocator; const DB = @import("DB.zig"); const File = @import("File.zig"); const ErrCtx = @import("ErrCtx.zig"); const CGroup = @import("Group.zig").CGroup; const PackedGroup = @import("PackedGroup.zig"); const CUser = @import("User.zig").CUser; const PackedUser = @import("PackedUser.zig"); const ShellReader = @import("shell.zig").ShellReader; const c = @cImport({ @cInclude("nss.h"); }); const ENV_DB = "TURBONSS_DB"; const ENV_LOGLEVEL = "TURBONSS_LOGLEVEL"; const ENV_OMIT_MEMBERS = "TURBONSS_OMIT_MEMBERS"; export var turbonss_db_path: [:0]const u8 = "/etc/turbonss/db.turbo"; pub const log_level: std.log.Level = .debug; // State is a type of the global variable holding the process state: // the DB handle and all the iterators. const State = struct { file: File, omit_members: bool, getpwent_iterator_mu: Mutex = Mutex{}, getpwent_iterator: ?PackedUser.Iterator = null, getgrent_iterator_mu: Mutex = Mutex{}, getgrent_iterator: ?PackedGroup.Iterator = null, // allocator for initgroups_dyn_allocator: Allocator, }; // global_state is initialized on first call to an nss function var global_state: ?State = null; var global_init = once(init); // assigns State from environment variables et al fn init() void { //if (os.getenvZ(ENV_LOGLEVEL)) |env| { // const got = mem.sliceTo(env, 0); // if (mem.eql(u8, got, "0")) { // log_level = .err; // } else if (mem.eql(u8, got, "1")) { // log_level = .warn; // } else if (mem.eql(u8, got, "2")) { // log_level = .info; // } else if (mem.eql(u8, got, "3")) { // log_level = .debug; // } else { // std.debug.print( // "warning: unrecognized {s}={s}. Expected between 0 and 3\n", // .{ ENV_LOGLEVEL, got }, // ); // } //} const omit_members = blk: { if (os.getenvZ(ENV_OMIT_MEMBERS)) |env| { const got = mem.sliceTo(env, 0); if (mem.eql(u8, got, "1")) { break :blk true; } else if (mem.eql(u8, got, "0")) { break :blk false; } else if (mem.eql(u8, got, "auto")) { // not set, do autodiscover } else { std.debug.print( "warning: unrecognized {s}={s}. Expected 0, 1 or auto\n", .{ ENV_OMIT_MEMBERS, got }, ); } } if (os.argv.len == 0) break :blk false; break :blk mem.eql(u8, mem.sliceTo(os.argv[0], 0), "id"); }; log.debug("omitting members from getgr* calls: {any}\n", .{omit_members}); const fname = os.getenvZ(ENV_DB) orelse turbonss_db_path; log.debug("opening '{s}'", .{fname}); const file = File.open(fname) catch |err| { log.warn("open '{s}': {s}", .{ fname, @errorName(err) }); return; }; log.debug("turbonss database opened", .{}); global_state = State{ .file = file, .omit_members = omit_members, .initgroups_dyn_allocator = heap.raw_c_allocator, }; } export fn _nss_turbo_getpwuid_r( uid: c_uint, passwd: *CUser, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const db = getDBErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; return getpwuid_r(&db, uid, passwd, buffer, buflen, errnop); } fn getpwuid_r( db: *const DB, uid: c_uint, passwd: *CUser, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { var cuser = db.getpwuid(uid, buffer[0..buflen]) catch |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, }; const got_cuser = cuser orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; passwd.* = got_cuser; return c.NSS_STATUS_SUCCESS; } export fn _nss_turbo_getpwnam_r( name: [*:0]const u8, passwd: *CUser, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const db = getDBErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; return getpwnam_r(&db, name, passwd, buffer, buflen, errnop); } fn getpwnam_r( db: *const DB, name: [*:0]const u8, passwd: *CUser, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const nameSlice = mem.sliceTo(name, 0); var buf = buffer[0..buflen]; const cuser = db.getpwnam(nameSlice, buf) catch |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, }; const got_cuser = cuser orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; passwd.* = got_cuser; return c.NSS_STATUS_SUCCESS; } export fn _nss_turbo_getgrgid_r( gid: c_uint, gr: *CGroup, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const state = getStateErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; return getgrgid_r(&state, gid, gr, buffer, buflen, errnop); } fn getgrgid_r( state: *const State, gid: c_uint, gr: *CGroup, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const db = state.file.db; const omit_members = state.omit_members; var buf = buffer[0..buflen]; var cgroup = db.getgrgid(gid, buf, omit_members) catch |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, }; const got_cgroup = cgroup orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; gr.* = got_cgroup; return c.NSS_STATUS_SUCCESS; } export fn _nss_turbo_getgrnam_r( name: [*:0]const u8, group: *CGroup, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const state = getStateErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; return getgrnam_r(&state, name, group, buffer, buflen, errnop); } fn getgrnam_r( state: *const State, name: [*:0]const u8, group: *CGroup, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { const db = state.file.db; const omit_members = state.omit_members; const nameSlice = mem.sliceTo(name, 0); var buf = buffer[0..buflen]; var cgroup = db.getgrnam(nameSlice, buf, omit_members) catch |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, }; const got_cgroup = cgroup orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; group.* = got_cgroup; return c.NSS_STATUS_SUCCESS; } export fn _nss_turbo_setpwent(_: c_int) void { global_init.call(); var state = global_state orelse return; setpwent(&state); } fn setpwent(state: *State) void { state.getpwent_iterator_mu.lock(); defer state.getpwent_iterator_mu.unlock(); const db = state.file.db; state.getpwent_iterator = PackedUser.iterator( db.users, db.header.num_users, db.shellReader(), ); } export fn _nss_turbo_endpwent() void { global_init.call(); var state = global_state orelse return; endpwent(&state); } fn endpwent(state: *State) void { state.getpwent_iterator_mu.lock(); state.getpwent_iterator = null; state.getpwent_iterator_mu.unlock(); } export fn _nss_turbo_setgrent(_: c_int) void { var state = getState() orelse return; setgrent(&state); } fn setgrent(state: *State) void { state.getgrent_iterator_mu.lock(); defer state.getgrent_iterator_mu.unlock(); state.getgrent_iterator = PackedGroup.iterator( state.file.db.groups, ); } export fn _nss_turbo_endgrent() void { var state = getState() orelse return; endgrent(&state); } fn endgrent(state: *State) void { state.getgrent_iterator_mu.lock(); state.getgrent_iterator = null; state.getgrent_iterator_mu.unlock(); } export fn _nss_turbo_getgrent_r( result: *CGroup, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { var state = getStateErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; return getgrent_r(&state, result, buffer, buflen, errnop); } fn getgrent_r( state: *State, result: *CGroup, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { state.getgrent_iterator_mu.lock(); defer state.getgrent_iterator_mu.unlock(); var it = state.getgrent_iterator orelse { // logic from _nss_systemd_getgrent_r errnop.* = @enumToInt(os.E.HOSTDOWN); return c.NSS_STATUS_UNAVAIL; }; const group = it.next() orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; const cgroup1 = if (state.omit_members) DB.packCGroupNoMembers(&group, buffer[0..buflen]) else state.file.db.packCGroup(&group, buffer[0..buflen]); if (cgroup1) |cgroup| { result.* = cgroup; return c.NSS_STATUS_SUCCESS; } else |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, } } export fn _nss_turbo_getpwent_r( result: *CUser, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { var state = getStateErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; return getpwent_r(&state, result, buffer, buflen, errnop); } fn getpwent_r( state: *State, result: *CUser, buffer: [*]u8, buflen: usize, errnop: *c_int, ) c.enum_nss_status { state.getpwent_iterator_mu.lock(); defer state.getpwent_iterator_mu.unlock(); var it = state.getpwent_iterator orelse { // logic from _nss_systemd_getgrent_r errnop.* = @enumToInt(os.E.HOSTDOWN); return c.NSS_STATUS_UNAVAIL; }; const user = it.next() orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; const buf = buffer[0..buflen]; const cuser = state.file.db.writeUser(user, buf) catch |err| switch (err) { error.BufferTooSmall => { errnop.* = @enumToInt(os.E.RANGE); return c.NSS_STATUS_TRYAGAIN; }, }; result.* = cuser; return c.NSS_STATUS_SUCCESS; } export fn _nss_turbo_initgroups_dyn( user_name: [*:0]const u8, gid: u32, start: *c_long, size: *c_long, groupsp: *[*]u32, limit: c_long, errnop: *c_int, ) c.enum_nss_status { const state = getStateErrno(errnop) orelse return c.NSS_STATUS_UNAVAIL; return initgroups_dyn(&state, user_name, gid, start, size, groupsp, limit, errnop); } fn initgroups_dyn( state: *const State, user_name: [*:0]const u8, _: u32, start: *c_long, size: *c_long, groupsp: *[*]u32, limit: c_long, errnop: *c_int, ) c.enum_nss_status { const db = state.file.db; const user = db.getUserByName(mem.sliceTo(user_name, 0)) orelse { errnop.* = @enumToInt(os.E.NOENT); return c.NSS_STATUS_NOTFOUND; }; var gids = db.userGids(user.additional_gids_offset); if (size.* < gids.remaining()) { const oldsize = @intCast(usize, size.*); const newsize = if (limit <= 0) oldsize + gids.remaining() else math.min(@intCast(usize, limit), oldsize + gids.remaining()); var buf = groupsp.*[0..oldsize]; const new_groups = state.initgroups_dyn_allocator.realloc(buf, newsize); if (new_groups) |newgroups| { groupsp.* = newgroups.ptr; size.* = @intCast(c_long, newsize); } else |err| switch (err) { error.OutOfMemory => { errnop.* = @enumToInt(os.E.NOMEM); return c.NSS_STATUS_TRYAGAIN; }, } } // I was not able to understand the expected behavior of limit. In glibc // compat-initgroups.c limit is used to malloc the buffer. Something like // this is all over the place: // if (limit > 0 && *size == limit) /* We reached the maximum. */ // return; // Our implementation will thus limit the number of *added* entries. var added: usize = 0; while (gids.nextMust()) |gid| { if (limit > 0 and added == limit) break; added += 1; groupsp.*[@intCast(usize, start.*)] = @intCast(u32, gid); start.* += 1; } return if (added > 0) c.NSS_STATUS_SUCCESS else c.NSS_STATUS_NOTFOUND; } fn getState() ?State { global_init.call(); return global_state; } fn getStateErrno(errnop: *c_int) ?State { global_init.call(); if (global_state) |state| { return state; } else { errnop.* = @enumToInt(os.E.AGAIN); return null; } } fn getDBErrno(errnop: *c_int) ?DB { if (getStateErrno(errnop)) |state| { return state.file.db; } else return null; } const testing = std.testing; test "libnss getpwuid_r and getpwnam_r" { var tf = try File.TestDB.init(testing.allocator); defer tf.deinit(); const state = State{ .file = tf.file, .omit_members = false, .initgroups_dyn_allocator = testing.failing_allocator, }; var buf: [1024]u8 = undefined; var errno: c_int = 0; var passwd: CUser = undefined; try testing.expectEqual( c.NSS_STATUS_SUCCESS, getpwuid_r(&state.file.db, 128, &passwd, &buf, buf.len, &errno), ); try testing.expectEqual(@as(c_int, 0), errno); try testVidmantas(passwd); passwd = undefined; try testing.expectEqual( c.NSS_STATUS_SUCCESS, getpwnam_r(&state.file.db, "vidmantas", &passwd, &buf, buf.len, &errno), ); try testing.expectEqual(@as(c_int, 0), errno); try testVidmantas(passwd); passwd = undefined; try testing.expectEqual( c.NSS_STATUS_NOTFOUND, getpwnam_r(&state.file.db, "does not exist", &passwd, &buf, buf.len, &errno), ); try testing.expectEqual(@enumToInt(os.E.NOENT), @intCast(u16, errno)); passwd = undefined; var small_buffer: [1]u8 = undefined; try testing.expectEqual( c.NSS_STATUS_TRYAGAIN, getpwuid_r(&state.file.db, 0, &passwd, &small_buffer, small_buffer.len, &errno), ); try testing.expectEqual(@enumToInt(os.E.RANGE), @intCast(u16, errno)); } fn testVidmantas(u: CUser) !void { try testing.expectEqualStrings("vidmantas", mem.sliceTo(u.pw_name, 0)); try testing.expectEqual(@as(u32, 128), u.pw_uid); try testing.expectEqual(@as(u32, 128), u.pw_gid); try testing.expectEqualStrings("Vidmantas Kaminskas", mem.sliceTo(u.pw_gecos, 0)); try testing.expectEqualStrings("/bin/bash", mem.sliceTo(u.pw_shell, 0)); } test "libnss getgrgid_r and getgrnam_r" { var tf = try File.TestDB.init(testing.allocator); defer tf.deinit(); const turbonss_db_path_old = turbonss_db_path; turbonss_db_path = tf.path; defer { turbonss_db_path = turbonss_db_path_old; } var buf: [1024]u8 = undefined; var errno: c_int = 0; var group: CGroup = undefined; try testing.expectEqual( c.NSS_STATUS_SUCCESS, _nss_turbo_getgrgid_r(128, &group, &buf, buf.len, &errno), ); try testing.expectEqual(@as(c_int, 0), errno); try testVidmantasGroup(group); group = undefined; try testing.expectEqual( c.NSS_STATUS_SUCCESS, _nss_turbo_getgrnam_r("vidmantas", &group, &buf, buf.len, &errno), ); try testing.expectEqual(@as(c_int, 0), errno); try testVidmantasGroup(group); group = undefined; try testing.expectEqual( c.NSS_STATUS_NOTFOUND, _nss_turbo_getgrnam_r("does not exist", &group, &buf, buf.len, &errno), ); try testing.expectEqual(@enumToInt(os.E.NOENT), @intCast(u16, errno)); } test "libnss initgroups_dyn" { const allocator = testing.allocator; var tf = try File.TestDB.init(allocator); defer tf.deinit(); var state = State{ .file = tf.file, .omit_members = false, .initgroups_dyn_allocator = allocator, }; var size: c_long = 3; // size of the gids array is this var groups = try allocator.alloc(u32, @intCast(usize, size)); // buffer too small defer allocator.free(groups); var errno: c_int = 42; // canary groups[0] = 42; // canary var start: c_long = 1; // canary is there const status = initgroups_dyn( &state, "vidmantas", 0, // gid, ignored &start, &size, &groups.ptr, 3, // limit: besides canary accept 2 groups &errno, ); try testing.expectEqual(c.NSS_STATUS_SUCCESS, status); try testing.expectEqual(@as(c_int, 42), errno); try testing.expectEqual(@as(u32, 42), groups[0]); try testing.expectEqual(@as(u32, 128), groups[1]); try testing.expectEqual(@as(u32, 9999), groups[2]); } fn testVidmantasGroup(g: CGroup) !void { try testing.expectEqual(@as(u32, 128), g.gid); try testing.expectEqualStrings("vidmantas", mem.sliceTo(g.name, 0)); const members = g.members; try testing.expect(members[0] != null); try testing.expectEqualStrings("vidmantas", mem.sliceTo(members[0].?, 0)); }