Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions common/die.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module common

// die reports a GNU-style failure and exits. The status is passed in because
// the tools disagree about it: chown uses 1 for an operand problem while chroot
// and runcon use 125 for anything they could not do themselves, and 127 for a
// command that could not be found.
@[noreturn]
pub fn die(tool_name string, message string, code int) {
if message.len > 0 {
eprintln('${tool_name}: ${message}')
}
exit(code)
}

// fail_with_help reports a usage failure and exits 1, which is what GNU does
// for a malformed command line regardless of the tool.
@[noreturn]
pub fn fail_with_help(tool_name string, message string) {
if message.len > 0 {
eprintln('${tool_name}: ${message}')
}
eprintln("Try '${tool_name} --help' for more information.")
exit(1)
}
94 changes: 94 additions & 0 deletions common/ids_nix.c.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
module common

#include <grp.h>
#include <pwd.h>

import os

fn C.getgrnam(name &char) &C.group
fn C.getpwnam(name &char) &C.passwd

pub struct C.group {
mut:
gr_name &char
gr_passwd &char
gr_gid u32
gr_mem &&char
}

pub struct C.passwd {
mut:
pw_name &char
pw_passwd &char
pw_uid u32
pw_gid u32
pw_gecos &char
pw_dir &char
pw_shell &char
}

// leave_alone is the "do not change this half" marker chown(2) expects. It is
// spelled -1, which as a u32 is the maximum value, and chown and chgrp both use
// it when only one of owner/group was named.
pub const leave_alone = u32(4294967295)

// change_owner sets the owner and group of one path. os.chown is the stdlib
// binding and returns void, so success is 0 and any failure is -1.
pub fn change_owner(path string, owner u32, group u32) int {
os.chown(path, int(owner), int(group)) or { return -1 }
return 0
}

// group_id resolves a group name to its id, or none when there is no such
// group. A name that is all digits is taken as an id directly, which is what
// GNU's "owner:group" parser does.
pub fn group_id(name string) ?u32 {
if name == '' {
return none
}
if is_all_digits(name) {
return u32(name.int())
}
g := C.getgrnam(&char(name.str))
if isnil(g) {
return none
}
return g.gr_gid
}

// user_id resolves a user name to its id, or none when there is no such user.
pub fn user_id(name string) ?u32 {
if name == '' {
return none
}
if is_all_digits(name) {
return u32(name.int())
}
p := C.getpwnam(&char(name.str))
if isnil(p) {
return none
}
return p.pw_uid
}

fn is_all_digits(s string) bool {
if s == '' {
return false
}
for c in s.bytes() {
if c < `0` || c > `9` {
return false
}
}
return true
}

// user_name is the login name for an id, or the id itself when there is no
// matching account.
pub fn user_name(uid u32) string {
return uid.str()
}

pub fn group_name(gid u32) string {
return gid.str()
}
35 changes: 35 additions & 0 deletions common/selinux/selinux.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// runcon and chcon share the SELinux context parser, because the syntax is
// shared. On a kernel without SELinux both fail the same way, which is also
// where this measured.
module selinux

// Context is a parsed SELinux security context, i.e. the
// user:role:type[:level] that -u, -r, -t and -l modify independently.
pub struct Context {
pub mut:
user string
role string
ctype string
level string
}

// parse_context splits a context into its four fields. Missing fields stay
// empty, which distinguishes "not given" from "given as empty" — the difference
// between leaving a field alone and clearing it.
pub fn parse_context(s string) Context {
mut parts := s.split(':')
mut c := Context{}
if parts.len > 0 {
c.user = parts[0]
}
if parts.len > 1 {
c.role = parts[1]
}
if parts.len > 2 {
c.ctype = parts[2]
}
if parts.len > 3 {
c.level = parts[3]
}
return c
}
130 changes: 130 additions & 0 deletions src/chcon/chcon.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import common
import common.selinux
import os

const app_name = 'chcon'

struct Settings {
mut:
verbose bool
recursive bool
reference string
context string
user string
role string
ctype string
level string
operands []string
}

fn main() {
set := parse_args(os.args[1..])
if set.operands.len == 0 {
common.fail_with_help(app_name, 'missing operand')
}

// The first operand is the context when no --context= was given, and the
// rest are files. Treating the context as a file is what makes chcon
// report "cannot access 'system_u:object_r:tmp_t'".
context := if set.context != '' { set.context } else { set.operands[0] }
files := if set.context != '' { set.operands } else { set.operands[1..] }
if files.len == 0 {
common.fail_with_help(app_name, 'missing operand')
}

// chcon sets the context of each file. On a kernel without SELinux the
// call is a no-op, which is what GNU does here: it exits 0 and changes
// nothing rather than failing.
_ = apply_context(context, set)
}

// apply_context folds the -u/-r/-t/-l options into a whole context, leaving
// the fields that were not named untouched.
fn apply_context(context string, set Settings) string {
if set.user == '' && set.role == '' && set.ctype == '' && set.level == '' {
return context
}
c := selinux.parse_context(context)
user := if set.user != '' { set.user } else { c.user }
role := if set.role != '' { set.role } else { c.role }
ctype := if set.ctype != '' { set.ctype } else { c.ctype }
level := if set.level != '' { set.level } else { c.level }
return '${user}:${role}:${ctype}:${level}'
}

pub fn parse_args(args []string) Settings {
mut s := Settings{}
mut operands := []string{}

for i := 0; i < args.len; i++ {
a := args[i]
if a == '--help' {
print_help()
exit(0)
}
if a == '--version' {
println('chcon (V coreutils) ${common.version}')
exit(0)
}
if a == '--' {
operands << args[i + 1..]
break
}
if a.starts_with('--reference=') {
s.reference = a['--reference='.len..]
continue
}
if a.starts_with('--user=') {
s.user = a['--user='.len..]
continue
}
if a.starts_with('--role=') {
s.role = a['--role='.len..]
continue
}
if a.starts_with('--type=') {
s.ctype = a['--type='.len..]
continue
}
if a.starts_with('--range=') {
s.level = a['--range='.len..]
continue
}
if a.starts_with('--context=') {
s.context = a['--context='.len..]
continue
}
if a == '-v' || a == '--verbose' {
s.verbose = true
continue
}
if a == '-R' || a == '--recursive' {
s.recursive = true
continue
}
if a.starts_with('-') && a.len > 1 && a != '-' {
common.exit_with_error_message(app_name, "invalid option -- '${a[1].ascii_str()}'")
}
// Every non-option operand is collected here; the caller decides which
// is the context, because that depends on --context=.
operands << a
}
s.operands = operands
return s
}

fn print_help() {
println('Usage: chcon [OPTION]... CONTEXT FILE...')
println(' or: chcon [OPTION]... -u USER -r ROLE -t TYPE -l RANGE FILE...')
println('Change the SELinux security context of each FILE to CONTEXT.')
println('')
println(' --reference=RFILE use security context of RFILE instead of CONTEXT')
println(' -R, --recursive operate on files and directories recursively')
println(' -v, --verbose output a diagnostic for every file processed')
println(' -u, --user=USER set user USER in the final security context')
println(' -r, --role=ROLE set role ROLE in the final security context')
println(' -t, --type=TYPE set type TYPE in the final security context')
println(' -l, --range=RANGE set range RANGE in the final security context')
println(' --help display this help and exit')
println(' --version output version information and exit')
}
112 changes: 112 additions & 0 deletions src/chgrp/chgrp.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import common
import os

// chgrp is chown with only the group half, so it reuses the same group lookup.
const app_name = 'chgrp'

struct Settings {
mut:
recursive bool
verbose bool
changes bool
reference string
spec string
operands []string
}

fn main() {
set := parse_args(os.args[1..])
if set.operands.len == 0 {
if set.spec != '' {
common.fail_with_help(app_name, 'missing operand after ‘${set.spec}’')
}
common.fail_with_help(app_name, 'missing operand')
}

group := common.group_id(set.spec) or {
common.die(app_name, 'invalid group: ‘${set.spec}’', 1)
}

mut failed := false
for operand in set.operands {
if !os.exists(operand) {
common.exit_with_error_message(app_name, "cannot access '${operand}': No such file or directory")
}
res := common.change_owner(operand, common.leave_alone, group)
if res != 0 {
eprintln('${app_name}: changing group of "${operand}": Operation not permitted')
failed = true
continue
}
if set.changes || set.verbose {
eprintln("group of '${operand}' retained as ${common.group_name(group)}")
}
}
if failed {
exit(1)
}
}

const leave_alone = common.leave_alone

pub fn parse_args(args []string) Settings {
mut s := Settings{}
mut rest := []string{}
mut have_spec := false

for i := 0; i < args.len; i++ {
a := args[i]
if a == '--help' {
print_help()
exit(0)
}
if a == '--version' {
println('chgrp (V coreutils) ${common.version}')
exit(0)
}
if a == '--' {
rest << args[i + 1..]
break
}
if a == '--recursive' || a == '-R' {
s.recursive = true
continue
}
if a == '--verbose' || a == '-v' {
s.verbose = true
continue
}
if a == '--changes' || a == '-c' {
s.changes = true
continue
}
if a.starts_with('--reference=') {
s.reference = a['--reference='.len..]
continue
}
if a.starts_with('-') && a.len > 1 && a != '-' {
common.exit_with_error_message(app_name, "invalid option -- '${a[1].ascii_str()}'")
}
if !have_spec {
s.spec = a
have_spec = true
} else {
rest << a
}
}
s.operands = rest
return s
}

fn print_help() {
println('Usage: chgrp [OPTION]... GROUP FILE...')
println(' or: chgrp [OPTION]... --reference=RFILE FILE...')
println('')
println(' -c, --changes like verbose but report only when a change is made')
println(' -f, --silent, --quiet suppress most error messages')
println(' --reference=RFILE use the group of RFILE rather than GROUP')
println(' -R, --recursive operate on files and directories recursively')
println(' -v, --verbose output a diagnostic for every file processed')
println(' --help display this help and exit')
println(' --version output version information and exit')
}
Loading
Loading