To avoid namespace_sem deadlock, this patch uses "current->last_vfsmount" associated by wrapper functions. Signed-off-by: Kentaro Takeda Signed-off-by: Tetsuo Handa security/tomoyo/tomoyo.c | 822 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 822 insertions(+) --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ linux-2.6-mm/security/tomoyo/tomoyo.c 2007-11-14 15:56:26.000000000 +0900 @@ -0,0 +1,822 @@ +/* + * security/tomoyo/tomoyo.c + * + * LSM hooks for TOMOYO Linux. + */ + +#include "tomoyo.h" +#include "realpath.h" + +#define MAX_SOCK_ADDR 128 /* net/socket.c */ + +LIST_HEAD(domain_list); + +static struct kmem_cache *tmy_cachep; + +static int tmy_task_alloc_security(struct task_struct *p) +{ + struct tmy_security *ptr = kmem_cache_alloc(tmy_cachep, GFP_KERNEL); + + if (!ptr) + return -ENOMEM; + memcpy(ptr, TMY_SECURITY, sizeof(*ptr)); + p->security = ptr; + return 0; +} + +static void tmy_task_free_security(struct task_struct *p) +{ + kmem_cache_free(tmy_cachep, p->security); +} + +static int tmy_bprm_alloc_security(struct linux_binprm *bprm) +{ + TMY_SECURITY->prev_domain = TMY_SECURITY->domain; + return 0; +} + +static int tmy_bprm_check_security(struct linux_binprm *bprm) +{ + struct domain_info *next_domain = NULL; + int retval = 0; + + tmy_load_policy(bprm->filename); + + /* + * TMY_CHECK_READ_FOR_OPEN_EXEC bit indicates whether this function is + * called by do_execve() or not. + * If called by do_execve(), I do domain transition. + */ + if (!(TMY_SECURITY->flags + & TMY_CHECK_READ_FOR_OPEN_EXEC)) { + retval = tmy_find_next_domain(bprm, &next_domain); + if (retval == 0) { + TMY_SECURITY->domain = next_domain; + TMY_SECURITY->flags |= + TMY_CHECK_READ_FOR_OPEN_EXEC; + } + } + + return retval; +} + +static void tmy_bprm_post_apply_creds(struct linux_binprm *bprm) +{ + TMY_SECURITY->prev_domain = TMY_SECURITY->domain; +} + +static void tmy_bprm_free_security(struct linux_binprm *bprm) +{ + TMY_SECURITY->domain = TMY_SECURITY->prev_domain; + TMY_SECURITY->flags &= ~TMY_CHECK_READ_FOR_OPEN_EXEC; +} + +static int tmy_sysctl(struct ctl_table *table, int op) +{ + int error; + char *name; + + if ((op & 6) == 0) + return 0; + + name = sysctlpath_from_table(table); + if (!name) + return -ENOMEM; + + error = tmy_file_perm(name, op & 6, "sysctl"); + tmy_free(name); + + return error; +} + +static int tmy_inode_permission(struct inode *inode, + int mask, + struct nameidata *nd) +{ + int flag = 0; + + if (S_ISDIR(inode->i_mode)) /* ignore because inode is directory */ + return 0; + /* + if (!nd) { + printk("tmy_inode_permission: NULL nameidata\n"); + dump_stack(); + return 0; + } else if (!nd->mnt) { + printk("tmy_inode_permission: NULL vfsmount\n"); + dump_stack(); + return 0; + } + */ + if (!nd || !nd->path.dentry || !nd->path.mnt) + return 0; + /* + * If called by other than do_execve(), I check for read permission of + * interpreter. + * Unlike DAC, I don't check for read permission of pathname passed to + * do_execve(). + * TOMOYO Linux checks for program's execute permission and + * interpreter's read permission. + */ + if ((mask == MAY_EXEC) && + (TMY_SECURITY->flags & TMY_CHECK_READ_FOR_OPEN_EXEC)) + mask = MAY_READ; + if ((mask == MAY_EXEC) || (mask == 0)) + return 0; + + if (mask == (MAY_READ | MAY_EXEC)) + flag |= O_RDONLY + 1; + else { + if (mask & MAY_READ) + flag |= O_RDONLY + 1; + if (mask & MAY_WRITE) + flag |= O_WRONLY + 1; + if ((mask & MAY_APPEND)) + flag |= O_APPEND; + } + + return tmy_open_perm(nd->path.dentry, nd->path.mnt, flag); +} + +static int tmy_do_single_write_perm(int operation, struct dentry *dentry) +{ + struct vfsmount *mnt = current->last_vfsmount; + if (!dentry || !mnt) + return 0; + if (!sbin_init_started) + return 0; + return tmy_single_write_perm(operation, dentry, mnt); +} + +static int tmy_inode_setattr(struct dentry *dentry, struct iattr *iattr) +{ + int err = 0; + unsigned int ia_valid = iattr->ia_valid; + if (ia_valid & ATTR_MODE) + err = tmy_capable(TMY_SYS_CHMOD); + if (!err && (ia_valid & (ATTR_UID | ATTR_GID))) + err = tmy_capable(TMY_SYS_CHOWN); + if (err) + return err; + if (ia_valid & ATTR_SIZE) + return tmy_do_single_write_perm(TMY_TYPE_TRUNCATE_ACL, dentry); + return 0; +} + +static int tmy_inode_create(struct inode *dir, struct dentry *dentry, int mode) +{ + return tmy_do_single_write_perm(TMY_TYPE_CREATE_ACL, dentry); +} + +static int tmy_inode_unlink(struct inode *dir, struct dentry *dentry) +{ + const int err = tmy_capable(TMY_SYS_UNLINK); + if (err) + return err; + return tmy_do_single_write_perm(TMY_TYPE_UNLINK_ACL, dentry); +} + +static int tmy_inode_mkdir(struct inode *dir, struct dentry *dentry, int mode) +{ + return tmy_do_single_write_perm(TMY_TYPE_MKDIR_ACL, dentry); +} + +static int tmy_inode_rmdir(struct inode *dir, struct dentry *dentry) +{ + return tmy_do_single_write_perm(TMY_TYPE_RMDIR_ACL, dentry); +} + +static int tmy_inode_symlink(struct inode *dir, + struct dentry *dentry, + const char *old_name) +{ + const int err = tmy_capable(TMY_SYS_SYMLINK); + if (err) + return err; + return tmy_do_single_write_perm(TMY_TYPE_SYMLINK_ACL, dentry); +} + +static int tmy_inode_mknod(struct inode *inode, + struct dentry *dentry, + int mode, + dev_t dev) +{ + int err; + if (S_ISCHR(mode)) { + err = tmy_capable(TMY_CREATE_CHAR_DEV); + if (err) + return err; + return tmy_do_single_write_perm(TMY_TYPE_MKCHAR_ACL, dentry); + } + if (S_ISBLK(mode)) { + err = tmy_capable(TMY_CREATE_BLOCK_DEV); + if (err) + return err; + return tmy_do_single_write_perm(TMY_TYPE_MKBLOCK_ACL, dentry); + } + if (S_ISFIFO(mode)) { + err = tmy_capable(TMY_CREATE_FIFO); + if (err) + return err; + return tmy_do_single_write_perm(TMY_TYPE_MKFIFO_ACL, dentry); + } + if (S_ISSOCK(mode)) { + err = tmy_capable(TMY_CREATE_UNIX_SOCKET); + if (err) + return err; + return tmy_do_single_write_perm(TMY_TYPE_MKSOCK_ACL, dentry); + } + + return 0; +} + +static int tmy_do_double_write_perm(int operation, + struct dentry *old_dentry, + struct dentry *new_dentry) +{ + struct vfsmount *mnt = current->last_vfsmount; + if (!old_dentry || !new_dentry || !mnt) + return 0; + if (!sbin_init_started) + return 0; + return tmy_double_write_perm(operation, old_dentry, mnt, + new_dentry, mnt); +} + +static int tmy_inode_link(struct dentry *old_dentry, + struct inode *inode, + struct dentry *new_dentry) +{ + const int err = tmy_capable(TMY_SYS_LINK); + if (err) + return err; + return tmy_do_double_write_perm(TMY_TYPE_LINK_ACL, + old_dentry, new_dentry); +} + +static int tmy_inode_rename(struct inode *old_inode, + struct dentry *old_dentry, + struct inode *new_inode, + struct dentry *new_dentry) +{ + const int err = tmy_capable(TMY_SYS_RENAME); + if (err) + return err; + return tmy_do_double_write_perm(TMY_TYPE_RENAME_ACL, + old_dentry, + new_dentry); +} + +static int tmy_file_fcntl(struct file *file, + unsigned int cmd, + unsigned long arg) +{ + if (!(arg & O_APPEND)) + return tmy_rewrite_perm(file); + return 0; +} + +static int tmy_file_ioctl(struct file *file, unsigned int cmd, + unsigned long arg) +{ + switch (cmd) { + case FIOCLEX: + case FIONCLEX: + case FIONBIO: + case FIOASYNC: + case FIOQSIZE: + return 0; + case FIBMAP: + case FIGETBSZ: + case FIONREAD: + if (S_ISREG(file->f_path.dentry->d_inode->i_mode)) + return 0; + } + return tmy_capable(TMY_SYS_IOCTL); +} + +static int tmy_socket_listen(struct socket *sock, int backlog) +{ + char addr[MAX_SOCK_ADDR]; + int addr_len; + int error; + + /* I don't check if called by kernel process. */ + if (segment_eq(get_fs(), KERNEL_DS)) + return 0; + + if (sock->type != SOCK_STREAM) + return 0; + if (sock->sk->sk_family != PF_INET && sock->sk->sk_family != PF_INET6) + return 0; + + error = tmy_capable(TMY_INET_STREAM_SOCKET_LISTEN); + if (error) + return error; + + if (sock->ops->getname(sock, (struct sockaddr *) addr, &addr_len, 0)) + return -EPERM; + + switch (((struct sockaddr *) addr)->sa_family) { + struct sockaddr_in6 *in6; + struct sockaddr_in *in; + + case AF_INET6: + in6 = (struct sockaddr_in6 *) addr; + error = tmy_network_listen_acl(1, in6->sin6_addr.s6_addr, + in6->sin6_port); + break; + case AF_INET: + in = (struct sockaddr_in *) addr; + error = tmy_network_listen_acl(0, (u8 *) &in->sin_addr, + in->sin_port); + break; + } + + return error; +} + +static int tmy_socket_create(int family, int type, int protocol, int kern) +{ + int error = 0; + if (kern) + return 0; + if (family == PF_INET || family == PF_INET6) { + switch (type) { + case SOCK_STREAM: + return tmy_capable(TMY_INET_STREAM_SOCKET_CREATE); + break; + case SOCK_DGRAM: + return tmy_capable(TMY_USE_INET_DGRAM_SOCKET); + break; + case SOCK_RAW: + return tmy_capable(TMY_USE_INET_RAW_SOCKET); + break; + } + } else if (family == PF_PACKET) { + return tmy_capable(TMY_USE_PACKET_SOCKET); + } else if (family == PF_ROUTE) { + return tmy_capable(TMY_USE_ROUTE_SOCKET); + } + return error; +} + +static int tmy_socket_connect(struct socket *sock, + struct sockaddr *addr, + int addr_len0) +{ + unsigned int addr_len = (unsigned int) addr_len0; + int error = 0; + const unsigned int type = sock->type; + + /* I don't check if called by kernel process. */ + if (segment_eq(get_fs(), KERNEL_DS)) + return 0; + + if (type == SOCK_STREAM) { + error = tmy_capable(TMY_INET_STREAM_SOCKET_CONNECT); + if (error) + return error; + } + + if (type != SOCK_STREAM && type != SOCK_DGRAM && type != SOCK_RAW) + return 0; + + switch (addr->sa_family) { + struct sockaddr_in6 *in6; + struct sockaddr_in *in; + + case AF_INET6: + if (addr_len < SIN6_LEN_RFC2133) + break; + + in6 = (struct sockaddr_in6 *) addr; + if (type != SOCK_RAW) + error = tmy_network_connect_acl(1, type, + in6->sin6_addr.s6_addr, + in6->sin6_port); + else { + const u16 port = htons(sock->sk->sk_protocol); + + error = tmy_network_connect_acl(1, SOCK_RAW, + in6->sin6_addr.s6_addr, + port); + } + break; + + case AF_INET: + if (addr_len < sizeof(struct sockaddr_in)) + break; + + in = (struct sockaddr_in *) addr; + if (type != SOCK_RAW) + error = tmy_network_connect_acl(0, type, + (u8 *) &in->sin_addr, + in->sin_port); + else { + const u16 port = htons(sock->sk->sk_protocol); + + error = tmy_network_connect_acl(0, SOCK_RAW, + (u8 *) &in->sin_addr, + port); + } + break; + } + + return error; +} + +static int tmy_socket_bind(struct socket *sock, + struct sockaddr *addr, + int addr_len0) +{ + unsigned int addr_len = (unsigned int) addr_len0; + int error = 0; + const unsigned int type = sock->type; + + /* I don't check if called by kernel process. */ + if (segment_eq(get_fs(), KERNEL_DS)) + return 0; + + if (type != SOCK_STREAM && type != SOCK_DGRAM && type != SOCK_RAW) + return error; + + switch (addr->sa_family) { + struct sockaddr_in6 *in6; + struct sockaddr_in *in; + + case AF_INET6: + if (addr_len < SIN6_LEN_RFC2133) + break; + + in6 = ((struct sockaddr_in6 *) addr); + if (type != SOCK_RAW) + error = tmy_network_bind_acl(1, type, + in6->sin6_addr.s6_addr, + in6->sin6_port); + else { + const u16 port = htons(sock->sk->sk_protocol); + + error = tmy_network_bind_acl(1, SOCK_RAW, + in6->sin6_addr.s6_addr, + port); + } + break; + + case AF_INET: + if (addr_len < sizeof(struct sockaddr_in)) + break; + + in = (struct sockaddr_in *) addr; + if (type != SOCK_RAW) + error = tmy_network_bind_acl(0, type, + (u8 *) &in->sin_addr, + in->sin_port); + else { + const u16 port = htons(sock->sk->sk_protocol); + + error = tmy_network_bind_acl(0, SOCK_RAW, + (u8 *) &in->sin_addr, + port); + } + break; + } + + return error; +} + +static int tmy_socket_sendmsg(struct socket *sock, struct msghdr *msg, int size) +{ + int error = 0; + const int type = sock->type; + struct sockaddr *addr = (struct sockaddr *) msg->msg_name; + const unsigned int addr_len = msg->msg_namelen; + + /* I don't check if called by kernel process. */ + if (segment_eq(get_fs(), KERNEL_DS)) + return 0; + + if (!addr || (type != SOCK_DGRAM && type != SOCK_RAW)) + return error; + + switch (addr->sa_family) { + struct sockaddr_in6 *in6; + struct sockaddr_in *in; + u16 port; + + case AF_INET6: + if (addr_len < SIN6_LEN_RFC2133) + break; + + in6 = (struct sockaddr_in6 *) addr; + port = htons(sock->sk->sk_protocol); + error = tmy_network_sendmsg_acl(1, type, in6->sin6_addr.s6_addr, + type == SOCK_DGRAM ? + in6->sin6_port : port); + break; + + case AF_INET: + if (addr_len < sizeof(struct sockaddr_in)) + break; + + in = (struct sockaddr_in *) addr; + port = htons(sock->sk->sk_protocol); + error = tmy_network_sendmsg_acl(0, type, (u8 *) &in->sin_addr, + type == SOCK_DGRAM ? + in->sin_port : port); + break; + } + + return error; +} + +#ifdef TMY_LSM_EXPANSION +static int tmy_socket_post_accept(struct socket *sock, struct socket *newsock) +{ + int error = 0; + int addr_len; + char addr[MAX_SOCK_ADDR]; + struct sockaddr *sockaddr = (struct sockaddr *) addr; + + /* I don't check if called by kernel process. */ + if (segment_eq(get_fs(), KERNEL_DS)) + return 0; + + if ((newsock->sk->sk_family != PF_INET) && + (newsock->sk->sk_family != PF_INET6)) + return error; + + if (newsock->ops->getname(newsock, sockaddr, &addr_len, 2) == 0) { + switch (sockaddr->sa_family) { + struct sockaddr_in6 *in6; + struct sockaddr_in *in; + + case AF_INET6: + in6 = (struct sockaddr_in6 *) addr; + error = tmy_network_accept_acl(1, + in6->sin6_addr.s6_addr, + in6->sin6_port); + break; + + case AF_INET: + in = (struct sockaddr_in *) addr; + error = tmy_network_accept_acl(0, (u8 *) &in->sin_addr, + in->sin_port); + break; + } + } else + error = -EPERM; + + if (error) + return -ECONNABORTED; + return error; +} + +static int tmy_post_recv_datagram(struct sock *sk, + struct sk_buff *skb, + unsigned int flags) +{ + int error = 0; + const unsigned int type = sk->sk_type; + + /* skb_recv_datagram() didn't dequeue. */ + if (!skb) + return 0; + + /* skb_recv_datagram() can be called from interrupt context. */ + if (in_interrupt()) + return 0; + /* I don't check if called by kernel process. */ + if (segment_eq(get_fs(), KERNEL_DS)) + return 0; + + if (type != SOCK_DGRAM && type != SOCK_RAW) + return 0; + + switch (sk->sk_family) { + struct sockaddr_in6 sin6; + struct sockaddr_in sin; + u16 port; + + case AF_INET6: + + if (type == SOCK_DGRAM) { + /* UDP IPv6 */ + sin6.sin6_family = AF_INET6; + sin6.sin6_port = udp_hdr(skb)->source; + + if (skb->protocol == htons(ETH_P_IP)) + ipv6_addr_set(&sin6.sin6_addr, 0, 0, + htonl(0xffff), + ip_hdr(skb)->saddr); + else + ipv6_addr_copy(&sin6.sin6_addr, + &ipv6_hdr(skb)->saddr); + + port = sin6.sin6_port; + } else { + /* RAW IPv6 */ + sin6.sin6_family = AF_INET6; + sin6.sin6_port = 0; + ipv6_addr_copy(&sin6.sin6_addr, &ipv6_hdr(skb)->saddr); + + port = htons(sk->sk_protocol); + } + + error = tmy_network_recvmsg_acl(1, type, + sin6.sin6_addr.s6_addr, port); + + break; + + case AF_INET: + + if (type == SOCK_DGRAM) { + /* UDP IPv4 */ + sin.sin_family = AF_INET; + sin.sin_port = udp_hdr(skb)->source; + sin.sin_addr.s_addr = ip_hdr(skb)->saddr; + + port = sin.sin_port; + } else { + /* RAW IPv4 */ + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = ip_hdr(skb)->saddr; + sin.sin_port = 0; + + port = htons(sk->sk_protocol); + } + + error = tmy_network_recvmsg_acl(0, type, + (u8 *) &sin.sin_addr, port); + + break; + + } + + if (error) + error = -EAGAIN; + return error; +} +#endif + +static int tmy_sb_mount(char *dev_name, + struct nameidata *nd, + char *type, + unsigned long flags, + void *data) +{ + char *buf; + char *dir_name; + int error; + + error = tmy_capable(TMY_SYS_MOUNT); + if (error) + return error; + + buf = kmalloc(PAGE_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + dir_name = d_path(&nd->path, buf, PAGE_SIZE); + + if (IS_ERR(dir_name)) + error = PTR_ERR(dir_name); + else + error = tmy_mount_perm(dev_name, dir_name, type, flags); + + if (!error && (flags & MS_REMOUNT) == 0) + error = tmy_conceal_mount(nd); + + kfree(buf); + return error; +} + +static int tmy_sb_umount(struct vfsmount *mnt, int flags) +{ + const int err = tmy_capable(TMY_SYS_UMOUNT); + if (err) + return err; + return tmy_umount_perm(mnt); +} + +static int tmy_settime(struct timespec *ts, struct timezone *tz) +{ + return tmy_capable(TMY_SYS_SETTIME); +} + +static int tmy_sb_pivotroot(struct nameidata *old_nd, struct nameidata *new_nd) +{ + const int err = tmy_capable(TMY_SYS_PIVOT_ROOT); + if (err) + return err; + return tmy_pivot_root_perm(old_nd, new_nd); +} + +static int tmy_task_capable(struct task_struct *tsk, int cap) +{ + int err = 0; + if (cap == CAP_SYS_CHROOT) + err = tmy_capable(TMY_SYS_CHROOT); + else if (cap == CAP_SYS_TTY_CONFIG) + err = tmy_capable(TMY_SYS_VHANGUP); + else if (cap == CAP_SYS_BOOT) + err = tmy_capable(TMY_SYS_REBOOT); + else if (cap == CAP_SYS_TIME) + err = tmy_capable(TMY_SYS_SETTIME); + if (err) + return err; + if (cap_raised(tsk->cap_effective, cap)) + return 0; + return -EPERM; +} + +#ifdef TMY_LSM_EXPANSION +static int tmy_task_kill_unlocked(int pid, int sig) +{ + const int err = tmy_capable(TMY_SYS_KILL); + if (err) + return err; + return tmy_signal_acl(sig, pid); +} + +static int tmy_task_tkill_unlocked(int pid, int sig) +{ + const int err = tmy_capable(TMY_SYS_KILL); + if (err) + return err; + return tmy_signal_acl(sig, pid); +} + +static int tmy_task_tgkill_unlocked(int tgid, int pid, int sig) +{ + const int err = tmy_capable(TMY_SYS_KILL); + if (err) + return err; + return tmy_signal_acl(sig, pid); +} +#endif + +static struct security_operations tomoyo_security_ops = { + .task_alloc_security = tmy_task_alloc_security, + .task_free_security = tmy_task_free_security, + .bprm_alloc_security = tmy_bprm_alloc_security, + .bprm_check_security = tmy_bprm_check_security, + .bprm_post_apply_creds = tmy_bprm_post_apply_creds, + .bprm_free_security = tmy_bprm_free_security, + .sysctl = tmy_sysctl, + .inode_permission = tmy_inode_permission, + .inode_setattr = tmy_inode_setattr, + .inode_create = tmy_inode_create, + .inode_unlink = tmy_inode_unlink, + .inode_mkdir = tmy_inode_mkdir, + .inode_rmdir = tmy_inode_rmdir, + .inode_symlink = tmy_inode_symlink, + .inode_mknod = tmy_inode_mknod, + .inode_link = tmy_inode_link, + .inode_rename = tmy_inode_rename, + .file_fcntl = tmy_file_fcntl, + .file_ioctl = tmy_file_ioctl, + .socket_listen = tmy_socket_listen, + .socket_create = tmy_socket_create, + .socket_connect = tmy_socket_connect, + .socket_bind = tmy_socket_bind, + .socket_sendmsg = tmy_socket_sendmsg, + .sb_mount = tmy_sb_mount, + .sb_umount = tmy_sb_umount, + .sb_pivotroot = tmy_sb_pivotroot, + .settime = tmy_settime, + .capable = tmy_task_capable, +#ifdef TMY_LSM_EXPANSION + .socket_post_accept = tmy_socket_post_accept, + .post_recv_datagram = tmy_post_recv_datagram, + .task_kill_unlocked = tmy_task_kill_unlocked, + .task_tkill_unlocked = tmy_task_tkill_unlocked, + .task_tgkill_unlocked = tmy_task_tgkill_unlocked, +#endif +}; + +static int __init tmy_init(void) +{ + + /* register ourselves with the security framework */ + if (register_security(&tomoyo_security_ops)) + panic("Failure registering TOMOYO Linux"); + + printk(KERN_INFO "TOMOYO Linux initialized\n"); + + INIT_LIST_HEAD(&KERNEL_DOMAIN.list); + INIT_LIST_HEAD(&KERNEL_DOMAIN.acl_info_list); + KERNEL_DOMAIN.domainname = tmy_save_name(TMY_ROOT_NAME); + list_add_tail_mb(&KERNEL_DOMAIN.list, &domain_list); + tmy_cachep = kmem_cache_create("tomoyo_security", + sizeof(struct tmy_security), + 0, SLAB_PANIC, NULL); + init_task.security = kmem_cache_alloc(tmy_cachep, GFP_KERNEL); + ((struct tmy_security *) init_task.security)->domain = &KERNEL_DOMAIN; + ((struct tmy_security *) init_task.security)->prev_domain = NULL; + ((struct tmy_security *) init_task.security)->flags = 0; + + return 0; +} + +security_initcall(tmy_init); -- - To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to majordomo@vger.kernel.org More majordomo info at http://vger.kernel.org/majordomo-info.html Please read the FAQ at http://www.tux.org/lkml/