[PATCH 4/4] MultiAdmin module
- Add the MultiAdmin to the mainline tree.
I hope the rest is self-explanatory :)
Signed-off-by: Jan Engelhardt <[email protected]>, May 01 2006
Modified July 11 2006
---
security/Kconfig | 17 +
security/Makefile | 3
security/multiadm.c | 628 ++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 648 insertions(+)
Index: linux-2.6.23.1/security/Kconfig
===================================================================
--- linux-2.6.23.1.orig/security/Kconfig
+++ linux-2.6.23.1/security/Kconfig
@@ -81,6 +81,23 @@ config SECURITY_NETWORK_XFRM
IPSec.
If you are unsure how to answer this question, answer N.
+config SECURITY_MULTIADM
+ tristate "MultiAdmin security module"
+ depends on SECURITY
+ select SECURITY_CAPABILITIES
+ ---help---
+ The MultiAdmin security kernel module provides means to have multiple
+ "root" users with unique UIDs. This fixes collation order problems
+ which for example appear with NSCD, allows to have files with
+ determinable owner and allows to track the quota usage for every
+ user, since they now have a unique uid.
+
+ It also implements a "sub-admin", a partially restricted root user
+ (or enhanced normal user, depending on the way you see it), who has
+ full read-only access to most subsystems, and additional write rights
+ only to a limited subset, e.g. writing to files or killing processes
+ only of certain users.
+
config SECURITY_CAPABILITIES
tristate "Default Linux Capabilities"
depends on SECURITY
Index: linux-2.6.23.1/security/Makefile
===================================================================
--- linux-2.6.23.1.orig/security/Makefile
+++ linux-2.6.23.1/security/Makefile
@@ -2,6 +2,9 @@
# Makefile for the kernel security code
#
+obj-$(CONFIG_SECURITY_MULTIADM) += multiadm.o
+CFLAGS_multiadm.o += $(if $(wildcard security/apparmor),-DAPPARMOR,)
+
obj-$(CONFIG_KEYS) += keys/
subdir-$(CONFIG_SECURITY_SELINUX) += selinux
Index: linux-2.6.23.1/security/multiadm.c
===================================================================
--- /dev/null
+++ linux-2.6.23.1/security/multiadm.c
@@ -0,0 +1,628 @@
+/*
+ * MultiAdmin Security Module
+ * Copyright © Jan Engelhardt <jengelh [at] gmx de>, 2005 - 2007
+ * v1.0.7, July 2007
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * version 2 or 3 as published by the Free Software Foundation.
+ */
+#include <linux/binfmts.h>
+#include <linux/capability.h>
+#include <linux/dcache.h>
+#include <linux/file.h>
+#include <linux/fs.h>
+#include <linux/init.h>
+#include <linux/ipc.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <linux/namei.h>
+#include <linux/sched.h>
+#include <linux/securebits.h>
+#include <linux/security.h>
+#include <linux/sem.h>
+#include <linux/types.h>
+#include <asm/siginfo.h>
+
+/* Out of tree helper */
+#if !defined(CONFIG_SECURITY_CAPABILITIES) && \
+ !defined(CONFIG_SECURITY_CAPABILITIES_MODULE)
+# error You need to have CONFIG_SECURITY_CAPABILITIES=y or =m \
+ for MultiAdmin to compile successfully.
+#endif
+
+#define BASENAME "multiadm"
+#define PREFIX BASENAME ": "
+
+static gid_t Supergid = -1;
+static gid_t Subgid = -1;
+static uid_t Superuid_start = 0;
+static uid_t Superuid_end = 0;
+static uid_t Subuid_start = -1;
+static uid_t Subuid_end = -1;
+static uid_t Wrtuid_start = -1;
+static uid_t Wrtuid_end = -1;
+static unsigned int Secondary = 0;
+
+module_param(Supergid, int, S_IRUSR | S_IWUSR);
+module_param(Superuid_start, int, S_IRUSR | S_IWUSR);
+module_param(Superuid_end, int, S_IRUSR | S_IWUSR);
+module_param(Subuid_start, int, S_IRUSR | S_IWUSR);
+module_param(Subuid_end, int, S_IRUSR | S_IWUSR);
+module_param(Subgid, int, S_IRUSR | S_IWUSR);
+module_param(Wrtuid_start, int, S_IRUGO | S_IWUSR);
+module_param(Wrtuid_end, int, S_IRUGO | S_IWUSR);
+MODULE_PARM_DESC(Wrtuid_start, "First UID of the write-enabled user range");
+MODULE_PARM_DESC(Wrtuid_end, "Last UID of the write-enabled user range");
+MODULE_PARM_DESC(Superuid_start, "First UIDs of the superadmin range");
+MODULE_PARM_DESC(Superuid_end, "Last UID of the superadmin range");
+MODULE_PARM_DESC(Supergid, "Superadmin GID");
+MODULE_PARM_DESC(Subuid_start, "First UIDs of the subadmin range");
+MODULE_PARM_DESC(Subuid_end, "Last UID of the subadmin range");
+MODULE_PARM_DESC(Subgid, "Subadmin GID");
+
+static inline void chg2_superadm(kernel_cap_t *c)
+{
+ cap_set_full(*c);
+ cap_lower(*c, CAP_SETPCAP);
+ /* Flag 31 is unused, but set */
+ cap_lower(*c, 31);
+ printk(KERN_WARNING "Changed to superadm\n");
+ return;
+}
+
+static inline void chg2_subadm(kernel_cap_t *c)
+{
+ cap_clear(*c);
+ cap_raise(*c, CAP_CHOWN);
+ cap_raise(*c, CAP_DAC_OVERRIDE);
+ cap_raise(*c, CAP_DAC_READ_SEARCH);
+ cap_raise(*c, CAP_FOWNER);
+ cap_raise(*c, CAP_KILL);
+ cap_raise(*c, CAP_SETUID);
+ cap_raise(*c, CAP_IPC_OWNER);
+ cap_raise(*c, CAP_SYS_PTRACE);
+ cap_raise(*c, CAP_SYS_ADMIN);
+ cap_raise(*c, CAP_SYS_NICE);
+ return;
+}
+
+static inline bool __is_uid_superadm(uid_t u)
+{
+ return (!issecure(SECURE_NOROOT) && u == 0) ||
+ (Superuid_start != -1 && Superuid_end != -1 &&
+ u >= Superuid_start && u <= Superuid_end);
+}
+
+static inline bool is_uid_superadm(uid_t u)
+{
+ bool r = __is_uid_superadm(u);
+ printk(KERN_WARNING "You are uid_superadm=%d\n", (int)r);
+ return r;
+}
+
+static inline bool is_gid_superadm(gid_t g)
+{
+ return Supergid != -1 && g == Supergid;
+}
+
+static inline bool is_any_superadm(uid_t u, gid_t g)
+{
+ return is_uid_superadm(u) || is_gid_superadm(g);
+}
+
+static inline bool is_uid_subadm(uid_t u)
+{
+ return Subuid_start != -1 && Subuid_end != -1 &&
+ u >= Subuid_start && u <= Subuid_end;
+}
+
+static inline bool is_gid_subadm(gid_t g)
+{
+ return Subgid != -1 && g == Subgid;
+}
+
+static inline bool is_any_subadm(uid_t u, gid_t g)
+{
+ return is_uid_subadm(u) || is_gid_subadm(g);
+}
+
+static inline bool is_uid_user(uid_t u)
+{
+ /*
+ * Special case Wrtuid_end == (unsigned) -1 means what it means:
+ * everything until the end. This is why there is no
+ * Wrtuid_end != -1 check.
+ */
+ return Wrtuid_start != -1 && u >= Wrtuid_start && u <= Wrtuid_end;
+}
+
+static inline bool is_task1_user(const struct task_struct *task)
+{
+ return is_uid_user(task->uid) || is_uid_user(task->suid);
+}
+
+static inline bool is_task_user(const struct task_struct *task)
+{
+ return is_uid_user(task->euid) && is_uid_user(task->uid) &&
+ is_uid_user(task->suid);
+}
+
+static inline bool range_intersect(uid_t as, uid_t ae, uid_t bs, uid_t be)
+{
+ if(as == -1 || ae == -1 || bs == -1 || be == -1)
+ return 0;
+ return (long)ae >= (long)bs && (long)as <= (long)be;
+}
+
+static inline bool range_intersect_wrt(uid_t as, uid_t ae, uid_t bs, uid_t be)
+{
+ if(as == -1 || ae == -1 || bs == -1)
+ return 0;
+ return (long)ae >= (long)bs && (long)as <= (long)be;
+}
+
+static int mt_bprm_set_security(struct linux_binprm *bp)
+{
+ /*
+ * In the function chain of exec(), we eventually get here, which is
+ * the place to set up new privileges.
+ */
+ cap_bprm_set_security(bp);
+
+ /*
+ * All of the following is nicely inlined. The capability raising is
+ * resolved to only one instruction for each set.
+ */
+ if(is_any_superadm(bp->e_uid, bp->e_gid)) {
+ chg2_superadm(&bp->cap_permitted);
+ chg2_superadm(&bp->cap_effective);
+ } else if(is_any_superadm(current->uid, current->gid)) {
+ chg2_superadm(&bp->cap_permitted);
+ } else if(is_any_subadm(bp->e_uid, bp->e_gid)) {
+ chg2_subadm(&bp->cap_permitted);
+ chg2_subadm(&bp->cap_effective);
+ } else if(is_any_subadm(current->uid, current->gid)) {
+ chg2_subadm(&bp->cap_permitted);
+ }
+ return 0;
+}
+
+static int mt_cap_extra(struct task_struct *task, unsigned int capability)
+{
+ /*
+ * Subadmin also has CAP_SYS_ADMIN, but if we get here, we did so by
+ * capable() -- not capable_light().
+ */
+ if (capability != CAP_SYS_ADMIN)
+ return 0;
+ if (!is_any_superadm(current->euid, current->egid))
+ return -EPERM;
+ return 0;
+}
+
+static int mt_inode_permission(struct inode *inode, int mask,
+ struct nameidata *nd)
+{
+ /*
+ * Check for superadmin is not done, since the only users that can get
+ * here is either superadmin or subadmin. By omitting the check for
+ * superadmin, only two comparisons need to be done for the subadmin
+ * case. This method is done almost throughout the entire module.
+ */
+ int ret;
+
+ if (is_any_subadm(current->euid, current->egid) &&
+ (mask & MAY_WRITE)) {
+ if (inode->i_uid == current->fsuid ||
+ is_uid_user(inode->i_uid))
+ return 0;
+
+ /*
+ * Since we practically jumped over the checks to get here
+ * (because of CAP_DAC_OVERRIDE), we need to do it again.
+ * Without CAP_DAC_OVERRIDE this time. Temporarily drop it.
+ */
+ cap_lower(current->cap_effective, CAP_DAC_OVERRIDE);
+
+ /* Copied from fs/namei.c */
+ if (inode->i_op != NULL && inode->i_op->permission != NULL)
+ ret = inode->i_op->permission(inode,
+ mask & ~MAY_APPEND, nd);
+ else
+ ret = generic_permission(inode,
+ mask & ~MAY_APPEND, NULL);
+
+ cap_raise(current->cap_effective, CAP_DAC_OVERRIDE);
+ return ret;
+ }
+ return 0;
+}
+
+#ifdef APPARMOR
+static int mt_inode_setattr(struct dentry *dentry, struct vfsmount *vfs,
+ struct iattr *attr)
+#else
+static int mt_inode_setattr(struct dentry *dentry, struct iattr *attr)
+#endif
+{
+ const struct inode *inode;
+
+ if (!is_any_subadm(current->euid, current->egid))
+ return 0;
+
+ /*
+ * Change is only allowed if either the inode belongs to us, or
+ * does belond, _and_ will belong in case of ATTR_UID, to a WRT
+ * user.
+ */
+ inode = dentry->d_inode;
+
+ if (inode->i_uid != current->fsuid && !is_uid_user(inode->i_uid))
+ return -EPERM;
+
+ if ((attr->ia_valid & ATTR_UID) && attr->ia_uid != current->fsuid &&
+ !is_uid_user(attr->ia_uid))
+ return -EPERM;
+
+ return 0;
+}
+
+static int mt_ipc_permission(struct kern_ipc_perm *perm, short flag)
+{
+ int req, grant;
+ if (!is_any_subadm(current->euid, current->egid))
+ return 0;
+
+ if (perm->uid == current->euid || perm->cuid == current->euid ||
+ is_uid_user(perm->uid) || is_uid_user(perm->cuid))
+ return 0;
+
+ /*
+ * Copied and modified from ipc/util.c. Subadmin always has read
+ * permission so add S_IRUGO to granted. Checking the owner permission
+ * part is not done anymore, because it is done above.
+ */
+ req = (flag >> 6) | (flag >> 3) | flag;
+ grant = (perm->mode | S_IRUGO) >> 3;
+ if(in_group_p(perm->gid) || in_group_p(perm->cgid))
+ grant >>= 3;
+ if(req & ~grant & 0007)
+ return -EPERM;
+ return 0;
+}
+
+static int mt_msq_msgctl(struct msg_queue *msq, int cmd)
+{
+ if (!is_any_subadm(current->euid, current->egid))
+ return 0;
+
+ if (cmd == MSG_INFO || cmd == MSG_STAT ||
+ cmd == IPC_INFO || cmd == IPC_STAT)
+ return 0;
+
+ /* UID or CUID (creator UID) must fit */
+ if (msq != NULL && msq->q_perm.uid != current->euid &&
+ msq->q_perm.cuid != current->euid &&
+ !is_uid_user(msq->q_perm.uid) && !is_uid_user(msq->q_perm.cuid))
+ return -EPERM;
+
+ return 0;
+}
+
+static int mt_ptrace(struct task_struct *tracer, struct task_struct *task)
+{
+ if (!is_any_subadm(tracer->euid, tracer->egid))
+ return 0;
+
+ /*
+ * Ownership check according to kernel/ptrace.c:
+ * all of [RES][UG]ID must match the tracer's R[UG]ID.
+ */
+ if (task->euid == tracer->uid && task->uid == tracer->uid &&
+ task->suid == tracer->uid && task->egid == tracer->gid &&
+ task->gid == tracer->gid && task->sgid == tracer->gid)
+ return 0;
+
+ /* ...or all [RES]UIDs must match a WRT user */
+ if (!is_task_user(task))
+ return -EPERM;
+ return 0;
+}
+
+static int mt_quotactl(int cmd, int type, int id, struct super_block *sb)
+{
+ if (!is_any_subadm(current->euid, current->egid))
+ return 0;
+
+ switch(cmd) {
+ case Q_SYNC:
+ case Q_GETFMT:
+ case Q_GETINFO:
+ case Q_GETQUOTA:
+ case Q_XGETQUOTA:
+ case Q_XGETQSTAT:
+ case Q_XQUOTASYNC:
+ return 0;
+ }
+ return -EPERM;
+}
+
+static int mt_sem_semctl(struct sem_array *sem, int cmd)
+{
+ if (!is_any_subadm(current->euid, current->euid))
+ return 0;
+
+ if (cmd == SEM_INFO || cmd == IPC_INFO || cmd == SEM_STAT)
+ return 0;
+ if (sem != NULL) {
+ const struct kern_ipc_perm *perm = &sem->sem_perm;
+
+ if (perm->uid != current->euid &&
+ perm->cuid != current->euid &&
+ !is_uid_user(perm->uid) && !is_uid_user(perm->cuid))
+ return -EPERM;
+ }
+ return 0;
+}
+
+static int mt_shm_shmctl(struct shmid_kernel *shp, int cmd)
+{
+ if (!is_any_subadm(current->euid, current->egid))
+ return 0;
+
+ if (cmd == SHM_INFO || cmd == SHM_STAT ||
+ cmd == IPC_INFO || cmd == IPC_STAT)
+ return 0;
+ if (shp != NULL) {
+ const struct kern_ipc_perm *perm = &shp->shm_perm;
+
+ if (perm->uid != current->euid &&
+ perm->cuid != current->euid &&
+ !is_uid_user(perm->uid) && !is_uid_user(perm->cuid))
+ return -EPERM;
+ }
+ return 0;
+}
+
+static int mt_task_kill(struct task_struct *task, struct siginfo *si,
+ int sig, u32 secid)
+{
+ if (!is_any_subadm(current->euid, current->egid))
+ return 0;
+
+ /* As tricky as the ptrace() permission net. */
+ if(is_uid_user(task->uid) || is_uid_user(task->suid))
+ return 0;
+
+ /* Subadmin's own process */
+ if (task->uid == current->euid || task->suid == current->euid ||
+ task->uid == current->uid || task->suid == current->uid)
+ return 0;
+
+ /* SIG_IGN or a kernel-generated signal */
+ if (si != NULL && ((long)si == 1 || (long)si == 2 || !SI_FROMUSER(si)))
+ return 0;
+
+ /* For the case of a privileged subshell, but with the same tty */
+ if (sig == SIGCONT &&
+ task->signal->__session == current->signal->__session)
+ return 0;
+
+ return -EPERM;
+}
+
+static int mt_task_post_setuid(uid_t old_ruid, uid_t old_euid,
+ uid_t old_suid, int flags)
+{
+ int ret = cap_task_post_setuid(old_ruid, old_euid, old_suid, flags);
+
+ if (ret != 0)
+ return ret;
+
+ switch (flags) {
+ case LSM_SETID_ID:
+ case LSM_SETID_RE:
+ case LSM_SETID_RES:
+ /*
+ * Unlike bprm_set_security(), effective must be set
+ * independently.
+ */
+ if (is_uid_superadm(current->uid))
+ chg2_superadm(¤t->cap_permitted);
+ else if (is_uid_subadm(current->uid))
+ chg2_subadm(¤t->cap_permitted);
+
+ if (is_uid_superadm(current->euid))
+ chg2_superadm(¤t->cap_effective);
+ else if (is_uid_subadm(current->euid))
+ chg2_subadm(¤t->cap_effective);
+ break;
+ }
+ return 0;
+}
+
+static int mt_task_post_setgid(gid_t old_rgid, gid_t old_egid,
+ gid_t old_sgid, unsigned int flags)
+{
+ switch (flags) {
+ case LSM_SETID_ID:
+ case LSM_SETID_RE:
+ case LSM_SETID_RES:
+ if (is_gid_superadm(current->gid))
+ chg2_superadm(¤t->cap_permitted);
+ else if (is_gid_subadm(current->gid))
+ chg2_subadm(¤t->cap_permitted);
+
+ if (is_gid_superadm(current->egid))
+ chg2_superadm(¤t->cap_effective);
+ else if (is_gid_subadm(current->egid))
+ chg2_subadm(¤t->cap_effective);
+ break;
+ }
+ return 0;
+}
+
+static int mt_task_setuid(uid_t ruid, uid_t euid, uid_t suid, int flags)
+{
+ if (is_any_superadm(current->euid, current->egid))
+ return 0;
+
+ if (is_any_subadm(current->euid, current->egid))
+ if ((ruid == -1 || is_uid_user(ruid)) &&
+ (euid == -1 || is_uid_user(euid)) &&
+ (suid == -1 || is_uid_user(suid)))
+ return 0;
+
+ switch (flags) {
+ case LSM_SETID_ID:
+ if (current->uid == ruid || current->suid == ruid)
+ return 0;
+ break;
+ case LSM_SETID_RE:
+ if (current->euid == ruid || current->euid == euid ||
+ current->uid == ruid || current->uid == euid ||
+ current->suid == euid)
+ return 0;
+ break;
+ case LSM_SETID_RES:
+ if (current->euid == ruid || current->euid == euid ||
+ current->euid == suid || current->uid == ruid ||
+ current->uid == euid || current->uid == suid ||
+ current->suid == ruid || current->suid == euid ||
+ current->suid == suid)
+ return 0;
+ break;
+ case LSM_SETID_FS:
+ if (current->euid == ruid)
+ return 0;
+ break;
+ default:
+ WARN_ON(1);
+ break;
+ }
+ return -EPERM;
+}
+
+static int mt_task_setnice(struct task_struct *task, int nice)
+{
+ if (!is_any_subadm(current->euid, current->egid))
+ return 0;
+ if (task->euid != current->euid && task->uid != current->euid &&
+ !is_task1_user(task))
+ return -EPERM;
+ if (nice < 0)
+ return -EACCES;
+ return 0;
+}
+
+static int mt_task_setscheduler(struct task_struct *task, int policy,
+ struct sched_param *param)
+{
+ /*
+ * Return 0 for superuser and normal users. The latters' checks are
+ * performed in sched.c.
+ */
+ if (!is_any_subadm(current->euid, current->egid))
+ return 0;
+
+ /* Copied from kernel/sched.c:sched_setscheduler() */
+ if (task->policy != policy)
+ return -EPERM;
+
+ if (policy != SCHED_NORMAL &&
+ param->sched_priority > task->rt_priority &&
+ param->sched_priority > task->signal->rlim[RLIMIT_RTPRIO].rlim_cur)
+ return -EPERM;
+
+ if (task->uid != current->euid && task->suid != current->euid &&
+ !is_task1_user(task))
+ return -EPERM;
+
+ return 0;
+}
+
+static struct security_operations mt_secops = {
+ .bprm_apply_creds = cap_bprm_apply_creds,
+ .bprm_set_security = mt_bprm_set_security,
+ .cap_extra = mt_cap_extra,
+ .capable = cap_capable,
+ .capget = cap_capget,
+ .capset_check = cap_capset_check,
+ .capset_set = cap_capset_set,
+ .inode_permission = mt_inode_permission,
+ .inode_setattr = mt_inode_setattr,
+ .ipc_permission = mt_ipc_permission,
+ .msg_queue_msgctl = mt_msq_msgctl,
+ .ptrace = mt_ptrace,
+ .quotactl = mt_quotactl,
+ .sem_semctl = mt_sem_semctl,
+ .shm_shmctl = mt_shm_shmctl,
+ .task_kill = mt_task_kill,
+ .task_post_setuid = mt_task_post_setuid,
+ .task_post_setgid = mt_task_post_setgid,
+ .task_setnice = mt_task_setnice,
+ .task_setscheduler = mt_task_setscheduler,
+ .task_setuid = mt_task_setuid,
+};
+
+static __init int multiadm_init(void)
+{
+ int ret, ret2;
+
+ if ((ret = register_security(&mt_secops)) != 0) {
+ if ((ret2 = mod_reg_security(BASENAME, &mt_secops)) != 0) {
+ printk(KERN_WARNING PREFIX "Could not register with "
+ "kernel: %d, %d\n", ret, ret2);
+ return ret2;
+ }
+ Secondary = 1;
+ }
+
+ if (range_intersect(Superuid_start, Superuid_end,
+ Subuid_start, Subuid_end))
+ printk(KERN_WARNING PREFIX
+ "Superadmin and Subadmin ranges intersect! "
+ "Unpredictable behavior may result: some operations "
+ "may classify you as a superadmin, others as a "
+ "subadmin. Security leak: subadmin could possibly "
+ "change into superadmin!\n");
+
+ if (range_intersect(Superuid_start, Superuid_end,
+ Wrtuid_start, Wrtuid_end))
+ printk(KERN_WARNING PREFIX
+ "Superadmin and write-enabled user range intersect! "
+ "A subadmin could setuid() into a superadmin!\n");
+
+ if (range_intersect_wrt(Subuid_start, Subuid_end,
+ Wrtuid_start, Wrtuid_end))
+ printk(KERN_WARNING PREFIX
+ "Subadmin and write-enabled user range intersect! "
+ "Subadmins are able to poke on other subadmins!\n");
+
+ printk(KERN_INFO "MultiAdmin loaded\n");
+ return 0;
+}
+
+static __exit void multiadm_exit(void)
+{
+ int ret = 0;
+
+ if(Secondary)
+ ret = mod_unreg_security(BASENAME, &mt_secops);
+ else
+ ret = unregister_security(&mt_secops);
+
+ if(ret != 0)
+ printk(KERN_WARNING PREFIX
+ "Could not unregister with kernel: %d\n", ret);
+
+ return;
+}
+
+module_init(multiadm_init);
+module_exit(multiadm_exit);
+MODULE_DESCRIPTION("MultiAdmin Security Module");
+MODULE_AUTHOR("Jan Engelhardt <jengelh [at] gmx de>");
+MODULE_LICENSE("GPL");
-
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to [email protected]
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/
[Index of Archives]
[Kernel Newbies]
[Netfilter]
[Bugtraq]
[Photo]
[Stuff]
[Gimp]
[Yosemite News]
[MIPS Linux]
[ARM Linux]
[Linux Security]
[Linux RAID]
[Video 4 Linux]
[Linux for the blind]
[Linux Resources]